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 }