diff --git a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml index 8c643ea3f6be..b53a4608cb14 100644 --- a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml +++ b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml @@ -27,65 +27,89 @@ data-l10n-id="translations-panel-settings-button" closemenu="none" /> - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + diff --git a/browser/components/translations/content/selectTranslationsPanel.js b/browser/components/translations/content/selectTranslationsPanel.js index 2893d9ab61a1..faa150830899 100644 --- a/browser/components/translations/content/selectTranslationsPanel.js +++ b/browser/components/translations/content/selectTranslationsPanel.js @@ -164,12 +164,20 @@ var SelectTranslationsPanel = new (class { fromMenuList: "select-translations-panel-from", fromMenuPopup: "select-translations-panel-from-menupopup", header: "select-translations-panel-header", + mainContent: "select-translations-panel-main-content", textArea: "select-translations-panel-text-area", toLabel: "select-translations-panel-to-label", toMenuList: "select-translations-panel-to", toMenuPopup: "select-translations-panel-to-menupopup", + translateButton: "select-translations-panel-translate-button", translateFullPageButton: "select-translations-panel-translate-full-page-button", + tryAnotherSourceMenuList: + "select-translations-panel-try-another-language", + unsupportedLanguageContent: + "select-translations-panel-unsupported-language-content", + unsupportedLanguageMessageBar: + "select-translations-panel-unsupported-language-message-bar", }); } @@ -290,10 +298,12 @@ var SelectTranslationsPanel = new (class { */ async #initializeLanguageMenuLists(langPairPromise) { const { fromLang, toLang } = await langPairPromise; - const { fromMenuList, toMenuList } = this.elements; + const { fromMenuList, toMenuList, tryAnotherSourceMenuList } = + this.elements; await Promise.all([ this.#initializeLanguageMenuList(fromLang, fromMenuList), this.#initializeLanguageMenuList(toLang, toMenuList), + this.#initializeLanguageMenuList(null, tryAnotherSourceMenuList), ]); } @@ -313,7 +323,7 @@ var SelectTranslationsPanel = new (class { return; } - this.#registerSourceText(sourceText); + this.#registerSourceText(sourceText, langPairPromise); await this.#ensureLangListsBuilt(); await Promise.all([ @@ -321,7 +331,6 @@ var SelectTranslationsPanel = new (class { this.#initializeLanguageMenuLists(langPairPromise), ]); - this.#displayIdlePlaceholder(); this.#maybeRequestTranslation(); await this.#openPopup(event, screenX, screenY); } @@ -335,6 +344,7 @@ var SelectTranslationsPanel = new (class { */ async #openPopup(event, screenX, screenY) { await window.ensureCustomElements("moz-button-group"); + await window.ensureCustomElements("moz-message-bar"); this.console?.log("Showing SelectTranslationsPanel"); const { panel } = this.elements; @@ -346,14 +356,30 @@ var SelectTranslationsPanel = new (class { * on the length of the text. * * @param {string} sourceText - The text to translate. + * @param {Promise<{fromLang?: string, toLang?: string}>} langPairPromise * * @returns {Promise} */ - #registerSourceText(sourceText) { + async #registerSourceText(sourceText, langPairPromise) { const { textArea } = this.elements; - this.#changeStateTo("idle", /* retainEntries */ false, { - sourceText, - }); + const { fromLang, toLang } = await langPairPromise; + const isFromLangSupported = await TranslationsParent.isSupportedAsFromLang( + fromLang + ); + + if (isFromLangSupported) { + this.#changeStateTo("idle", /* retainEntries */ false, { + sourceText, + fromLanguage: fromLang, + toLanguage: toLang, + }); + } else { + this.#changeStateTo("unsupported", /* retainEntries */ false, { + sourceText, + detectedLanguage: fromLang, + toLanguage: toLang, + }); + } if (sourceText.length < SelectTranslationsPanel.textLengthThreshold) { textArea.style.height = SelectTranslationsPanel.shortTextHeight; @@ -586,7 +612,8 @@ var SelectTranslationsPanel = new (class { case "closed": case "idle": case "translatable": - case "translated": { + case "translated": + case "unsupported": { textArea.classList.remove("translating"); break; } @@ -615,9 +642,11 @@ var SelectTranslationsPanel = new (class { return; } - const { fromLanguage, toLanguage } = this.#translationState; + const { fromLanguage, toLanguage, detectedLanguage } = + this.#translationState; + const sourceLanguage = fromLanguage ? fromLanguage : detectedLanguage; this.console?.debug( - `SelectTranslationsPanel (${fromLanguage ? fromLanguage : "??"}-${ + `SelectTranslationsPanel (${sourceLanguage ? sourceLanguage : "??"}-${ toLanguage ? toLanguage : "??" }) state change (${previousPhase} => ${phase})` ); @@ -682,13 +711,15 @@ var SelectTranslationsPanel = new (class { let nextPhase = "translatable"; - if (!fromLanguage || !toLanguage) { - // No valid language pair is selected, so we cannot translate. - nextPhase = "idle"; - } else if ( + if ( + // No from-language is selected, so we cannot translate. + !fromLanguage || + // No to-language is selected, so we cannot translate. + !toLanguage || // The languages have not changed, so there is nothing to do. - previousFromLanguage === fromLanguage && - previousToLanguage === toLanguage + (this.phase() !== "idle" && + previousFromLanguage === fromLanguage && + previousToLanguage === toLanguage) ) { nextPhase = previousPhase; } @@ -727,6 +758,8 @@ var SelectTranslationsPanel = new (class { * Displays the placeholder text for the translation state's "idle" phase. */ #displayIdlePlaceholder() { + this.#showMainContent(); + const { textArea } = SelectTranslationsPanel.elements; textArea.value = this.#idlePlaceholderText; this.#updateTextDirection(); @@ -738,6 +771,8 @@ var SelectTranslationsPanel = new (class { * Displays the placeholder text for the translation state's "translating" phase. */ #displayTranslatingPlaceholder() { + this.#showMainContent(); + const { textArea } = SelectTranslationsPanel.elements; textArea.value = this.#translatingPlaceholderText; this.#updateTextDirection(); @@ -749,6 +784,8 @@ var SelectTranslationsPanel = new (class { * Displays the translated text for the translation state's "translated" phase. */ #displayTranslatedText() { + this.#showMainContent(); + const { toLanguage } = this.#getSelectedLanguagePair(); const { textArea } = SelectTranslationsPanel.elements; textArea.value = this.getTranslatedText(); @@ -757,12 +794,48 @@ var SelectTranslationsPanel = new (class { this.#indicateTranslatedTextArea({ overflow: "auto" }); } + /** + * Sets attributes on panel elements that are specifically relevant + * to the SelectTranslationsPanel's state. + * + * @param {object} options - Options of which attributes to set. + * @param {Record} options.makeHidden - Make these elements hidden. + * @param {Record} options.makeVisible - Make these elements visible. + * @param {Record} options.addDefault - Give these elements the default attribute. + * @param {Record} options.removeDefault - Remove the default attribute from these elements. + */ + #setPanelElementAttributes({ + makeHidden = [], + makeVisible = [], + addDefault = [], + removeDefault = [], + }) { + for (const element of makeHidden) { + element.hidden = true; + } + for (const element of makeVisible) { + element.hidden = false; + } + for (const element of addDefault) { + element.setAttribute("default", "true"); + } + for (const element of removeDefault) { + element.removeAttribute("default"); + } + } + /** * Enables or disables UI components that are conditional on a valid language pair being selected. */ #updateConditionalUIEnabledState() { const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); - const { copyButton, translateFullPageButton, textArea } = this.elements; + const { + copyButton, + translateFullPageButton, + translateButton, + textArea, + tryAnotherSourceMenuList, + } = this.elements; const invalidLangPairSelected = !fromLanguage || !toLanguage; const isTranslating = this.phase() === "translating"; @@ -770,6 +843,7 @@ var SelectTranslationsPanel = new (class { textArea.disabled = invalidLangPairSelected; translateFullPageButton.disabled = invalidLangPairSelected; copyButton.disabled = invalidLangPairSelected || isTranslating; + translateButton.disabled = !tryAnotherSourceMenuList.value; } /** @@ -789,9 +863,84 @@ var SelectTranslationsPanel = new (class { this.#displayTranslatedText(); break; } + case "unsupported": { + this.#displayUnsupportedLanguageMessage(); + } } } + /** + * Shows the panel's main-content group of elements. + */ + #showMainContent() { + const { + mainContent, + unsupportedLanguageContent, + doneButton, + translateButton, + translateFullPageButton, + } = this.elements; + this.#setPanelElementAttributes({ + makeHidden: [unsupportedLanguageContent, translateButton], + makeVisible: [mainContent, doneButton, translateFullPageButton], + addDefault: [doneButton], + removeDefault: [translateButton], + }); + } + + /** + * Shows the panel's unsupported-language group of elements. + */ + #showUnsupportedLanguageContent() { + const { + mainContent, + unsupportedLanguageContent, + doneButton, + translateButton, + translateFullPageButton, + } = this.elements; + this.#setPanelElementAttributes({ + makeHidden: [mainContent, translateFullPageButton], + makeVisible: [unsupportedLanguageContent, doneButton, translateButton], + addDefault: [translateButton], + removeDefault: [doneButton], + }); + } + + /** + * Displays the panel's unsupported language message bar, showing + * the panel's unsupported-language elements. + */ + #displayUnsupportedLanguageMessage() { + const { detectedLanguage } = this.#translationState; + const { unsupportedLanguageMessageBar } = this.elements; + const displayNames = new Services.intl.DisplayNames(undefined, { + type: "language", + }); + try { + const language = displayNames.of(detectedLanguage); + if (language) { + document.l10n.setAttributes( + unsupportedLanguageMessageBar, + "select-translations-panel-unsupported-language-message-known", + { language } + ); + } else { + // Will be immediately caught. + throw new Error(); + } + } catch { + // Either displayNames.of() threw, or we threw due to no display name found. + // In either case, localize the message for an unknown language. + document.l10n.setAttributes( + unsupportedLanguageMessageBar, + "select-translations-panel-unsupported-language-message-unknown" + ); + } + this.#updateConditionalUIEnabledState(); + this.#showUnsupportedLanguageContent(); + } + /** * Sets the text direction attribute in the text areas based on the specified language. * Uses the given language tag if provided, otherwise uses the current app locale. diff --git a/browser/components/translations/tests/browser/browser.toml b/browser/components/translations/tests/browser/browser.toml index 4a6e386c7ae3..b88969357ee5 100644 --- a/browser/components/translations/tests/browser/browser.toml +++ b/browser/components/translations/tests/browser/browser.toml @@ -134,3 +134,5 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227 ["browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js"] ["browser_translations_select_panel_translate_on_open.js"] + +["browser_translations_select_panel_unsupported_language.js"] diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js b/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js new file mode 100644 index 000000000000..56d3e42ff69d --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of opening the SelectTranslationsPanel to an unsupported language + * and then clicking the done button to close the panel. + */ +add_task( + async function test_select_translations_panel_unsupported_click_done_button() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/head.js b/browser/components/translations/tests/browser/head.js index 8ec31f3e0493..b2a7dae6f595 100644 --- a/browser/components/translations/tests/browser/head.js +++ b/browser/components/translations/tests/browser/head.js @@ -1456,11 +1456,16 @@ class SelectTranslationsTestUtils { fromMenuList: false, fromMenuPopup: false, header: false, + mainContent: false, textArea: false, toLabel: false, toMenuList: false, toMenuPopup: false, + translateButton: false, translateFullPageButton: false, + tryAnotherSourceMenuList: false, + unsupportedLanguageContent: false, + unsupportedLanguageMessageBar: false, // Overwrite any of the above defaults with the passed in expectations. ...expectations, } @@ -1505,6 +1510,7 @@ class SelectTranslationsTestUtils { fromLabel: true, fromMenuList: true, header: true, + mainContent: true, textArea: true, toLabel: true, toMenuList: true, @@ -1535,6 +1541,27 @@ class SelectTranslationsTestUtils { ); } + /** + * Asserts that the SelectTranslationsPanel UI matches the expected + * state when the panel has completed its translation. + */ + static async assertPanelViewUnsupportedLanguage() { + await SelectTranslationsTestUtils.waitForPanelState("unsupported"); + SelectTranslationsTestUtils.#assertPanelElementVisibility({ + betaIcon: true, + doneButton: true, + header: true, + translateButton: true, + tryAnotherSourceMenuList: true, + unsupportedLanguageContent: true, + unsupportedLanguageMessageBar: true, + }); + SelectTranslationsTestUtils.#assertConditionalUIEnabled({ + doneButton: true, + translateButton: false, + }); + } + /** * Asserts that the SelectTranslationsPanel translated text area is * both scrollable and scrolled to the top. @@ -1597,6 +1624,7 @@ class SelectTranslationsTestUtils { fromLabel: true, fromMenuList: true, header: true, + mainContent: true, textArea: true, toLabel: true, toMenuList: true, diff --git a/browser/locales-preview/select-translations.ftl b/browser/locales-preview/select-translations.ftl index 39c882ec646c..18a0f8c63fe5 100644 --- a/browser/locales-preview/select-translations.ftl +++ b/browser/locales-preview/select-translations.ftl @@ -38,6 +38,12 @@ select-translations-panel-from-label = From # Text displayed above the to-language dropdown menu. select-translations-panel-to-label = To +# Text displayed above the try-another-source-language dropdown menu. +select-translations-panel-try-another-language-label = Try another source language + +# Text displayed on the cancel button. +select-translations-panel-cancel-button = Cancel + # Text displayed on the copy button. select-translations-panel-copy-button = Copy @@ -47,8 +53,22 @@ select-translations-panel-done-button = Done # Text displayed on translate-full-page button. select-translations-panel-translate-full-page-button = Translate full page +# Text displayed on translate button. +select-translations-panel-translate-button = Translate + # Text displayed as a placeholder when the panel is idle. select-translations-panel-idle-placeholder-text = Translated text will appear here. # Text displayed as a placeholder when the panel is actively translating. select-translations-panel-translating-placeholder-text = Translating… + +# If your language requires declining the language name, a possible solution +# is to adapt the structure of the phrase, or use a support noun, e.g. +# `Sorry, we don't support the language yet: { $language } +# +# Variables: +# $language (string) - The language of the document. +select-translations-panel-unsupported-language-message-known = + .message = Sorry, we don’t support { $language } yet. +select-translations-panel-unsupported-language-message-unknown = + .message = Sorry, we don’t support this language yet. diff --git a/toolkit/components/translations/translations.d.ts b/toolkit/components/translations/translations.d.ts index 9823f6a84536..a3d92e1d141d 100644 --- a/toolkit/components/translations/translations.d.ts +++ b/toolkit/components/translations/translations.d.ts @@ -276,3 +276,4 @@ export type SelectTranslationsPanelState = | { phase: "translatable"; fromLanguage: string; toLanguage: string, sourceText: string, } | { phase: "translating"; fromLanguage: string; toLanguage: string, sourceText: string, } | { phase: "translated"; fromLanguage: string; toLanguage: string, sourceText: string, translatedText: string, } + | { phase: "unsupported"; detectedLanguage: string; toLanguage: string, sourceText: string }