From ed2bd312a33c9001e25e8ed18b709ea9b40dc5b0 Mon Sep 17 00:00:00 2001 From: eeeXun Date: Thu, 26 Jan 2023 01:15:53 +0800 Subject: [PATCH] refactor: make translator to an interface --- config.go | 45 +++--- internal/{translate => lock}/lock.go | 2 +- internal/translate/{ => google}/language.go | 6 +- internal/translate/google/translator.go | 127 +++++++++++++++ internal/translate/google/tts.go | 68 ++++++++ internal/translate/translator.go | 168 +++----------------- main.go | 3 +- ui.go | 37 ++--- 8 files changed, 266 insertions(+), 190 deletions(-) rename internal/{translate => lock}/lock.go (94%) rename internal/translate/{ => google}/language.go (98%) create mode 100644 internal/translate/google/translator.go create mode 100644 internal/translate/google/tts.go diff --git a/config.go b/config.go index ddf3260..5b6337a 100644 --- a/config.go +++ b/config.go @@ -14,13 +14,14 @@ var ( hideBelow bool // default config defaultConfig = map[string]interface{}{ - "transparent": false, - "theme": "Gruvbox", - "source.language": "English", - "source.borderColor": "red", - "destination.language": "Chinese (Traditional)", - "destination.borderColor": "blue", - "hide_below": false, + "transparent": false, + "theme": "Gruvbox", + "source.borderColor": "red", + "destination.borderColor": "blue", + "source.google.language": "English", + "destination.google.language": "Chinese (Traditional)", + "hide_below": false, + "translator": "google", } ) @@ -62,15 +63,19 @@ func configInit() { } // setup - if len(*srcLangArg) > 0 { - translator.SrcLang = *srcLangArg - } else { - translator.SrcLang = config.GetString("source.language") - } - if len(*dstLangArg) > 0 { - translator.DstLang = *dstLangArg - } else { - translator.DstLang = config.GetString("destination.language") + switch config.GetString("translator") { + case "google": + translator = googleTranslate + if len(*srcLangArg) > 0 { + translator.SetSrcLang(*srcLangArg) + } else { + translator.SetSrcLang(config.GetString("source.google.language")) + } + if len(*dstLangArg) > 0 { + translator.SetDstLang(*dstLangArg) + } else { + translator.SetDstLang(config.GetString("destination.google.language")) + } } hideBelow = config.GetBool("hide_below") style.Theme = config.GetString("theme") @@ -85,15 +90,15 @@ func updateConfig() { // Source language is not passed in argument if len(*srcLangArg) == 0 && - config.GetString("source.language") != translator.SrcLang { + config.GetString("source.google.language") != googleTranslate.GetSrcLang() { changed = true - config.Set("source.language", translator.SrcLang) + config.Set("source.google.language", googleTranslate.GetSrcLang()) } // Destination language is not passed in argument if len(*dstLangArg) == 0 && - config.GetString("destination.language") != translator.DstLang { + config.GetString("destination.google.language") != googleTranslate.GetDstLang() { changed = true - config.Set("destination.language", translator.DstLang) + config.Set("destination.google.language", googleTranslate.GetDstLang()) } if config.GetBool("hide_below") != hideBelow { changed = true diff --git a/internal/translate/lock.go b/internal/lock/lock.go similarity index 94% rename from internal/translate/lock.go rename to internal/lock/lock.go index 3f5973d..8cfe944 100644 --- a/internal/translate/lock.go +++ b/internal/lock/lock.go @@ -1,4 +1,4 @@ -package translate +package lock type Lock struct { Stop bool diff --git a/internal/translate/language.go b/internal/translate/google/language.go similarity index 98% rename from internal/translate/language.go rename to internal/translate/google/language.go index 94cb10f..91b4711 100644 --- a/internal/translate/language.go +++ b/internal/translate/google/language.go @@ -1,8 +1,8 @@ -package translate +package google // https://cloud.google.com/translate/docs/languages var ( - Lang = []string{ + lang = []string{ "Afrikaans", "Albanian", "Amharic", @@ -113,7 +113,7 @@ var ( "Yoruba", "Zulu", } - LangCode = map[string]string{ + langCode = map[string]string{ "Afrikaans": "af", "Albanian": "sq", "Amharic": "am", diff --git a/internal/translate/google/translator.go b/internal/translate/google/translator.go new file mode 100644 index 0000000..ee63017 --- /dev/null +++ b/internal/translate/google/translator.go @@ -0,0 +1,127 @@ +package google + +import ( + "encoding/json" + "errors" + "fmt" + "gtt/internal/lock" + "io/ioutil" + "net/http" + "net/url" +) + +const ( + textURL = "https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&dt=bd&dt=md&dt=ex&sl=%s&tl=%s&q=%s" +) + +type GoogleTranslate struct { + srcLang string + dstLang string + SoundLock *lock.Lock +} + +func (t *GoogleTranslate) GetAllLang() []string { + return lang +} + +func (t *GoogleTranslate) GetSrcLang() string { + return t.srcLang +} + +func (t *GoogleTranslate) GetDstLang() string { + return t.dstLang +} + +func (t *GoogleTranslate) SetSrcLang(srcLang string) { + t.srcLang = srcLang +} + +func (t *GoogleTranslate) SetDstLang(dstLang string) { + t.dstLang = dstLang +} + +func (t *GoogleTranslate) SwapLang() { + t.srcLang, t.dstLang = t.dstLang, t.srcLang +} + +func (t *GoogleTranslate) Translate(message string) ( + translation string, + definition string, + partOfSpeech string, + err error) { + var data []interface{} + + urlStr := fmt.Sprintf( + textURL, + langCode[t.srcLang], + langCode[t.dstLang], + url.QueryEscape(message), + ) + res, err := http.Get(urlStr) + if err != nil { + return "", "", "", err + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", "", "", err + } + + if err = json.Unmarshal(body, &data); err != nil { + return "", "", "", err + } + + if len(data) > 0 { + // translation = data[0] + for _, lines := range data[0].([]interface{}) { + translatedLine := lines.([]interface{})[0] + translation += fmt.Sprintf("%v", translatedLine) + } + + // part of speech = data[1] + if data[1] != nil { + for _, parts := range data[1].([]interface{}) { + // part of speech + part := parts.([]interface{})[0] + partOfSpeech += fmt.Sprintf("[%v]\n", part) + for _, words := range parts.([]interface{})[2].([]interface{}) { + // dst lang + dstWord := words.([]interface{})[0] + partOfSpeech += fmt.Sprintf("\t%v:", dstWord) + // src lang + firstWord := true + for _, word := range words.([]interface{})[1].([]interface{}) { + if firstWord { + partOfSpeech += fmt.Sprintf(" %v", word) + firstWord = false + } else { + partOfSpeech += fmt.Sprintf(", %v", word) + } + } + partOfSpeech += "\n" + } + } + } + + // definition = data[12] + if len(data) >= 13 && data[12] != nil { + for _, parts := range data[12].([]interface{}) { + // part of speech + part := parts.([]interface{})[0] + definition += fmt.Sprintf("[%v]\n", part) + for _, sentences := range parts.([]interface{})[1].([]interface{}) { + // definition + def := sentences.([]interface{})[0] + definition += fmt.Sprintf("\t- %v\n", def) + // example sentence + if len(sentences.([]interface{})) >= 3 && sentences.([]interface{})[2] != nil { + example := sentences.([]interface{})[2] + definition += fmt.Sprintf("\t\t\"%v\"\n", example) + } + } + } + } + return translation, definition, partOfSpeech, nil + } + + return "", "", "", errors.New("Translation not found") +} diff --git a/internal/translate/google/tts.go b/internal/translate/google/tts.go new file mode 100644 index 0000000..17d4523 --- /dev/null +++ b/internal/translate/google/tts.go @@ -0,0 +1,68 @@ +package google + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/hajimehoshi/go-mp3" + "github.com/hajimehoshi/oto/v2" +) + +const ( + ttsURL = "https://translate.google.com.vn/translate_tts?ie=UTF-8&q=%s&tl=%s&client=tw-ob" +) + +func (t *GoogleTranslate) LockAvailable() bool { + return t.SoundLock.Available() +} + +func (t *GoogleTranslate) LockAcquire() { + t.SoundLock.Acquire() +} + +func (t *GoogleTranslate) StopTTS() { + t.SoundLock.Stop = true +} + +func (t *GoogleTranslate) PlayTTS(lang string, message string) error { + urlStr := fmt.Sprintf( + ttsURL, + url.QueryEscape(message), + langCode[lang], + ) + res, err := http.Get(urlStr) + if err != nil { + t.SoundLock.Release() + return err + } + decoder, err := mp3.NewDecoder(res.Body) + if err != nil { + t.SoundLock.Release() + return err + } + otoCtx, readyChan, err := oto.NewContext(decoder.SampleRate(), 2, 2) + if err != nil { + t.SoundLock.Release() + return err + } + <-readyChan + player := otoCtx.NewPlayer(decoder) + player.Play() + for player.IsPlaying() { + if t.SoundLock.Stop { + t.SoundLock.Release() + return nil + } else { + time.Sleep(time.Millisecond) + } + } + if err = player.Close(); err != nil { + t.SoundLock.Release() + return err + } + + t.SoundLock.Release() + return nil +} diff --git a/internal/translate/translator.go b/internal/translate/translator.go index b13a6d3..d93c34f 100644 --- a/internal/translate/translator.go +++ b/internal/translate/translator.go @@ -1,154 +1,32 @@ package translate import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "time" - - "github.com/hajimehoshi/go-mp3" - "github.com/hajimehoshi/oto/v2" + "gtt/internal/lock" + "gtt/internal/translate/google" ) -const ( - textURL = "https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&dt=bd&dt=md&dt=ex&sl=%s&tl=%s&q=%s" - soundURL = "https://translate.google.com.vn/translate_tts?ie=UTF-8&q=%s&tl=%s&client=tw-ob" -) - -type Translator struct { - SrcLang string - DstLang string - SoundLock *Lock +type Translator interface { + // text + GetAllLang() []string + GetSrcLang() string + GetDstLang() string + SetSrcLang(srcLang string) + SetDstLang(dstLang string) + SwapLang() + Translate(message string) ( + translation string, + definition string, + partOfSpeech string, + err error) + // text to speech + LockAvailable() bool + LockAcquire() + StopTTS() + PlayTTS(lang string, message string) error } -func NewTranslator() *Translator { - return &Translator{ - SoundLock: NewLock(), +func NewGoogleTranslate() *google.GoogleTranslate { + return &google.GoogleTranslate{ + SoundLock: lock.NewLock(), } } - -func (t *Translator) Translate(message string) ( - translation string, - definition string, - partOfSpeech string, - err error) { - var data []interface{} - - urlStr := fmt.Sprintf( - textURL, - LangCode[t.SrcLang], - LangCode[t.DstLang], - url.QueryEscape(message), - ) - res, err := http.Get(urlStr) - if err != nil { - return "", "", "", err - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return "", "", "", err - } - - if err = json.Unmarshal(body, &data); err != nil { - return "", "", "", err - } - - if len(data) > 0 { - // translation = data[0] - for _, lines := range data[0].([]interface{}) { - translatedLine := lines.([]interface{})[0] - translation += fmt.Sprintf("%v", translatedLine) - } - - // part of speech = data[1] - if data[1] != nil { - for _, parts := range data[1].([]interface{}) { - // part of speech - part := parts.([]interface{})[0] - partOfSpeech += fmt.Sprintf("[%v]\n", part) - for _, words := range parts.([]interface{})[2].([]interface{}) { - // dst lang - dstWord := words.([]interface{})[0] - partOfSpeech += fmt.Sprintf("\t%v:", dstWord) - // src lang - firstWord := true - for _, word := range words.([]interface{})[1].([]interface{}) { - if firstWord { - partOfSpeech += fmt.Sprintf(" %v", word) - firstWord = false - } else { - partOfSpeech += fmt.Sprintf(", %v", word) - } - } - partOfSpeech += "\n" - } - } - } - - // definition = data[12] - if len(data) >= 13 && data[12] != nil { - for _, parts := range data[12].([]interface{}) { - // part of speech - part := parts.([]interface{})[0] - definition += fmt.Sprintf("[%v]\n", part) - for _, sentences := range parts.([]interface{})[1].([]interface{}) { - // definition - def := sentences.([]interface{})[0] - definition += fmt.Sprintf("\t- %v\n", def) - // example sentence - if len(sentences.([]interface{})) >= 3 && sentences.([]interface{})[2] != nil { - example := sentences.([]interface{})[2] - definition += fmt.Sprintf("\t\t\"%v\"\n", example) - } - } - } - } - return translation, definition, partOfSpeech, nil - } - - return "", "", "", errors.New("Translation not found") -} - -func (t *Translator) PlaySound(lang string, message string) error { - urlStr := fmt.Sprintf( - soundURL, - url.QueryEscape(message), - LangCode[lang], - ) - res, err := http.Get(urlStr) - if err != nil { - t.SoundLock.Release() - return err - } - decoder, err := mp3.NewDecoder(res.Body) - if err != nil { - t.SoundLock.Release() - return err - } - otoCtx, readyChan, err := oto.NewContext(decoder.SampleRate(), 2, 2) - if err != nil { - t.SoundLock.Release() - return err - } - <-readyChan - player := otoCtx.NewPlayer(decoder) - player.Play() - for player.IsPlaying() { - if t.SoundLock.Stop { - t.SoundLock.Release() - return nil - } else { - time.Sleep(time.Millisecond) - } - } - if err = player.Close(); err != nil { - t.SoundLock.Release() - return err - } - - t.SoundLock.Release() - return nil -} diff --git a/main.go b/main.go index a8f9466..45e7347 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,8 @@ var ( srcLangArg *string = flag.String("src", "", "Set source language") dstLangArg *string = flag.String("dst", "", "Set destination language") // Translate - translator = translate.NewTranslator() + translator translate.Translator + googleTranslate = translate.NewGoogleTranslate() // UI app = tview.NewApplication() srcInput = tview.NewTextArea() diff --git a/ui.go b/ui.go index a06ed6f..063d3fd 100644 --- a/ui.go +++ b/ui.go @@ -3,7 +3,6 @@ package main import ( "fmt" "gtt/internal/color" - "gtt/internal/translate" "strconv" "github.com/gdamore/tcell/v2" @@ -172,16 +171,14 @@ func updateAllColor() { // Update title and option func updateTitle() { - srcInput.SetTitle(translator.SrcLang) - dstOutput.SetTitle(translator.DstLang) + srcInput.SetTitle(translator.GetSrcLang()) + dstOutput.SetTitle(translator.GetDstLang()) srcLangDropDown.SetCurrentOption( - IndexOf(translator.SrcLang, - translate.Lang)). - SetTitle(translator.SrcLang) + IndexOf(translator.GetSrcLang(), translator.GetAllLang())). + SetTitle(translator.GetSrcLang()) dstLangDropDown.SetCurrentOption( - IndexOf(translator.DstLang, - translate.Lang)). - SetTitle(translator.DstLang) + IndexOf(translator.GetDstLang(), translator.GetAllLang())). + SetTitle(translator.GetDstLang()) } func attachButton() *tview.Flex { @@ -204,7 +201,7 @@ func uiInit() { // dropdown for _, langDropDown := range []*tview.DropDown{srcLangDropDown, dstLangDropDown} { - langDropDown.SetOptions(translate.Lang, nil). + langDropDown.SetOptions(translator.GetAllLang(), nil). SetBorder(true) } themeDropDown.SetLabel("Theme: "). @@ -323,13 +320,13 @@ func uiInit() { keyMapWindow.SetInputCapture(popOutWindowHandler) srcLangDropDown.SetDoneFunc(langDropDownHandler). SetSelectedFunc(func(text string, index int) { - translator.SrcLang = text + translator.SetSrcLang(text) srcInput.SetTitle(text) srcLangDropDown.SetTitle(text) }) dstLangDropDown.SetDoneFunc(langDropDownHandler). SetSelectedFunc(func(text string, index int) { - translator.DstLang = text + translator.SetDstLang(text) dstOutput.SetTitle(text) dstLangDropDown.SetTitle(text) }) @@ -444,7 +441,7 @@ func translateWindowHandler(event *tcell.EventKey) *tcell.EventKey { CopyToClipboard(text[:len(text)-1]) } case tcell.KeyCtrlS: - translator.SrcLang, translator.DstLang = translator.DstLang, translator.SrcLang + translator.SwapLang() updateTitle() srcText := srcInput.GetText() dstText := dstOutput.GetText(false) @@ -457,13 +454,13 @@ func translateWindowHandler(event *tcell.EventKey) *tcell.EventKey { dstOutput.SetText(srcText) case tcell.KeyCtrlO: // Play source sound - if translator.SoundLock.Available() { + if translator.LockAvailable() { message := srcInput.GetText() // Only play when message exist if len(message) > 0 { - translator.SoundLock.Acquire() + translator.LockAcquire() go func() { - err := translator.PlaySound(translator.SrcLang, message) + err := translator.PlayTTS(translator.GetSrcLang(), message) if err != nil { srcInput.SetText(err.Error(), true) } @@ -473,13 +470,13 @@ func translateWindowHandler(event *tcell.EventKey) *tcell.EventKey { } case tcell.KeyCtrlP: // Play destination sound - if translator.SoundLock.Available() { + if translator.LockAvailable() { message := dstOutput.GetText(false) // Only play when message exist if len(message) > 0 { - translator.SoundLock.Acquire() + translator.LockAcquire() go func() { - err := translator.PlaySound(translator.DstLang, message) + err := translator.PlayTTS(translator.GetDstLang(), message) if err != nil { dstOutput.SetText(err.Error()) } @@ -488,7 +485,7 @@ func translateWindowHandler(event *tcell.EventKey) *tcell.EventKey { } case tcell.KeyCtrlX: // Stop play sound - translator.SoundLock.Stop = true + translator.StopTTS() } return event