-
Notifications
You must be signed in to change notification settings - Fork 67
Fix translate: working default backend, settings UI, outgoing translation #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -277,7 +277,14 @@ bool CChat::OnInput(const IInput::CEvent &Event) | |
| else if(GameClient()->m_TClient.ChatDoSpecId(m_Input.GetString())) | ||
| ; // Do nothing as specid was executed | ||
| else | ||
| SendChatQueued(m_Input.GetString()); | ||
| { | ||
| const char *pMessage = m_Input.GetString(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To reduce diff, make translate outgoing swallow messages like ChatDoSpecId above it |
||
| // Don't translate server commands (e.g. "/w name msg"), translating would break their syntax | ||
| if(g_Config.m_TcTranslateOutgoing && pMessage[0] != '\0' && pMessage[0] != '/') | ||
| GameClient()->m_Translate.TranslateOutgoing(m_Mode == MODE_TEAM ? 1 : 0, pMessage); | ||
| else | ||
| SendChatQueued(pMessage); | ||
| } | ||
| m_pHistoryEntry = nullptr; | ||
| DisableMode(); | ||
| GameClient()->OnRelease(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1094,6 +1094,117 @@ void CMenus::RenderSettingsTClientSettings(CUIRect MainView) | |
| Ui()->DoEditBox(&s_FinishName, &Button, EditBoxFontSize); | ||
| s_SectionBoxes.back().h = Column.y - s_SectionBoxes.back().y; | ||
|
|
||
| // ***** Translate ***** // | ||
| Column.HSplitTop(MarginBetweenSections, nullptr, &Column); | ||
| s_SectionBoxes.push_back(Column); | ||
| Column.HSplitTop(HeadlineHeight, &Label, &Column); | ||
| Ui()->DoLabel(&Label, TCLocalize("Translate"), HeadlineFontSize, TEXTALIGN_ML); | ||
| Column.HSplitTop(MarginSmall, nullptr, &Column); | ||
|
|
||
| static const char *s_apTranslateLanguageNames[] = { | ||
| "Arabic", "Chinese (Simplified)", "Chinese (Traditional)", "Czech", "Dutch", "English", | ||
| "Finnish", "French", "German", "Greek", "Hebrew", "Hindi", "Hungarian", "Italian", | ||
| "Japanese", "Korean", "Polish", "Portuguese", "Romanian", "Russian", "Spanish", "Swedish", | ||
| "Thai", "Turkish", "Ukrainian", "Vietnamese"}; | ||
| static const char *s_apTranslateLanguageCodes[] = { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The backend should determine the language options. Google has many more than libretranslate |
||
| "ar", "zh", "zh-TW", "cs", "nl", "en", | ||
| "fi", "fr", "de", "el", "he", "hi", "hu", "it", | ||
| "ja", "ko", "pl", "pt", "ro", "ru", "es", "sv", | ||
| "th", "tr", "uk", "vi"}; | ||
| const int NumTranslateLanguages = (int)std::size(s_apTranslateLanguageNames); | ||
| static_assert(std::size(s_apTranslateLanguageNames) == std::size(s_apTranslateLanguageCodes), "language name/code list length mismatch"); | ||
| auto TranslateLanguageIndex = [&](const char *pCode) { | ||
| for(int i = 0; i < NumTranslateLanguages; i++) | ||
| if(str_comp_nocase(pCode, s_apTranslateLanguageCodes[i]) == 0) | ||
| return i; | ||
| return 5; // Fall back to English if unset or a custom code we don't have a name for | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Magic number! (don't fix by static asserting english is 5) |
||
| }; | ||
|
|
||
| { | ||
| static std::vector<const char *> s_TranslateBackendNames = {"Google", "FreeTranslateAPI (defunct)", "LibreTranslate"}; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These values are duplicated from ibackend::name |
||
| static const char *s_apTranslateBackendValues[] = {"google", "ftapi", "libretranslate"}; | ||
| static CUi::SDropDownState s_TranslateBackendDropDownState; | ||
| static CScrollRegion s_TranslateBackendDropDownScrollRegion; | ||
| s_TranslateBackendDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_TranslateBackendDropDownScrollRegion; | ||
| int BackendSelectedOld = 0; | ||
| for(int i = 0; i < (int)std::size(s_apTranslateBackendValues); i++) | ||
| { | ||
| if(str_comp_nocase(g_Config.m_TcTranslateBackend, s_apTranslateBackendValues[i]) == 0) | ||
| BackendSelectedOld = i; | ||
| } | ||
| CUIRect BackendDropDownRect; | ||
| Column.HSplitTop(LineSize, &BackendDropDownRect, &Column); | ||
| BackendDropDownRect.VSplitLeft(120.0f, &Label, &BackendDropDownRect); | ||
| Ui()->DoLabel(&Label, TCLocalize("Backend: "), FontSize, TEXTALIGN_ML); | ||
| const int BackendSelectedNew = Ui()->DoDropDown(&BackendDropDownRect, BackendSelectedOld, s_TranslateBackendNames.data(), s_TranslateBackendNames.size(), s_TranslateBackendDropDownState); | ||
| if(BackendSelectedOld != BackendSelectedNew) | ||
| str_copy(g_Config.m_TcTranslateBackend, s_apTranslateBackendValues[BackendSelectedNew]); | ||
| } | ||
| Column.HSplitTop(MarginSmall, nullptr, &Column); | ||
|
|
||
| DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_TcTranslateAuto, TCLocalize("Automatically translate incoming messages"), &g_Config.m_TcTranslateAuto, &Column, LineSize); | ||
| if(g_Config.m_TcTranslateAuto && str_comp_nocase(g_Config.m_TcTranslateBackend, "ftapi") == 0) | ||
| { | ||
| Column.HSplitTop(LineSize, &Label, &Column); | ||
| Ui()->DoLabel(&Label, TCLocalize("FreeTranslateAPI does not support automatic translation, switch to Google or LibreTranslate"), FontSize * 0.8f, TEXTALIGN_ML); | ||
| } | ||
| CUIRect IncomingTargetBox; | ||
| Column.HSplitTop(LineSize + MarginExtraSmall, &IncomingTargetBox, &Column); | ||
| if(g_Config.m_TcTranslateAuto) | ||
| { | ||
| IncomingTargetBox.HSplitTop(MarginExtraSmall, nullptr, &IncomingTargetBox); | ||
| IncomingTargetBox.VSplitMid(&Label, &IncomingTargetBox); | ||
| Ui()->DoLabel(&Label, TCLocalize("Translate incoming to:"), FontSize, TEXTALIGN_ML); | ||
| static CUi::SDropDownState s_IncomingLangDropDownState; | ||
| static CScrollRegion s_IncomingLangDropDownScrollRegion; | ||
| s_IncomingLangDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_IncomingLangDropDownScrollRegion; | ||
| const int IncomingLangOld = TranslateLanguageIndex(g_Config.m_TcTranslateTarget); | ||
| const int IncomingLangNew = Ui()->DoDropDown(&IncomingTargetBox, IncomingLangOld, s_apTranslateLanguageNames, NumTranslateLanguages, s_IncomingLangDropDownState); | ||
| if(IncomingLangOld != IncomingLangNew) | ||
| str_copy(g_Config.m_TcTranslateTarget, s_apTranslateLanguageCodes[IncomingLangNew]); | ||
| } | ||
| Column.HSplitTop(MarginSmall, nullptr, &Column); | ||
|
|
||
| DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_TcTranslateOutgoing, TCLocalize("Translate your outgoing messages"), &g_Config.m_TcTranslateOutgoing, &Column, LineSize); | ||
| CUIRect OutgoingTargetBox; | ||
| Column.HSplitTop(LineSize + MarginExtraSmall, &OutgoingTargetBox, &Column); | ||
| if(g_Config.m_TcTranslateOutgoing) | ||
| { | ||
| OutgoingTargetBox.HSplitTop(MarginExtraSmall, nullptr, &OutgoingTargetBox); | ||
| OutgoingTargetBox.VSplitMid(&Label, &OutgoingTargetBox); | ||
| Ui()->DoLabel(&Label, TCLocalize("Translate outgoing to:"), FontSize, TEXTALIGN_ML); | ||
| static CUi::SDropDownState s_OutgoingLangDropDownState; | ||
| static CScrollRegion s_OutgoingLangDropDownScrollRegion; | ||
| s_OutgoingLangDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_OutgoingLangDropDownScrollRegion; | ||
| const int OutgoingLangOld = TranslateLanguageIndex(g_Config.m_TcTranslateOutgoingTarget); | ||
| const int OutgoingLangNew = Ui()->DoDropDown(&OutgoingTargetBox, OutgoingLangOld, s_apTranslateLanguageNames, NumTranslateLanguages, s_OutgoingLangDropDownState); | ||
| if(OutgoingLangOld != OutgoingLangNew) | ||
| str_copy(g_Config.m_TcTranslateOutgoingTarget, s_apTranslateLanguageCodes[OutgoingLangNew]); | ||
| } | ||
| Column.HSplitTop(MarginSmall, nullptr, &Column); | ||
|
|
||
| if(str_comp_nocase(g_Config.m_TcTranslateBackend, "libretranslate") == 0) | ||
| { | ||
| CUIRect EndpointBox; | ||
| Column.HSplitTop(LineSize + MarginExtraSmall, &EndpointBox, &Column); | ||
| EndpointBox.VSplitMid(&Label, &EndpointBox); | ||
| Ui()->DoLabel(&Label, TCLocalize("LibreTranslate endpoint:"), FontSize, TEXTALIGN_ML); | ||
| static CLineInput s_TranslateEndpoint(g_Config.m_TcTranslateEndpoint, sizeof(g_Config.m_TcTranslateEndpoint)); | ||
| s_TranslateEndpoint.SetEmptyText("localhost:5000/translate"); | ||
| Ui()->DoEditBox(&s_TranslateEndpoint, &EndpointBox, EditBoxFontSize); | ||
| Column.HSplitTop(MarginExtraSmall, nullptr, &Column); | ||
|
|
||
| CUIRect KeyBox; | ||
| Column.HSplitTop(LineSize + MarginExtraSmall, &KeyBox, &Column); | ||
| KeyBox.VSplitMid(&Label, &KeyBox); | ||
| Ui()->DoLabel(&Label, TCLocalize("LibreTranslate API key:"), FontSize, TEXTALIGN_ML); | ||
| static CLineInput s_TranslateKey(g_Config.m_TcTranslateKey, sizeof(g_Config.m_TcTranslateKey)); | ||
| s_TranslateKey.SetEmptyText(TCLocalize("Optional")); | ||
| Ui()->DoEditBox(&s_TranslateKey, &KeyBox, EditBoxFontSize); | ||
| } | ||
| Column.HSplitTop(MarginExtraSmall, nullptr, &Column); | ||
| s_SectionBoxes.back().h = Column.y - s_SectionBoxes.back().y; | ||
|
|
||
| // ***** END OF PAGE 1 SETTINGS ***** // | ||
| RightView = Column; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -196,7 +196,7 @@ class CTranslateBackendLibretranslate : public ITranslateBackendHttp | |
| { | ||
| return "LibreTranslate"; | ||
| } | ||
| CTranslateBackendLibretranslate(IHttp &Http, const char *pText) | ||
| CTranslateBackendLibretranslate(IHttp &Http, const char *pText, const char *pTargetLang) | ||
| { | ||
| CJsonStringWriter Json = CJsonStringWriter(); | ||
| Json.BeginObject(); | ||
|
|
@@ -205,7 +205,7 @@ class CTranslateBackendLibretranslate : public ITranslateBackendHttp | |
| Json.WriteAttribute("source"); | ||
| Json.WriteStrValue("auto"); | ||
| Json.WriteAttribute("target"); | ||
| Json.WriteStrValue(EncodeTarget(g_Config.m_TcTranslateTarget)); | ||
| Json.WriteStrValue(EncodeTarget(pTargetLang)); | ||
| Json.WriteAttribute("format"); | ||
| Json.WriteStrValue("text"); | ||
| if(g_Config.m_TcTranslateKey[0] != '\0') | ||
|
|
@@ -289,19 +289,108 @@ class CTranslateBackendFtapi : public ITranslateBackendHttp | |
| { | ||
| return "FreeTranslateAPI"; | ||
| } | ||
| CTranslateBackendFtapi(IHttp &Http, const char *pText) | ||
| CTranslateBackendFtapi(IHttp &Http, const char *pText, const char *pTargetLang) | ||
| { | ||
| char aBuf[4096]; | ||
| str_format(aBuf, sizeof(aBuf), "%s/translate?dl=%s&text=", | ||
| g_Config.m_TcTranslateEndpoint[0] != '\0' ? g_Config.m_TcTranslateEndpoint : "https://ftapi.pythonanywhere.com", | ||
| EncodeTarget(g_Config.m_TcTranslateTarget)); | ||
| EncodeTarget(pTargetLang)); | ||
|
|
||
| UrlEncode(pText, aBuf + strlen(aBuf), sizeof(aBuf) - strlen(aBuf)); | ||
|
|
||
| CreateHttpRequest(Http, aBuf); | ||
| } | ||
| }; | ||
|
|
||
| // Talks directly to Google's free, unofficial (undocumented) translate endpoint. | ||
| // This is the same endpoint FreeTranslateAPI (ftapi.pythonanywhere.com, now dead) used to wrap. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Link to the repository or a more alive one |
||
| class CTranslateBackendGoogle : public ITranslateBackendHttp | ||
| { | ||
| private: | ||
| bool ParseResponseJson(const json_value *pObj, CTranslateResponse &Out) | ||
| { | ||
| if(!pObj) | ||
| { | ||
| str_copy(Out.m_Text, "Response is not JSON"); | ||
| return false; | ||
| } | ||
| if(pObj->type != json_array || json_array_length(pObj) < 3) | ||
| { | ||
| str_copy(Out.m_Text, "Unexpected response format"); | ||
| return false; | ||
| } | ||
|
|
||
| const json_value *pSentences = json_array_get(pObj, 0); | ||
| if(!pSentences || pSentences->type != json_array) | ||
| { | ||
| str_copy(Out.m_Text, "No translated sentences"); | ||
| return false; | ||
| } | ||
|
|
||
| char aText[sizeof(Out.m_Text)] = ""; | ||
| const int NumSentences = json_array_length(pSentences); | ||
| for(int i = 0; i < NumSentences; i++) | ||
| { | ||
| const json_value *pSentence = json_array_get(pSentences, i); | ||
| if(!pSentence || pSentence->type != json_array || json_array_length(pSentence) < 1) | ||
| continue; | ||
| const json_value *pChunk = json_array_get(pSentence, 0); | ||
| if(!pChunk || pChunk->type != json_string) | ||
| continue; | ||
| str_append(aText, pChunk->u.string.ptr); | ||
| } | ||
| if(aText[0] == '\0') | ||
| { | ||
| str_copy(Out.m_Text, "Empty translation"); | ||
| return false; | ||
| } | ||
|
|
||
| const json_value *pLanguage = json_array_get(pObj, 2); | ||
| if(pLanguage && pLanguage->type == json_string) | ||
| str_copy(Out.m_Language, pLanguage->u.string.ptr); | ||
|
|
||
| str_copy(Out.m_Text, aText); | ||
| return true; | ||
| } | ||
|
|
||
| protected: | ||
| bool ParseResponse(CTranslateResponse &Out) override | ||
| { | ||
| json_value *pObj = m_pHttpRequest->ResultJson(); | ||
| bool Res = ParseResponseJson(pObj, Out); | ||
| json_value_free(pObj); | ||
| return Res; | ||
| } | ||
|
|
||
| public: | ||
| const char *Name() const override | ||
| { | ||
| return "Google"; | ||
| } | ||
| CTranslateBackendGoogle(IHttp &Http, const char *pText, const char *pTargetLang) | ||
| { | ||
| char aBuf[4096]; | ||
| str_format(aBuf, sizeof(aBuf), "%s?client=gtx&sl=auto&dt=t&tl=%s&q=", | ||
| g_Config.m_TcTranslateEndpoint[0] != '\0' ? g_Config.m_TcTranslateEndpoint : "https://translate.googleapis.com/translate_a/single", | ||
| EncodeTarget(pTargetLang)); | ||
|
|
||
| UrlEncode(pText, aBuf + strlen(aBuf), sizeof(aBuf) - strlen(aBuf)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be much better if you could put it in the body |
||
|
|
||
| CreateHttpRequest(Http, aBuf); | ||
| } | ||
| }; | ||
|
|
||
| static std::unique_ptr<ITranslateBackend> CreateTranslateBackend(IHttp &Http, const char *pText, const char *pTargetLang) | ||
| { | ||
| if(str_comp_nocase(g_Config.m_TcTranslateBackend, "libretranslate") == 0) | ||
| return std::make_unique<CTranslateBackendLibretranslate>(Http, pText, pTargetLang); | ||
| if(str_comp_nocase(g_Config.m_TcTranslateBackend, "ftapi") == 0) | ||
| return std::make_unique<CTranslateBackendFtapi>(Http, pText, pTargetLang); | ||
| if(str_comp_nocase(g_Config.m_TcTranslateBackend, "google") == 0) | ||
| return std::make_unique<CTranslateBackendGoogle>(Http, pText, pTargetLang); | ||
| return nullptr; | ||
| } | ||
|
|
||
| void CTranslate::ConTranslate(IConsole::IResult *pResult, void *pUserData) | ||
| { | ||
| const char *pName; | ||
|
|
@@ -398,11 +487,8 @@ void CTranslate::Translate(CChat::CLine &Line, bool ShowProgress) | |
| Job.m_pTranslateResponse = std::make_shared<CTranslateResponse>(); | ||
| Job.m_pLine->m_pTranslateResponse = Job.m_pTranslateResponse; | ||
|
|
||
| if(str_comp_nocase(g_Config.m_TcTranslateBackend, "libretranslate") == 0) | ||
| Job.m_pBackend = std::make_unique<CTranslateBackendLibretranslate>(*Http(), Job.m_pLine->m_aText); | ||
| else if(str_comp_nocase(g_Config.m_TcTranslateBackend, "ftapi") == 0) | ||
| Job.m_pBackend = std::make_unique<CTranslateBackendFtapi>(*Http(), Job.m_pLine->m_aText); | ||
| else | ||
| Job.m_pBackend = CreateTranslateBackend(*Http(), Job.m_pLine->m_aText, g_Config.m_TcTranslateTarget); | ||
| if(!Job.m_pBackend) | ||
| { | ||
| GameClient()->m_Chat.Echo("Invalid translate backend"); | ||
| return; | ||
|
|
@@ -428,6 +514,23 @@ void CTranslate::OnRender() | |
| { | ||
| const auto Time = time(); | ||
| auto ForEach = [&](CTranslateJob &Job) { | ||
| if(Job.m_IsOutgoing) | ||
| { | ||
| const std::optional<bool> Done = Job.m_pBackend->Update(*Job.m_pTranslateResponse); | ||
| if(!Done.has_value()) | ||
| return false; // Keep ongoing tasks | ||
| if(*Done && Job.m_pTranslateResponse->m_Text[0] != '\0') | ||
| GameClient()->m_Chat.SendChat(Job.m_OutgoingTeam, Job.m_pTranslateResponse->m_Text); | ||
| else | ||
| { | ||
| // Translation failed (or came back empty): don't swallow the message, send it untranslated | ||
| char aBuf[sizeof(Job.m_pTranslateResponse->m_Text) + 64]; | ||
| str_format(aBuf, sizeof(aBuf), TCLocalize("Translating your message failed, sending it untranslated: %s", "translate"), Job.m_pTranslateResponse->m_Text); | ||
| GameClient()->m_Chat.Echo(aBuf); | ||
| GameClient()->m_Chat.SendChat(Job.m_OutgoingTeam, Job.m_aOutgoingOriginalText); | ||
| } | ||
| return true; | ||
| } | ||
| if(Job.m_pLine->m_pTranslateResponse != Job.m_pTranslateResponse) | ||
| return true; // Not the same line anymore | ||
| const std::optional<bool> Done = Job.m_pBackend->Update(*Job.m_pTranslateResponse); | ||
|
|
@@ -471,3 +574,29 @@ void CTranslate::AutoTranslate(CChat::CLine &Line) | |
| } | ||
| Translate(Line, false); | ||
| } | ||
|
|
||
| void CTranslate::TranslateOutgoing(int Team, const char *pText) | ||
| { | ||
| if(m_vJobs.size() > 15) | ||
| { | ||
| // Too many jobs in flight, don't make the player wait: send untranslated | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| GameClient()->m_Chat.SendChat(Team, pText); | ||
| return; | ||
| } | ||
|
|
||
| CTranslateJob Job; | ||
| Job.m_IsOutgoing = true; | ||
| Job.m_OutgoingTeam = Team; | ||
| str_copy(Job.m_aOutgoingOriginalText, pText); | ||
| Job.m_pTranslateResponse = std::make_shared<CTranslateResponse>(); | ||
|
|
||
| Job.m_pBackend = CreateTranslateBackend(*Http(), pText, g_Config.m_TcTranslateOutgoingTarget); | ||
| if(!Job.m_pBackend) | ||
| { | ||
| GameClient()->m_Chat.Echo("Invalid translate backend"); | ||
| GameClient()->m_Chat.SendChat(Team, pText); | ||
| return; | ||
| } | ||
|
|
||
| m_vJobs.emplace_back(std::move(Job)); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
People running their own stuff will just use libretranslate. You can remove this comment and the ftapi backend