diff --git a/README.md b/README.md index 752a754..ad4b250 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,22 @@ Google Translate TUI (Originally) Supported Translator: -[`ApertiumTranslate`](https://www.apertium.org/), -[`ArgosTranslate`](https://translate.argosopentech.com/)(default), -[`BingTranslate`](https://www.bing.com/translator), -[`GoogleTranslate`](https://translate.google.com/), -[`ReversoTranslate`](https://www.reverso.net/text-translation) +[`Apertium`](https://www.apertium.org/), +[`Argos`](https://translate.argosopentech.com/), +[`Bing`](https://www.bing.com/translator), +[`ChatGPT`](https://chat.openai.com/), +[`Google`](https://translate.google.com/)(default), +[`Reverso`](https://www.reverso.net/text-translation) + +## ⚠️ Note for ChatGPT + +You need to apply an API key on [OpenAI API keys](https://platform.openai.com/account/api-keys). +And write it to `$XDG_CONFIG_HOME/gtt/gtt.yaml` or `$HOME/.config/gtt/gtt.yaml`. + +```yaml +api_key: + chatgpt: YOUR_API_KEY # <- Replace with your API Key +``` ## ScreenShot @@ -47,11 +58,12 @@ gtt -src "English" -dst "Chinese (Traditional)" See available languages on: -- [Apertium Translate](https://www.apertium.org/) for `ApertiumTranslate` -- [argosopentech/argos-translate](https://github.com/argosopentech/argos-translate#supported-languages) for `ArgosTranslate` -- [Bing language-support](https://learn.microsoft.com/en-us/azure/cognitive-services/translator/language-support#translation) for `BingTranslate` -- [Google Language support](https://cloud.google.com/translate/docs/languages) for `GoogleTranslate` -- [Reverso Translation](https://www.reverso.net/text-translation) for `ReversoTranslate` +- [Apertium Translate](https://www.apertium.org/) for `Apertium` +- [argosopentech/argos-translate](https://github.com/argosopentech/argos-translate#supported-languages) for `Argos` +- [Bing language-support](https://learn.microsoft.com/en-us/azure/cognitive-services/translator/language-support#translation) for `Bing` +- `ChatGPT` is same as `Google`. See [Google Language support](https://cloud.google.com/translate/docs/languages) +- [Google Language support](https://cloud.google.com/translate/docs/languages) for `Google` +- [Reverso Translation](https://www.reverso.net/text-translation) for `Reverso` ## Key Map @@ -80,10 +92,10 @@ Copy all text in source of translation window. Copy all text in destination of translation window. `` -Play sound on source of translation window. +Play text to speech on source of translation window. `` -Play sound on destination of translation window. +Play text to speech on destination of translation window. `` Stop play sound. @@ -106,8 +118,6 @@ Switch pop out window. [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) for Linux/Wayland to copy text. -`pbcopy` For macOS to copy text. - ## Credit [soimort/translate-shell](https://github.com/soimort/translate-shell), diff --git a/config.go b/config.go index fc9349c..443581e 100644 --- a/config.go +++ b/config.go @@ -15,22 +15,24 @@ func configInit() { defaultConfigPath string themeConfig = config.New() defaultConfig = map[string]interface{}{ - "hide_below": false, - "transparent": false, - "theme": "Gruvbox", - "source.border_color": "red", - "destination.border_color": "blue", - "source.language.apertiumtranslate": "English", - "destination.language.apertiumtranslate": "English", - "source.language.argostranslate": "English", - "destination.language.argostranslate": "English", - "source.language.bingtranslate": "English", - "destination.language.bingtranslate": "English", - "source.language.googletranslate": "English", - "destination.language.googletranslate": "English", - "source.language.reversotranslate": "English", - "destination.language.reversotranslate": "English", - "translator": "ArgosTranslate", + "hide_below": false, + "transparent": false, + "theme": "gruvbox", + "source.border_color": "red", + "destination.border_color": "blue", + "source.language.apertium": "English", + "destination.language.apertium": "English", + "source.language.argos": "English", + "destination.language.argos": "English", + "source.language.bing": "English", + "destination.language.bing": "English", + "source.language.chatgpt": "English", + "destination.language.chatgpt": "English", + "source.language.google": "English", + "destination.language.google": "English", + "source.language.reverso": "English", + "destination.language.reverso": "English", + "translator": "Google", } ) @@ -48,7 +50,7 @@ func configInit() { config.AddConfigPath("$HOME/.config/gtt") themeConfig.AddConfigPath("$HOME/.config/gtt") - // Create config file if not exists + // Create config file if it does not exist // Otherwise check if config value is missing if err := config.ReadInConfig(); err != nil { for key, value := range defaultConfig { @@ -66,6 +68,16 @@ func configInit() { missing = true } } + // Set to default theme if theme in config does not exist + if IndexOf(config.GetString("theme"), style.AllTheme) < 0 { + config.Set("theme", defaultConfig["theme"]) + missing = true + } + // Set to default translator if translator in config does not exist + if IndexOf(config.GetString("translator"), translate.AllTranslator) < 0 { + config.Set("translator", defaultConfig["translator"]) + missing = true + } if missing { config.WriteConfig() } @@ -84,7 +96,7 @@ func configInit() { } } - // setup + // Setup for _, name := range translate.AllTranslator { translators[name] = translate.NewTranslator(name) translators[name].SetSrcLang( @@ -98,7 +110,11 @@ func configInit() { uiStyle.Transparent = config.GetBool("transparent") uiStyle.SetSrcBorderColor(config.GetString("source.border_color")). SetDstBorderColor(config.GetString("destination.border_color")) - // set argument language + // Set API Key + if config.Get("api_key.chatgpt") != nil { + translators["ChatGPT"].SetAPIKey(config.GetString("api_key.chatgpt")) + } + // Set argument language if len(*srcLangArg) > 0 { translator.SetSrcLang(*srcLangArg) } diff --git a/internal/style/color.go b/internal/style/color.go index 5e136f3..2ed2c60 100644 --- a/internal/style/color.go +++ b/internal/style/color.go @@ -5,10 +5,10 @@ import ( ) var ( - AllTheme = []string{"Gruvbox", "Nord"} + AllTheme = []string{"gruvbox", "nord"} Palette = []string{"red", "green", "yellow", "blue", "purple", "cyan", "orange"} themes = map[string]map[string]tcell.Color{ - "Gruvbox": { + "gruvbox": { "bg": tcell.NewHexColor(0x282828), "fg": tcell.NewHexColor(0xebdbb2), "gray": tcell.NewHexColor(0x665c54), @@ -20,7 +20,7 @@ var ( "cyan": tcell.NewHexColor(0x8ec07c), "orange": tcell.NewHexColor(0xfe8019), }, - "Nord": { + "nord": { "bg": tcell.NewHexColor(0x3b4252), "fg": tcell.NewHexColor(0xeceff4), "gray": tcell.NewHexColor(0x4c566a), diff --git a/internal/translate/apertiumtranslate/language.go b/internal/translate/apertium/language.go similarity index 98% rename from internal/translate/apertiumtranslate/language.go rename to internal/translate/apertium/language.go index 09c611f..b45b7fd 100644 --- a/internal/translate/apertiumtranslate/language.go +++ b/internal/translate/apertium/language.go @@ -1,4 +1,4 @@ -package apertiumtranslate +package apertium var ( lang = []string{ diff --git a/internal/translate/apertium/translator.go b/internal/translate/apertium/translator.go new file mode 100644 index 0000000..cf420f9 --- /dev/null +++ b/internal/translate/apertium/translator.go @@ -0,0 +1,81 @@ +package apertium + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/eeeXun/gtt/internal/translate/core" +) + +const ( + textURL = "https://www.apertium.org/apy/translate?langpair=%s|%s&q=%s" +) + +type Translator struct { + *core.APIKey + *core.Language + *core.TTSLock + core.EngineName +} + +func NewTranslator() *Translator { + return &Translator{ + APIKey: new(core.APIKey), + Language: new(core.Language), + TTSLock: core.NewTTSLock(), + EngineName: core.NewEngineName("Apertium"), + } +} + +func (t *Translator) GetAllLang() []string { + return lang +} + +func (t *Translator) Translate(message string) (translation *core.Translation, err error) { + translation = new(core.Translation) + var data map[string]interface{} + + urlStr := fmt.Sprintf( + textURL, + langCode[t.GetSrcLang()], + langCode[t.GetDstLang()], + url.QueryEscape(message), + ) + res, err := http.Get(urlStr) + if err != nil { + return nil, err + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + if err = json.Unmarshal(body, &data); err != nil { + return nil, err + } + + if len(data) <= 0 { + return nil, errors.New("Translation not found") + } + // If responseData is nil, then suppose the translation pair is not available + if data["responseData"] == nil { + return nil, errors.New(fmt.Sprintf("%s does not support translate from %s to %s.", + t.GetEngineName(), + t.GetSrcLang(), + t.GetDstLang(), + )) + } + + translation.TEXT = data["responseData"].(map[string]interface{})["translatedText"].(string) + + return translation, nil +} + +func (t *Translator) PlayTTS(lang, message string) error { + defer t.ReleaseLock() + + return errors.New(t.GetEngineName() + " does not support text to speech") +} diff --git a/internal/translate/apertiumtranslate/translator.go b/internal/translate/apertiumtranslate/translator.go deleted file mode 100644 index 4046fb3..0000000 --- a/internal/translate/apertiumtranslate/translator.go +++ /dev/null @@ -1,82 +0,0 @@ -package apertiumtranslate - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/url" - - "github.com/eeeXun/gtt/internal/translate/core" -) - -const ( - textURL = "https://www.apertium.org/apy/translate?langpair=%s|%s&q=%s" -) - -type ApertiumTranslate struct { - *core.Language - *core.TTSLock - core.EngineName -} - -func NewApertiumTranslate() *ApertiumTranslate { - return &ApertiumTranslate{ - Language: core.NewLanguage(), - TTSLock: core.NewTTSLock(), - EngineName: core.NewEngineName("ApertiumTranslate"), - } -} - -func (t *ApertiumTranslate) GetAllLang() []string { - return lang -} - -func (t *ApertiumTranslate) Translate(message string) (translation, definition, partOfSpeech string, err error) { - var data map[string]interface{} - - urlStr := fmt.Sprintf( - textURL, - langCode[t.GetSrcLang()], - langCode[t.GetDstLang()], - 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 { - return "", "", "", errors.New("Translation not found") - } - - switch res.StatusCode { - case 200: - translation = fmt.Sprintf("%v", - data["responseData"].(map[string]interface{})["translatedText"]) - default: - return "", "", "", errors.New( - fmt.Sprintf("%s does not support translate from %s to %s.\nSee available pair on %s", - t.GetEngineName(), - t.GetSrcLang(), - t.GetDstLang(), - "https://www.apertium.org/", - )) - } - - return translation, definition, partOfSpeech, nil -} - -func (t *ApertiumTranslate) PlayTTS(lang, message string) error { - defer t.ReleaseLock() - - return errors.New(t.GetEngineName() + " does not support text to speech") -} diff --git a/internal/translate/argostranslate/language.go b/internal/translate/argos/language.go similarity index 96% rename from internal/translate/argostranslate/language.go rename to internal/translate/argos/language.go index 0e5873f..8cfdd5c 100644 --- a/internal/translate/argostranslate/language.go +++ b/internal/translate/argos/language.go @@ -1,4 +1,4 @@ -package argostranslate +package argos var ( lang = []string{ diff --git a/internal/translate/argostranslate/translator.go b/internal/translate/argos/translator.go similarity index 52% rename from internal/translate/argostranslate/translator.go rename to internal/translate/argos/translator.go index 3a00fc1..93e0ea1 100644 --- a/internal/translate/argostranslate/translator.go +++ b/internal/translate/argos/translator.go @@ -1,9 +1,8 @@ -package argostranslate +package argos import ( "encoding/json" "errors" - "fmt" "io/ioutil" "net/http" "net/url" @@ -15,25 +14,28 @@ const ( textURL = "https://translate.argosopentech.com/translate" ) -type ArgosTranslate struct { +type Translator struct { + *core.APIKey *core.Language *core.TTSLock core.EngineName } -func NewArgosTranslate() *ArgosTranslate { - return &ArgosTranslate{ - Language: core.NewLanguage(), +func NewTranslator() *Translator { + return &Translator{ + APIKey: new(core.APIKey), + Language: new(core.Language), TTSLock: core.NewTTSLock(), - EngineName: core.NewEngineName("ArgosTranslate"), + EngineName: core.NewEngineName("Argos"), } } -func (t *ArgosTranslate) GetAllLang() []string { +func (t *Translator) GetAllLang() []string { return lang } -func (t *ArgosTranslate) Translate(message string) (translation, definition, partOfSpeech string, err error) { +func (t *Translator) Translate(message string) (translation *core.Translation, err error) { + translation = new(core.Translation) var data map[string]interface{} res, err := http.PostForm(textURL, @@ -43,26 +45,26 @@ func (t *ArgosTranslate) Translate(message string) (translation, definition, par "target": {langCode[t.GetDstLang()]}, }) if err != nil { - return "", "", "", err + return nil, err } body, err := ioutil.ReadAll(res.Body) if err != nil { - return "", "", "", err + return nil, err } if err = json.Unmarshal(body, &data); err != nil { - return "", "", "", err + return nil, err } if len(data) <= 0 { - return "", "", "", errors.New("Translation not found") + return nil, errors.New("Translation not found") } - translation = fmt.Sprintf("%v", data["translatedText"]) + translation.TEXT = data["translatedText"].(string) - return translation, definition, partOfSpeech, nil + return translation, nil } -func (t *ArgosTranslate) PlayTTS(lang, message string) error { +func (t *Translator) PlayTTS(lang, message string) error { defer t.ReleaseLock() return errors.New(t.GetEngineName() + " does not support text to speech") diff --git a/internal/translate/bingtranslate/lang.go b/internal/translate/bing/language.go similarity index 99% rename from internal/translate/bingtranslate/lang.go rename to internal/translate/bing/language.go index 9ca806d..f083553 100644 --- a/internal/translate/bingtranslate/lang.go +++ b/internal/translate/bing/language.go @@ -1,4 +1,4 @@ -package bingtranslate +package bing var ( lang = []string{ diff --git a/internal/translate/bingtranslate/translator.go b/internal/translate/bing/translator.go similarity index 84% rename from internal/translate/bingtranslate/translator.go rename to internal/translate/bing/translator.go index 82d35bd..995be6e 100644 --- a/internal/translate/bingtranslate/translator.go +++ b/internal/translate/bing/translator.go @@ -1,4 +1,4 @@ -package bingtranslate +package bing import ( "encoding/json" @@ -24,7 +24,8 @@ const ( ttsSSML = "%s" ) -type BingTranslate struct { +type Translator struct { + *core.APIKey *core.Language *core.TTSLock core.EngineName @@ -37,20 +38,21 @@ type setUpData struct { token string } -func NewBingTranslate() *BingTranslate { - return &BingTranslate{ - Language: core.NewLanguage(), +func NewTranslator() *Translator { + return &Translator{ + APIKey: new(core.APIKey), + Language: new(core.Language), TTSLock: core.NewTTSLock(), - EngineName: core.NewEngineName("BingTranslate"), + EngineName: core.NewEngineName("Bing"), } } -func (t *BingTranslate) GetAllLang() []string { +func (t *Translator) GetAllLang() []string { return lang } -func (t *BingTranslate) setUp() (*setUpData, error) { - var data setUpData +func (t *Translator) setUp() (*setUpData, error) { + data := new(setUpData) res, err := http.Get(setUpURL) if err != nil { @@ -79,15 +81,16 @@ func (t *BingTranslate) setUp() (*setUpData, error) { data.key = paramsStr[0] data.token = paramsStr[1][1 : len(paramsStr[1])-1] - return &data, nil + return data, nil } -func (t *BingTranslate) Translate(message string) (translation, definition, partOfSpeech string, err error) { +func (t *Translator) Translate(message string) (translation *core.Translation, err error) { + translation = new(core.Translation) var data []interface{} initData, err := t.setUp() if err != nil { - return "", "", "", err + return nil, err } userData := url.Values{ "fromLang": {langCode[t.GetSrcLang()]}, @@ -104,23 +107,23 @@ func (t *BingTranslate) Translate(message string) (translation, definition, part req.Header.Add("User-Agent", core.UserAgent) res, err := http.DefaultClient.Do(req) if err != nil { - return "", "", "", err + return nil, err } body, err := ioutil.ReadAll(res.Body) if err != nil { - return "", "", "", err + return nil, err } if err = json.Unmarshal(body, &data); err != nil { - return "", "", "", err + return nil, err } if len(data) <= 0 { - return "", "", "", errors.New("Translation not found") + return nil, errors.New("Translation not found") } // translation - translation = fmt.Sprintf("%v", - data[0].(map[string]interface{})["translations"].([]interface{})[0].(map[string]interface{})["text"]) + translation.TEXT = + data[0].(map[string]interface{})["translations"].([]interface{})[0].(map[string]interface{})["text"].(string) // request part of speech userData.Del("fromLang") @@ -133,11 +136,11 @@ func (t *BingTranslate) Translate(message string) (translation, definition, part req.Header.Add("User-Agent", core.UserAgent) res, err = http.DefaultClient.Do(req) if err != nil { - return "", "", "", err + return nil, err } body, err = ioutil.ReadAll(res.Body) if err != nil { - return "", "", "", err + return nil, err } // Bing will return the request with list when success. // Otherwises, it would return map. Then the following err would not be nil. @@ -154,13 +157,13 @@ func (t *BingTranslate) Translate(message string) (translation, definition, part } poses.add(pos["posTag"].(string), words) } - partOfSpeech = poses.format() + translation.POS = poses.format() } - return translation, definition, partOfSpeech, nil + return translation, nil } -func (t *BingTranslate) PlayTTS(lang, message string) error { +func (t *Translator) PlayTTS(lang, message string) error { defer t.ReleaseLock() name, ok := voiceName[lang] diff --git a/internal/translate/bingtranslate/utils.go b/internal/translate/bing/utils.go similarity index 97% rename from internal/translate/bingtranslate/utils.go rename to internal/translate/bing/utils.go index 44c4521..359ccd7 100644 --- a/internal/translate/bingtranslate/utils.go +++ b/internal/translate/bing/utils.go @@ -1,4 +1,4 @@ -package bingtranslate +package bing import ( "fmt" diff --git a/internal/translate/chatgpt/language.go b/internal/translate/chatgpt/language.go new file mode 100644 index 0000000..2b607a3 --- /dev/null +++ b/internal/translate/chatgpt/language.go @@ -0,0 +1,142 @@ +package chatgpt + +var ( + // Generated from Google + lang = []string{ + "Afrikaans", + "Albanian", + "Amharic", + "Arabic", + "Armenian", + "Auto", + "Assamese", + "Aymara", + "Azerbaijani", + "Bambara", + "Basque", + "Belarusian", + "Bengali", + "Bhojpuri", + "Bosnian", + "Bulgarian", + "Catalan", + "Cebuano", + "Chinese (Simplified)", + "Chinese (Traditional)", + "Corsican", + "Croatian", + "Czech", + "Danish", + "Dhivehi", + "Dogri", + "Dutch", + "English", + "Esperanto", + "Estonian", + "Ewe", + "Filipino (Tagalog)", + "Finnish", + "French", + "Frisian", + "Galician", + "Georgian", + "German", + "Greek", + "Guarani", + "Gujarati", + "Haitian Creole", + "Hausa", + "Hawaiian", + "Hebrew", + "Hindi", + "Hmong", + "Hungarian", + "Icelandic", + "Igbo", + "Ilocano", + "Indonesian", + "Irish", + "Italian", + "Japanese", + "Javanese", + "Kannada", + "Kazakh", + "Khmer", + "Kinyarwanda", + "Konkani", + "Korean", + "Krio", + "Kurdish", + "Kurdish (Sorani)", + "Kyrgyz", + "Lao", + "Latin", + "Latvian", + "Lingala", + "Lithuanian", + "Luganda", + "Luxembourgish", + "Macedonian", + "Maithili", + "Malagasy", + "Malay", + "Malayalam", + "Maltese", + "Maori", + "Marathi", + "Meiteilon (Manipuri)", + "Mizo", + "Mongolian", + "Myanmar (Burmese)", + "Nepali", + "Norwegian", + "Nyanja (Chichewa)", + "Odia (Oriya)", + "Oromo", + "Pashto", + "Persian", + "Polish", + "Portuguese (Portugal, Brazil)", + "Punjabi", + "Quechua", + "Romanian", + "Russian", + "Samoan", + "Sanskrit", + "Scots Gaelic", + "Sepedi", + "Serbian", + "Sesotho", + "Shona", + "Sindhi", + "Sinhala (Sinhalese)", + "Slovak", + "Slovenian", + "Somali", + "Spanish", + "Sundanese", + "Swahili", + "Swedish", + "Tagalog (Filipino)", + "Tajik", + "Tamil", + "Tatar", + "Telugu", + "Thai", + "Tigrinya", + "Tsonga", + "Turkish", + "Turkmen", + "Twi (Akan)", + "Ukrainian", + "Urdu", + "Uyghur", + "Uzbek", + "Vietnamese", + "Welsh", + "Xhosa", + "Yiddish", + "Yoruba", + "Zulu", + } +) diff --git a/internal/translate/chatgpt/translator.go b/internal/translate/chatgpt/translator.go new file mode 100644 index 0000000..dee3c30 --- /dev/null +++ b/internal/translate/chatgpt/translator.go @@ -0,0 +1,94 @@ +package chatgpt + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + + "github.com/eeeXun/gtt/internal/translate/core" +) + +const ( + textURL = "https://api.openai.com/v1/chat/completions" +) + +type Translator struct { + *core.APIKey + *core.Language + *core.TTSLock + core.EngineName +} + +func NewTranslator() *Translator { + return &Translator{ + APIKey: new(core.APIKey), + Language: new(core.Language), + TTSLock: core.NewTTSLock(), + EngineName: core.NewEngineName("ChatGPT"), + } +} + +func (t *Translator) GetAllLang() []string { + return lang +} + +func (t *Translator) Translate(message string) (translation *core.Translation, err error) { + translation = new(core.Translation) + var data map[string]interface{} + + if len(t.GetAPIKey()) <= 0 { + return nil, errors.New("Please write your API Key in config file for " + t.GetEngineName()) + } + + userData, _ := json.Marshal(map[string]interface{}{ + "model": "gpt-3.5-turbo", + "messages": []map[string]string{{ + "role": "user", + "content": fmt.Sprintf( + "Translate following text from %s to %s\n%s", + t.GetSrcLang(), + t.GetDstLang(), + message, + ), + }}, + "temperature": 0.7, + }) + req, _ := http.NewRequest("POST", + textURL, + bytes.NewBuffer(userData), + ) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+t.GetAPIKey()) + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + if err = json.Unmarshal(body, &data); err != nil { + return nil, err + } + + if len(data) <= 0 { + return nil, errors.New("Translation not found") + } + if data["error"] != nil { + return nil, errors.New(data["error"].(map[string]interface{})["message"].(string)) + } + + translation.TEXT = + data["choices"].([]interface{})[0].(map[string]interface{})["message"].(map[string]interface{})["content"].(string) + + return translation, nil +} + +func (t *Translator) PlayTTS(lang, message string) error { + defer t.ReleaseLock() + + return errors.New(t.GetEngineName() + " does not support text to speech") +} diff --git a/internal/translate/core/apikey.go b/internal/translate/core/apikey.go new file mode 100644 index 0000000..9208a34 --- /dev/null +++ b/internal/translate/core/apikey.go @@ -0,0 +1,13 @@ +package core + +type APIKey struct { + key string +} + +func (k *APIKey) SetAPIKey(key string) { + k.key = key +} + +func (k *APIKey) GetAPIKey() string { + return k.key +} diff --git a/internal/translate/core/language.go b/internal/translate/core/language.go index a5fbb22..c778bff 100644 --- a/internal/translate/core/language.go +++ b/internal/translate/core/language.go @@ -5,10 +5,6 @@ type Language struct { dstLang string } -func NewLanguage() *Language { - return &Language{} -} - func (l *Language) GetSrcLang() string { return l.srcLang } diff --git a/internal/translate/core/utils.go b/internal/translate/core/utils.go index 28dfdac..0534a41 100644 --- a/internal/translate/core/utils.go +++ b/internal/translate/core/utils.go @@ -3,3 +3,14 @@ package core const ( UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" ) + +type Translation struct { + // translation text + TEXT string + + // translation definition or example + DEF string + + // translation part of speech + POS string +} diff --git a/internal/translate/googletranslate/language.go b/internal/translate/google/language.go similarity index 81% rename from internal/translate/googletranslate/language.go rename to internal/translate/google/language.go index d119988..2be84fb 100644 --- a/internal/translate/googletranslate/language.go +++ b/internal/translate/google/language.go @@ -1,4 +1,4 @@ -package googletranslate +package google // https://cloud.google.com/translate/docs/languages var ( @@ -9,10 +9,14 @@ var ( "Arabic", "Armenian", "Auto", + "Assamese", + "Aymara", "Azerbaijani", + "Bambara", "Basque", "Belarusian", "Bengali", + "Bhojpuri", "Bosnian", "Bulgarian", "Catalan", @@ -23,10 +27,14 @@ var ( "Croatian", "Czech", "Danish", + "Dhivehi", + "Dogri", "Dutch", "English", "Esperanto", "Estonian", + "Ewe", + "Filipino (Tagalog)", "Finnish", "French", "Frisian", @@ -34,6 +42,7 @@ var ( "Georgian", "German", "Greek", + "Guarani", "Gujarati", "Haitian Creole", "Hausa", @@ -44,6 +53,7 @@ var ( "Hungarian", "Icelandic", "Igbo", + "Ilocano", "Indonesian", "Irish", "Italian", @@ -53,36 +63,48 @@ var ( "Kazakh", "Khmer", "Kinyarwanda", + "Konkani", "Korean", + "Krio", "Kurdish", + "Kurdish (Sorani)", "Kyrgyz", "Lao", "Latin", "Latvian", + "Lingala", "Lithuanian", + "Luganda", "Luxembourgish", "Macedonian", + "Maithili", "Malagasy", "Malay", "Malayalam", "Maltese", "Maori", "Marathi", + "Meiteilon (Manipuri)", + "Mizo", "Mongolian", "Myanmar (Burmese)", "Nepali", "Norwegian", "Nyanja (Chichewa)", "Odia (Oriya)", + "Oromo", "Pashto", "Persian", "Polish", "Portuguese (Portugal, Brazil)", "Punjabi", + "Quechua", "Romanian", "Russian", "Samoan", + "Sanskrit", "Scots Gaelic", + "Sepedi", "Serbian", "Sesotho", "Shona", @@ -101,8 +123,11 @@ var ( "Tatar", "Telugu", "Thai", + "Tigrinya", + "Tsonga", "Turkish", "Turkmen", + "Twi (Akan)", "Ukrainian", "Urdu", "Uyghur", @@ -121,10 +146,14 @@ var ( "Arabic": "ar", "Armenian": "hy", "Auto": "auto", + "Assamese": "as", + "Aymara": "ay", "Azerbaijani": "az", + "Bambara": "bm", "Basque": "eu", "Belarusian": "be", "Bengali": "bn", + "Bhojpuri": "bho", "Bosnian": "bs", "Bulgarian": "bg", "Catalan": "ca", @@ -135,10 +164,14 @@ var ( "Croatian": "hr", "Czech": "cs", "Danish": "da", + "Dhivehi": "dv", + "Dogri": "doi", "Dutch": "nl", "English": "en", "Esperanto": "eo", "Estonian": "et", + "Ewe": "ee", + "Filipino (Tagalog)": "fil", "Finnish": "fi", "French": "fr", "Frisian": "fy", @@ -146,6 +179,7 @@ var ( "Georgian": "ka", "German": "de", "Greek": "el", + "Guarani": "gn", "Gujarati": "gu", "Haitian Creole": "ht", "Hausa": "ha", @@ -156,6 +190,7 @@ var ( "Hungarian": "hu", "Icelandic": "is", "Igbo": "ig", + "Ilocano": "ilo", "Indonesian": "id", "Irish": "ga", "Italian": "it", @@ -165,36 +200,48 @@ var ( "Kazakh": "kk", "Khmer": "km", "Kinyarwanda": "rw", + "Konkani": "gom", "Korean": "ko", + "Krio": "kri", "Kurdish": "ku", + "Kurdish (Sorani)": "ckb", "Kyrgyz": "ky", "Lao": "lo", "Latin": "la", "Latvian": "lv", + "Lingala": "ln", "Lithuanian": "lt", + "Luganda": "lg", "Luxembourgish": "lb", "Macedonian": "mk", + "Maithili": "mai", "Malagasy": "mg", "Malay": "ms", "Malayalam": "ml", "Maltese": "mt", "Maori": "mi", "Marathi": "mr", + "Meiteilon (Manipuri)": "mni-Mtei", + "Mizo": "lus", "Mongolian": "mn", "Myanmar (Burmese)": "my", "Nepali": "ne", "Norwegian": "no", "Nyanja (Chichewa)": "ny", "Odia (Oriya)": "or", + "Oromo": "om", "Pashto": "ps", "Persian": "fa", "Polish": "pl", "Portuguese (Portugal, Brazil)": "pt", "Punjabi": "pa", + "Quechua": "qu", "Romanian": "ro", "Russian": "ru", "Samoan": "sm", + "Sanskrit": "sa", "Scots Gaelic": "gd", + "Sepedi": "nso", "Serbian": "sr", "Sesotho": "st", "Shona": "sn", @@ -213,8 +260,11 @@ var ( "Tatar": "tt", "Telugu": "te", "Thai": "th", + "Tigrinya": "ti", + "Tsonga": "ts", "Turkish": "tr", "Turkmen": "tk", + "Twi (Akan)": "ak", "Ukrainian": "uk", "Urdu": "ur", "Uyghur": "ug", diff --git a/internal/translate/googletranslate/translator.go b/internal/translate/google/translator.go similarity index 69% rename from internal/translate/googletranslate/translator.go rename to internal/translate/google/translator.go index 2e41c3d..4024e7c 100644 --- a/internal/translate/googletranslate/translator.go +++ b/internal/translate/google/translator.go @@ -1,4 +1,4 @@ -package googletranslate +package google import ( "encoding/json" @@ -19,25 +19,28 @@ const ( ttsURL = "https://translate.google.com.vn/translate_tts?ie=UTF-8&q=%s&tl=%s&client=tw-ob" ) -type GoogleTranslate struct { +type Translator struct { + *core.APIKey *core.Language *core.TTSLock core.EngineName } -func NewGoogleTranslate() *GoogleTranslate { - return &GoogleTranslate{ - Language: core.NewLanguage(), +func NewTranslator() *Translator { + return &Translator{ + APIKey: new(core.APIKey), + Language: new(core.Language), TTSLock: core.NewTTSLock(), - EngineName: core.NewEngineName("GoogleTranslate"), + EngineName: core.NewEngineName("Google"), } } -func (t *GoogleTranslate) GetAllLang() []string { +func (t *Translator) GetAllLang() []string { return lang } -func (t *GoogleTranslate) Translate(message string) (translation, definition, partOfSpeech string, err error) { +func (t *Translator) Translate(message string) (translation *core.Translation, err error) { + translation = new(core.Translation) var data []interface{} urlStr := fmt.Sprintf( @@ -48,24 +51,24 @@ func (t *GoogleTranslate) Translate(message string) (translation, definition, pa ) res, err := http.Get(urlStr) if err != nil { - return "", "", "", err + return nil, err } body, err := ioutil.ReadAll(res.Body) if err != nil { - return "", "", "", err + return nil, err } if err = json.Unmarshal(body, &data); err != nil { - return "", "", "", err + return nil, err } if len(data) <= 0 { - return "", "", "", errors.New("Translation not found") + return nil, errors.New("Translation not found") } // translation = data[0] for _, line := range data[0].([]interface{}) { translatedLine := line.([]interface{})[0] - translation += fmt.Sprintf("%v", translatedLine) + translation.TEXT += translatedLine.(string) } // part of speech = data[1] if data[1] != nil { @@ -73,23 +76,23 @@ func (t *GoogleTranslate) Translate(message string) (translation, definition, pa partOfSpeeches := partOfSpeeches.([]interface{}) // part of speech pos := partOfSpeeches[0] - partOfSpeech += fmt.Sprintf("[%v]\n", pos) + translation.POS += fmt.Sprintf("[%v]\n", pos) for _, words := range partOfSpeeches[2].([]interface{}) { words := words.([]interface{}) // dst lang dstWord := words[0] - partOfSpeech += fmt.Sprintf("\t%v:", dstWord) + translation.POS += fmt.Sprintf("\t%v:", dstWord) // src lang firstWord := true for _, word := range words[1].([]interface{}) { if firstWord { - partOfSpeech += fmt.Sprintf(" %v", word) + translation.POS += fmt.Sprintf(" %v", word) firstWord = false } else { - partOfSpeech += fmt.Sprintf(", %v", word) + translation.POS += fmt.Sprintf(", %v", word) } } - partOfSpeech += "\n" + translation.POS += "\n" } } } @@ -99,25 +102,25 @@ func (t *GoogleTranslate) Translate(message string) (translation, definition, pa definitions := definitions.([]interface{}) // part of speech pos := definitions[0] - definition += fmt.Sprintf("[%v]\n", pos) + translation.DEF += fmt.Sprintf("[%v]\n", pos) for _, sentences := range definitions[1].([]interface{}) { sentences := sentences.([]interface{}) // definition def := sentences[0] - definition += fmt.Sprintf("\t- %v\n", def) + translation.DEF += fmt.Sprintf("\t- %v\n", def) // example sentence if len(sentences) >= 3 && sentences[2] != nil { example := sentences[2] - definition += fmt.Sprintf("\t\t\"%v\"\n", example) + translation.DEF += fmt.Sprintf("\t\t\"%v\"\n", example) } } } } - return translation, definition, partOfSpeech, nil + return translation, nil } -func (t *GoogleTranslate) PlayTTS(lang, message string) error { +func (t *Translator) PlayTTS(lang, message string) error { defer t.ReleaseLock() urlStr := fmt.Sprintf( @@ -129,6 +132,9 @@ func (t *GoogleTranslate) PlayTTS(lang, message string) error { if err != nil { return err } + if res.StatusCode == 400 { + return errors.New(t.GetEngineName() + " does not support text to speech of " + lang) + } decoder, err := mp3.NewDecoder(res.Body) if err != nil { return err diff --git a/internal/translate/reversotranslate/language.go b/internal/translate/reverso/language.go similarity index 98% rename from internal/translate/reversotranslate/language.go rename to internal/translate/reverso/language.go index 750f255..8264900 100644 --- a/internal/translate/reversotranslate/language.go +++ b/internal/translate/reverso/language.go @@ -1,4 +1,4 @@ -package reversotranslate +package reverso var ( lang = []string{ diff --git a/internal/translate/reversotranslate/translator.go b/internal/translate/reverso/translator.go similarity index 73% rename from internal/translate/reversotranslate/translator.go rename to internal/translate/reverso/translator.go index 1251c57..88ed339 100644 --- a/internal/translate/reversotranslate/translator.go +++ b/internal/translate/reverso/translator.go @@ -1,4 +1,4 @@ -package reversotranslate +package reverso import ( "bytes" @@ -21,29 +21,32 @@ const ( ttsURL = "https://voice.reverso.net/RestPronunciation.svc/v1/output=json/GetVoiceStream/voiceName=%s?voiceSpeed=80&inputText=%s" ) -type ReversoTranslate struct { +type Translator struct { + *core.APIKey *core.Language *core.TTSLock core.EngineName } -func NewReversoTranslate() *ReversoTranslate { - return &ReversoTranslate{ - Language: core.NewLanguage(), +func NewTranslator() *Translator { + return &Translator{ + APIKey: new(core.APIKey), + Language: new(core.Language), TTSLock: core.NewTTSLock(), - EngineName: core.NewEngineName("ReversoTranslate"), + EngineName: core.NewEngineName("Reverso"), } } -func (t *ReversoTranslate) GetAllLang() []string { +func (t *Translator) GetAllLang() []string { return lang } -func (t *ReversoTranslate) Translate(message string) (translation, definition, partOfSpeech string, err error) { +func (t *Translator) Translate(message string) (translation *core.Translation, err error) { + translation = new(core.Translation) var data map[string]interface{} if t.GetSrcLang() == t.GetDstLang() { - return "", "", "", errors.New( + return nil, errors.New( fmt.Sprintf("%s doesn't support translation of the same language.\ni.e. %s to %s", t.GetEngineName(), t.GetSrcLang(), t.GetDstLang())) } @@ -62,28 +65,28 @@ func (t *ReversoTranslate) Translate(message string) (translation, definition, p }) req, _ := http.NewRequest("POST", textURL, - bytes.NewBuffer([]byte(userData))) + bytes.NewBuffer(userData)) req.Header.Add("Content-Type", "application/json") req.Header.Add("User-Agent", core.UserAgent) res, err := http.DefaultClient.Do(req) if err != nil { - return "", "", "", err + return nil, err } body, err := ioutil.ReadAll(res.Body) if err != nil { - return "", "", "", err + return nil, err } if err = json.Unmarshal(body, &data); err != nil { - return "", "", "", err + return nil, err } if len(data) <= 0 { - return "", "", "", errors.New("Translation not found") + return nil, errors.New("Translation not found") } // translation for _, line := range data["translation"].([]interface{}) { - translation += fmt.Sprintf("%v", line) + translation.TEXT += line.(string) } // definition and part of speech if data["contextResults"] != nil { @@ -94,23 +97,23 @@ func (t *ReversoTranslate) Translate(message string) (translation, definition, p dstExample := results["targetExamples"].([]interface{}) if len(srcExample) > 0 && len(dstExample) > 0 { for i := 0; i < len(srcExample) && i < len(dstExample); i++ { - definition += fmt.Sprintf("- %v\n\t\"%v\"\n", srcExample[i], dstExample[i]) + translation.DEF += fmt.Sprintf("- %v\n\t\"%v\"\n", srcExample[i], dstExample[i]) } } // part of speech if results["partOfSpeech"] == nil { - partOfSpeech += fmt.Sprintf("%v\n", results["translation"]) + translation.POS += fmt.Sprintf("%v\n", results["translation"]) } else { - partOfSpeech += fmt.Sprintf("%v [%v]\n", results["translation"], results["partOfSpeech"]) + translation.POS += fmt.Sprintf("%v [%v]\n", results["translation"], results["partOfSpeech"]) } } - definition = regexp.MustCompile("<(|/)em>").ReplaceAllString(definition, "") + translation.DEF = regexp.MustCompile("<(|/)em>").ReplaceAllString(translation.DEF, "") } - return translation, definition, partOfSpeech, nil + return translation, nil } -func (t *ReversoTranslate) PlayTTS(lang, message string) error { +func (t *Translator) PlayTTS(lang, message string) error { defer t.ReleaseLock() name, ok := voiceName[lang] diff --git a/internal/translate/translator.go b/internal/translate/translator.go index 3a942dc..4fa6aa4 100644 --- a/internal/translate/translator.go +++ b/internal/translate/translator.go @@ -1,20 +1,23 @@ package translate import ( - "github.com/eeeXun/gtt/internal/translate/apertiumtranslate" - "github.com/eeeXun/gtt/internal/translate/argostranslate" - "github.com/eeeXun/gtt/internal/translate/bingtranslate" - "github.com/eeeXun/gtt/internal/translate/googletranslate" - "github.com/eeeXun/gtt/internal/translate/reversotranslate" + "github.com/eeeXun/gtt/internal/translate/apertium" + "github.com/eeeXun/gtt/internal/translate/argos" + "github.com/eeeXun/gtt/internal/translate/bing" + "github.com/eeeXun/gtt/internal/translate/chatgpt" + "github.com/eeeXun/gtt/internal/translate/core" + "github.com/eeeXun/gtt/internal/translate/google" + "github.com/eeeXun/gtt/internal/translate/reverso" ) var ( AllTranslator = []string{ - "ApertiumTranslate", - "BingTranslate", - "ArgosTranslate", - "GoogleTranslate", - "ReversoTranslate", + "Apertium", + "Argos", + "Bing", + "ChatGPT", + "Google", + "Reverso", } ) @@ -40,6 +43,9 @@ type Translator interface { // Swap source and destination language of the translator SwapLang() + // Set API Key + SetAPIKey(key string) + // Check if lock is available LockAvailable() bool @@ -50,7 +56,7 @@ type Translator interface { StopTTS() // Translate from source to destination language - Translate(message string) (translation, definition, partOfSpeech string, err error) + Translate(message string) (translation *core.Translation, err error) // Play text to speech PlayTTS(lang, message string) error @@ -60,16 +66,18 @@ func NewTranslator(name string) Translator { var translator Translator switch name { - case "ApertiumTranslate": - translator = apertiumtranslate.NewApertiumTranslate() - case "ArgosTranslate": - translator = argostranslate.NewArgosTranslate() - case "BingTranslate": - translator = bingtranslate.NewBingTranslate() - case "GoogleTranslate": - translator = googletranslate.NewGoogleTranslate() - case "ReversoTranslate": - translator = reversotranslate.NewReversoTranslate() + case "Apertium": + translator = apertium.NewTranslator() + case "Argos": + translator = argos.NewTranslator() + case "Bing": + translator = bing.NewTranslator() + case "ChatGPT": + translator = chatgpt.NewTranslator() + case "Google": + translator = google.NewTranslator() + case "Reverso": + translator = reverso.NewTranslator() } return translator diff --git a/ui.go b/ui.go index fd053b8..d2308ee 100644 --- a/ui.go +++ b/ui.go @@ -30,9 +30,9 @@ const ( [#%[1]s][-] Copy all text in destination of translation window. [#%[1]s][-] - Play sound on source of translation window. + Play text to speech on source of translation window. [#%[1]s][-] - Play sound on destination of translation window. + Play text to speech on destination of translation window. [#%[1]s][-] Stop play sound. [#%[1]s][-] @@ -233,6 +233,26 @@ func attachItems(center bool, direction int, items ...Item) *tview.Flex { return container } +func showLangPopout() { + mainPage.HidePage("stylePopOut") + mainPage.HidePage("keyMapPopOut") + mainPage.ShowPage("langPopOut") + app.SetFocus(langCycle.GetCurrentUI()) +} + +func showStylePopout() { + mainPage.HidePage("langPopOut") + mainPage.HidePage("keyMapPopOut") + mainPage.ShowPage("stylePopOut") + app.SetFocus(styleCycle.GetCurrentUI()) +} + +func showKeyMapPopout() { + mainPage.HidePage("langPopOut") + mainPage.HidePage("stylePopOut") + mainPage.ShowPage("keyMapPopOut") +} + func uiInit() { // input/output srcInput.SetBorder(true) @@ -296,7 +316,7 @@ func uiInit() { Item{item: attachItems(true, tview.FlexColumn, Item{item: attachItems(false, tview.FlexRow, Item{item: translatorDropDown, fixedSize: 0, proportion: 1, focus: false}), - fixedSize: 0, proportion: 2, focus: false}), + fixedSize: 0, proportion: 1, focus: false}), fixedSize: 1, proportion: 1, focus: false}, Item{item: attachItems(false, tview.FlexColumn, Item{item: srcLangDropDown, fixedSize: 0, proportion: 1, focus: true}, @@ -402,23 +422,9 @@ func uiInit() { mainPage.HidePage("keyMapPopOut") } }) - langButton.SetSelectedFunc(func() { - mainPage.HidePage("stylePopOut") - mainPage.HidePage("keyMapPopOut") - mainPage.ShowPage("langPopOut") - app.SetFocus(langCycle.GetCurrentUI()) - }) - styleButton.SetSelectedFunc(func() { - mainPage.HidePage("langPopOut") - mainPage.HidePage("keyMapPopOut") - mainPage.ShowPage("stylePopOut") - app.SetFocus(styleCycle.GetCurrentUI()) - }) - keyMapButton.SetSelectedFunc(func() { - mainPage.HidePage("langPopOut") - mainPage.HidePage("stylePopOut") - mainPage.ShowPage("keyMapPopOut") - }) + langButton.SetSelectedFunc(showLangPopout) + styleButton.SetSelectedFunc(showStylePopout) + keyMapButton.SetSelectedFunc(showKeyMapPopout) } func mainPageHandler(event *tcell.EventKey) *tcell.EventKey { @@ -454,13 +460,13 @@ func translateWindowHandler(event *tcell.EventKey) *tcell.EventKey { message := srcInput.GetText() // Only translate when message exist if len(message) > 0 { - translation, definition, partOfSpeech, err := translator.Translate(message) + translation, err := translator.Translate(message) if err != nil { dstOutput.SetText(err.Error()) } else { - dstOutput.SetText(translation) - defOutput.SetText(definition, false) - posOutput.SetText(partOfSpeech, false) + dstOutput.SetText(translation.TEXT) + defOutput.SetText(translation.DEF, false) + posOutput.SetText(translation.POS, false) } } case tcell.KeyCtrlQ: @@ -494,7 +500,7 @@ func translateWindowHandler(event *tcell.EventKey) *tcell.EventKey { } dstOutput.SetText(srcText) case tcell.KeyCtrlO: - // Play source sound + // Play text to speech on source of translation window. if translator.LockAvailable() { message := srcInput.GetText() // Only play when message exist @@ -504,13 +510,14 @@ func translateWindowHandler(event *tcell.EventKey) *tcell.EventKey { err := translator.PlayTTS(translator.GetSrcLang(), message) if err != nil { srcInput.SetText(err.Error(), true) + app.Draw() } }() } } case tcell.KeyCtrlP: - // Play destination sound + // Play text to speech on destination of translation window. if translator.LockAvailable() { message := dstOutput.GetText(false) // Only play when message exist @@ -520,6 +527,7 @@ func translateWindowHandler(event *tcell.EventKey) *tcell.EventKey { err := translator.PlayTTS(translator.GetDstLang(), message) if err != nil { dstOutput.SetText(err.Error()) + app.Draw() } }() } @@ -537,19 +545,11 @@ func popOutHandler(event *tcell.EventKey) *tcell.EventKey { switch ch { case '1': - mainPage.HidePage("stylePopOut") - mainPage.HidePage("keyMapPopOut") - mainPage.ShowPage("langPopOut") - app.SetFocus(langCycle.GetCurrentUI()) + showLangPopout() case '2': - mainPage.HidePage("langPopOut") - mainPage.HidePage("keyMapPopOut") - mainPage.ShowPage("stylePopOut") - app.SetFocus(styleCycle.GetCurrentUI()) + showStylePopout() case '3': - mainPage.HidePage("langPopOut") - mainPage.HidePage("stylePopOut") - mainPage.ShowPage("keyMapPopOut") + showKeyMapPopout() } return event