Bug 1870314 - Add SelectTranslationsPanel unsupported-language content r=translations-reviewers,fluent-reviewers,gregtatum,bolsson

Adds the elements for showing the unsupported language error
in the SelectTranslationsPanel.

Differential Revision: https://phabricator.services.mozilla.com/D207199
This commit is contained in:
Erik Nordin 2024-04-15 13:04:39 +00:00
parent 17a3827652
commit 98490170e8
7 changed files with 339 additions and 75 deletions

View file

@ -27,65 +27,89 @@
data-l10n-id="translations-panel-settings-button"
closemenu="none" />
</hbox>
<vbox class="select-translations-panel-content">
<hbox id="select-translations-panel-lang-selection">
<vbox flex="1">
<label id="select-translations-panel-from-label"
class="select-translations-panel-label"
data-l10n-id="select-translations-panel-from-label">
</label>
<menulist id="select-translations-panel-from"
flex="1"
value=""
size="large"
data-l10n-id="translations-panel-choose-language"
aria-labelledby="select-translations-panel-from-label"
noinitialselection="true"
oncommand="SelectTranslationsPanel.onChangeFromLanguage(event)">
<menupopup id="select-translations-panel-from-menupopup"
onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)"
class="translations-panel-language-menupopup-from">
<!-- The list of <menuitem> will be dynamically inserted. -->
</menupopup>
</menulist>
</vbox>
<vbox flex="1">
<label id="select-translations-panel-to-label"
class="select-translations-panel-label"
data-l10n-id="select-translations-panel-to-label">
</label>
<menulist id="select-translations-panel-to"
flex="1"
value=""
size="large"
data-l10n-id="translations-panel-choose-language"
aria-labelledby="select-translations-panel-to-label"
noinitialselection="true"
oncommand="SelectTranslationsPanel.onChangeToLanguage(event)">
<menupopup id="select-translations-panel-to-menupopup"
onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)"
class="translations-panel-language-menupopup-to">
<!-- The list of <menuitem> will be dynamically inserted. -->
</menupopup>
</menulist>
</vbox>
<html:div id="select-translations-panel-unsupported-language-content" hidden="true">
<vbox flex="1" class="select-translations-panel-content">
<html:moz-message-bar id="select-translations-panel-unsupported-language-message-bar"
data-l10n-attrs="message">
</html:moz-message-bar>
<label id="select-translations-panel-try-another-language-label"
class="select-translations-panel-label"
data-l10n-id="select-translations-panel-try-another-language-label">
</label>
<menulist id="select-translations-panel-try-another-language"
flex="1"
value=""
size="large"
data-l10n-id="translations-panel-choose-language"
aria-labelledby="select-translations-panel-try-another-language-label"
noinitialselection="true">
<menupopup id="select-translations-panel-try-another-language-menupopup"
class="translations-panel-language-menupopup-from">
<!-- The list of <menuitem> will be dynamically inserted. -->
</menupopup>
</menulist>
</vbox>
</html:div>
<html:div id="select-translations-panel-main-content">
<vbox class="select-translations-panel-content">
<hbox id="select-translations-panel-lang-selection">
<vbox flex="1">
<label id="select-translations-panel-from-label"
class="select-translations-panel-label"
data-l10n-id="select-translations-panel-from-label">
</label>
<menulist id="select-translations-panel-from"
flex="1"
value=""
size="large"
data-l10n-id="translations-panel-choose-language"
aria-labelledby="select-translations-panel-from-label"
noinitialselection="true"
oncommand="SelectTranslationsPanel.onChangeFromLanguage(event)">
<menupopup id="select-translations-panel-from-menupopup"
onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)"
class="translations-panel-language-menupopup-from">
<!-- The list of <menuitem> will be dynamically inserted. -->
</menupopup>
</menulist>
</vbox>
<vbox flex="1">
<label id="select-translations-panel-to-label"
class="select-translations-panel-label"
data-l10n-id="select-translations-panel-to-label">
</label>
<menulist id="select-translations-panel-to"
flex="1"
value=""
size="large"
data-l10n-id="translations-panel-choose-language"
aria-labelledby="select-translations-panel-to-label"
noinitialselection="true"
oncommand="SelectTranslationsPanel.onChangeToLanguage(event)">
<menupopup id="select-translations-panel-to-menupopup"
onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)"
class="translations-panel-language-menupopup-to">
<!-- The list of <menuitem> will be dynamically inserted. -->
</menupopup>
</menulist>
</vbox>
</hbox>
</vbox>
<vbox class="select-translations-panel-content">
<html:textarea id="select-translations-panel-text-area"
class="select-translations-panel-text-area"
readonly="true"
tabindex="0">
</html:textarea>
</vbox>
<hbox class="select-translations-panel-content">
<button id="select-translations-panel-copy-button"
class="footer-button select-translations-panel-button select-translations-panel-copy-button"
data-l10n-id="select-translations-panel-copy-button">
</button>
</hbox>
</vbox>
<vbox class="select-translations-panel-content">
<html:textarea id="select-translations-panel-text-area"
class="select-translations-panel-text-area"
readonly="true"
tabindex="0">
</html:textarea>
</vbox>
<hbox class="select-translations-panel-content">
<button id="select-translations-panel-copy-button"
class="footer-button select-translations-panel-button select-translations-panel-copy-button"
data-l10n-id="select-translations-panel-copy-button">
</button>
</hbox>
</html:div>
<html:moz-button-group class="panel-footer translations-panel-footer">
<button id="select-translations-panel-translate-full-page-button"
class="footer-button select-translations-panel-button"
@ -97,6 +121,13 @@
default="true"
oncommand = "SelectTranslationsPanel.close()">
</button>
<button id="select-translations-panel-translate-button"
class="footer-button select-translations-panel-button"
data-l10n-id="select-translations-panel-translate-button"
hidden="true"
default="true"
disabled="true">
</button>
</html:moz-button-group>
</panel>
</html:template>

View file

@ -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<void>}
*/
#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<string, Element[]>} options.makeHidden - Make these elements hidden.
* @param {Record<string, Element[]>} options.makeVisible - Make these elements visible.
* @param {Record<string, Element[]>} options.addDefault - Give these elements the default attribute.
* @param {Record<string, Element[]>} 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.

View file

@ -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"]

View file

@ -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();
}
);

View file

@ -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,

View file

@ -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 dont support { $language } yet.
select-translations-panel-unsupported-language-message-unknown =
.message = Sorry, we dont support this language yet.

View file

@ -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 }