From b6ddd10aadcf8c55bc373eb71ff0caae45c7ed3a Mon Sep 17 00:00:00 2001 From: Erik Nordin Date: Tue, 18 Apr 2023 16:25:22 +0000 Subject: [PATCH] Bug 1813777 - Display beta tags on beta languages for Firefox Translations r=gregtatum,fluent-reviewers,flod Displays languages as being in beta in the selectors for both the about:translations page and for in-page translations. Differential Revision: https://phabricator.services.mozilla.com/D175440 --- .../translations/content/translationsPanel.js | 24 +++++-- .../browser/browser_translations_panel.js | 56 +++++++++++++-- browser/locales-preview/translations.ftl | 6 ++ .../actors/TranslationsParent.sys.mjs | 52 ++++++++++---- .../translations/content/translations.mjs | 44 +++++++++--- .../browser/browser_about_translations.js | 70 +++++++++++++------ .../tests/browser/browser_full_page.js | 12 ++-- .../browser/browser_translations_actor.js | 16 ++--- .../translations/tests/browser/shared-head.js | 2 +- .../components/translations/translations.d.ts | 4 +- toolkit/locales-preview/aboutTranslations.ftl | 8 +++ 11 files changed, 223 insertions(+), 71 deletions(-) diff --git a/browser/components/translations/content/translationsPanel.js b/browser/components/translations/content/translationsPanel.js index 8313fe38a1c3..b0130720ccfa 100644 --- a/browser/components/translations/content/translationsPanel.js +++ b/browser/components/translations/content/translationsPanel.js @@ -168,16 +168,32 @@ var TranslationsPanel = new (class { throw new Error("No translation languages were retrieved."); } - for (const { langTag, displayName } of fromLanguages) { + for (const { langTag, isBeta, displayName } of fromLanguages) { const fromMenuItem = document.createXULElement("menuitem"); - fromMenuItem.setAttribute("label", displayName); fromMenuItem.setAttribute("value", langTag); + if (isBeta) { + document.l10n.setAttributes( + fromMenuItem, + "translations-panel-displayname-beta", + { language: displayName } + ); + } else { + fromMenuItem.setAttribute("label", displayName); + } this.elements.fromMenuPopup.appendChild(fromMenuItem); } - for (const { langTag, displayName } of toLanguages) { + for (const { langTag, isBeta, displayName } of toLanguages) { const toMenuItem = document.createXULElement("menuitem"); - toMenuItem.setAttribute("label", displayName); toMenuItem.setAttribute("value", langTag); + if (isBeta) { + document.l10n.setAttributes( + toMenuItem, + "translations-panel-displayname-beta", + { language: displayName } + ); + } else { + toMenuItem.setAttribute("label", displayName); + } this.elements.toMenuPopup.appendChild(toMenuItem); } this.#langListsPhase = "initialized"; diff --git a/browser/components/translations/tests/browser/browser_translations_panel.js b/browser/components/translations/tests/browser/browser_translations_panel.js index ec042164a397..28e95472715e 100644 --- a/browser/components/translations/tests/browser/browser_translations_panel.js +++ b/browser/components/translations/tests/browser/browser_translations_panel.js @@ -4,10 +4,12 @@ "use strict"; const languagePairs = [ - { fromLang: "es", toLang: "en" }, - { fromLang: "en", toLang: "es" }, - { fromLang: "fr", toLang: "en" }, - { fromLang: "en", toLang: "fr" }, + { fromLang: "es", toLang: "en", isBeta: false }, + { fromLang: "en", toLang: "es", isBeta: false }, + { fromLang: "fr", toLang: "en", isBeta: false }, + { fromLang: "en", toLang: "fr", isBeta: false }, + { fromLang: "en", toLang: "uk", isBeta: true }, + { fromLang: "uk", toLang: "en", isBeta: true }, ]; const spanishPageUrl = TRANSLATIONS_TESTER_ES; @@ -226,3 +228,49 @@ add_task(async function test_translations_panel_switch_language() { await cleanup(); }); + +/** + * Tests that languages are displayed correctly as being in beta or not. + */ +add_task(async function test_translations_panel_display_beta_languages() { + const { cleanup } = await loadTestPage({ + page: spanishPageUrl, + languagePairs, + }); + + function assertBetaDisplay(selectElement) { + const betaL10nId = "translations-panel-displayname-beta"; + const options = selectElement.firstChild.getElementsByTagName("menuitem"); + for (const option of options) { + for (const languagePair of languagePairs) { + if ( + languagePair.fromLang === option.value || + languagePair.toLang === option.value + ) { + if (option.getAttribute("data-l10n-id") === betaL10nId) { + is( + languagePair.isBeta, + true, + `Since data-l10n-id was ${betaL10nId} for ${option.value}, then it must be part of a beta language pair, but it was not.` + ); + } + if (!languagePair.isBeta) { + is( + option.getAttribute("data-l10n-id") === betaL10nId, + false, + `Since the languagePair is non-beta, the language option ${option.value} should not have a data-l10-id of ${betaL10nId}, but it does.` + ); + } + } + } + } + } + + const fromSelect = document.getElementById("translations-panel-from"); + const toSelect = document.getElementById("translations-panel-to"); + + assertBetaDisplay(fromSelect); + assertBetaDisplay(toSelect); + + await cleanup(); +}); diff --git a/browser/locales-preview/translations.ftl b/browser/locales-preview/translations.ftl index b5a4fda0bc2c..53003eef551b 100644 --- a/browser/locales-preview/translations.ftl +++ b/browser/locales-preview/translations.ftl @@ -14,6 +14,12 @@ translations-panel-dual-from-label = Choose the current page language translations-panel-dual-to-label = Choose the language to translate into translations-panel-dual-translate-button = Translate +# Text displayed on a language dropdown when the language is in beta +# Variables: +# $language (string) - The localized display name of the detected language +translations-panel-displayname-beta = + .label = { $language } BETA + ## The translation panel appears from the url bar, and this view is the "restore" view ## that lets a user restore a page to the original language. diff --git a/toolkit/components/translations/actors/TranslationsParent.sys.mjs b/toolkit/components/translations/actors/TranslationsParent.sys.mjs index ccc649a0dd3c..780028f86b98 100644 --- a/toolkit/components/translations/actors/TranslationsParent.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsParent.sys.mjs @@ -423,15 +423,17 @@ export class TranslationsParent extends JSWindowActorParent { } const records = await this.#getTranslationModelRecords(); const languagePairKeys = new Set(); - for (const { fromLang, toLang } of records.values()) { - languagePairKeys.add(fromLang + toLang); + for (const { fromLang, toLang, version } of records.values()) { + const isBeta = Services.vc.compare(version, "1.0") < 0; + languagePairKeys.add({ key: fromLang + toLang, isBeta }); } const languagePairs = []; - for (const key of languagePairKeys) { + for (const { key, isBeta } of languagePairKeys) { languagePairs.push({ fromLang: key[0] + key[1], toLang: key[2] + key[3], + isBeta, }); } @@ -447,14 +449,31 @@ export class TranslationsParent extends JSWindowActorParent { async getSupportedLanguages() { const languagePairs = await this.getLanguagePairs(); - /** @type {Set} */ - const fromLanguages = new Set(); - /** @type {Set} */ - const toLanguages = new Set(); + /** @type {Map} */ + const fromLanguages = new Map(); + /** @type {Map} */ + const toLanguages = new Map(); - for (const { fromLang, toLang } of languagePairs) { - fromLanguages.add(fromLang); - toLanguages.add(toLang); + for (const { fromLang, toLang, isBeta } of languagePairs) { + // [BetaLanguage, BetaLanguage] => isBeta == true, + // [BetaLanguage, NonBetaLanguage] => isBeta == true, + // [NonBetaLanguage, BetaLanguage] => isBeta == true, + // [NonBetaLanguage, NonBetaLanguage] => isBeta == false, + if (isBeta) { + // If these languages are part of a beta languagePair, at least one of them is a beta language + // but the other may not be, so only tentatively mark them as beta if there is no entry. + if (!fromLanguages.has(fromLang)) { + fromLanguages.set(fromLang, isBeta); + } + if (!toLanguages.has(toLang)) { + toLanguages.set(toLang, isBeta); + } + } else { + // If these languages are part of a non-beta languagePair, then they are both + // guaranteed to be non-beta languages. Idempotently overwrite any previous entry. + fromLanguages.set(fromLang, isBeta); + toLanguages.set(toLang, isBeta); + } } // Build a map of the langTag to the display name. @@ -466,7 +485,7 @@ export class TranslationsParent extends JSWindowActorParent { }); for (const langTagSet of [fromLanguages, toLanguages]) { - for (const langTag of langTagSet) { + for (const langTag of langTagSet.keys()) { if (displayNames.has(langTag)) { continue; } @@ -475,8 +494,9 @@ export class TranslationsParent extends JSWindowActorParent { } } - const addDisplayName = langTag => ({ + const addDisplayName = ([langTag, isBeta]) => ({ langTag, + isBeta, displayName: displayNames.get(langTag), }); @@ -484,8 +504,12 @@ export class TranslationsParent extends JSWindowActorParent { return { languagePairs, - fromLanguages: [...fromLanguages].map(addDisplayName).sort(sort), - toLanguages: [...toLanguages].map(addDisplayName).sort(sort), + fromLanguages: Array.from(fromLanguages.entries()) + .map(addDisplayName) + .sort(sort), + toLanguages: Array.from(toLanguages.entries()) + .map(addDisplayName) + .sort(sort), }; } diff --git a/toolkit/components/translations/content/translations.mjs b/toolkit/components/translations/content/translations.mjs index 1ee8f53c5283..ed28e3c012fe 100644 --- a/toolkit/components/translations/content/translations.mjs +++ b/toolkit/components/translations/content/translations.mjs @@ -286,9 +286,9 @@ class TranslationsState { ({ langTag }) => langTag === languageLabel ); if (entry) { - const { displayName } = entry; + const { displayName, isBeta } = entry; await this.setFromLanguage(languageLabel); - this.ui.setDetectOptionTextContent(displayName); + this.ui.setDetectOptionTextContent(displayName, isBeta); } } @@ -390,17 +390,41 @@ class TranslationsUI { const supportedLanguages = await this.state.supportedLanguages; // Update the DOM elements with the display names. - for (const { langTag, displayName } of supportedLanguages.toLanguages) { + for (const { + langTag, + isBeta, + displayName, + } of supportedLanguages.toLanguages) { const option = document.createElement("option"); option.value = langTag; - option.text = displayName; + if (isBeta) { + document.l10n.setAttributes( + option, + "about-translations-displayname-beta", + { language: displayName } + ); + } else { + option.text = displayName; + } this.languageTo.add(option); } - for (const { langTag, displayName } of supportedLanguages.fromLanguages) { + for (const { + langTag, + isBeta, + displayName, + } of supportedLanguages.fromLanguages) { const option = document.createElement("option"); option.value = langTag; - option.text = displayName; + if (isBeta) { + document.l10n.setAttributes( + option, + "about-translations-displayname-beta", + { language: displayName } + ); + } else { + option.text = displayName; + } this.languageFrom.add(option); } @@ -464,12 +488,14 @@ class TranslationsUI { * * @param {string} displayName */ - setDetectOptionTextContent(displayName) { + setDetectOptionTextContent(displayName, isBeta = false) { + // Set the text to the fluent value that takes an arg to display the language name. if (displayName) { - // Set the text to the fluent value that takes an arg to display the language name. document.l10n.setAttributes( this.#detectOption, - "about-translations-detect-lang", + isBeta + ? "about-translations-detect-lang-beta" + : "about-translations-detect-lang", { language: displayName } ); } else { diff --git a/toolkit/components/translations/tests/browser/browser_about_translations.js b/toolkit/components/translations/tests/browser/browser_about_translations.js index 80b0070b1cd4..dd08efc8952f 100644 --- a/toolkit/components/translations/tests/browser/browser_about_translations.js +++ b/toolkit/components/translations/tests/browser/browser_about_translations.js @@ -82,14 +82,16 @@ add_task(async function test_about_translations_disabled() { }); add_task(async function test_about_translations_dropdowns() { + let languagePairs = [ + { fromLang: "en", toLang: "es", isBeta: false }, + { fromLang: "es", toLang: "en", isBeta: false }, + // This is not a bi-directional translation. + { fromLang: "is", toLang: "en", isBeta: true }, + ]; await openAboutTranslations({ - languagePairs: [ - { fromLang: "en", toLang: "es" }, - { fromLang: "es", toLang: "en" }, - // This is not a bi-directional translation. - { fromLang: "is", toLang: "en" }, - ], - runInPage: async ({ selectors }) => { + languagePairs, + dataForContent: languagePairs, + runInPage: async ({ dataForContent: languagePairs, selectors }) => { const { document } = content; await ContentTaskUtils.waitForCondition(() => { @@ -112,16 +114,38 @@ add_task(async function test_about_translations_dropdowns() { availableOptions, selectedValue, }) { - const options = [...select.options] - .filter(option => !option.hidden) - .map(option => option.value); - + const options = [...select.options]; + const betaL10nId = "about-translations-displayname-beta"; + for (const option of options) { + for (const languagePair of languagePairs) { + if ( + languagePair.fromLang === option.value || + languagePair.toLang === option.value + ) { + if (option.getAttribute("data-l10n-id") === betaL10nId) { + is( + languagePair.isBeta, + true, + `Since data-l10n-id was ${betaL10nId} for ${option.value}, then it must be part of a beta language pair, but it was not.` + ); + } + if (!languagePair.isBeta) { + is( + option.getAttribute("data-l10n-id") === betaL10nId, + false, + `Since the languagePair is non-beta, the language option ${option.value} should not have a data-l10-id of ${betaL10nId}, but it does.` + ); + } + } + } + } info(message); Assert.deepEqual( - options, + options.filter(option => !option.hidden).map(option => option.value), availableOptions, "The available options match." ); + is(selectedValue, select.value, "The selected value matches."); } @@ -192,10 +216,10 @@ add_task(async function test_about_translations_dropdowns() { add_task(async function test_about_translations_translations() { await openAboutTranslations({ languagePairs: [ - { fromLang: "en", toLang: "fr" }, - { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr", isBeta: false }, + { fromLang: "fr", toLang: "en", isBeta: false }, // This is not a bi-directional translation. - { fromLang: "is", toLang: "en" }, + { fromLang: "is", toLang: "en", isBeta: true }, ], runInPage: async ({ selectors }) => { const { document, window } = content; @@ -280,8 +304,8 @@ add_task(async function test_about_translations_language_directions() { await openAboutTranslations({ languagePairs: [ // English (en) is LTR and Arabic (ar) is RTL. - { fromLang: "en", toLang: "ar" }, - { fromLang: "ar", toLang: "en" }, + { fromLang: "en", toLang: "ar", isBeta: true }, + { fromLang: "ar", toLang: "en", isBeta: true }, ], runInPage: async ({ selectors }) => { const { document, window } = content; @@ -351,8 +375,8 @@ add_task(async function test_about_translations_language_directions() { add_task(async function test_about_translations_debounce() { await openAboutTranslations({ languagePairs: [ - { fromLang: "en", toLang: "fr" }, - { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr", isBeta: false }, + { fromLang: "fr", toLang: "en", isBeta: false }, ], runInPage: async ({ selectors }) => { const { document, window } = content; @@ -431,8 +455,8 @@ add_task(async function test_about_translations_debounce() { add_task(async function test_about_translations_html() { await openAboutTranslations({ languagePairs: [ - { fromLang: "en", toLang: "fr" }, - { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr", isBeta: false }, + { fromLang: "fr", toLang: "en", isBeta: false }, ], prefs: [["browser.translations.useHTML", true]], runInPage: async ({ selectors }) => { @@ -492,8 +516,8 @@ add_task(async function test_about_translations_language_identification() { detectedLanguageLabel: "en", detectedLanguageConfidence: "0.98", languagePairs: [ - { fromLang: "en", toLang: "fr" }, - { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr", isBeta: false }, + { fromLang: "fr", toLang: "en", isBeta: false }, ], runInPage: async ({ selectors }) => { const { document, window } = content; diff --git a/toolkit/components/translations/tests/browser/browser_full_page.js b/toolkit/components/translations/tests/browser/browser_full_page.js index 95dc93c68ce9..5b03675f8503 100644 --- a/toolkit/components/translations/tests/browser/browser_full_page.js +++ b/toolkit/components/translations/tests/browser/browser_full_page.js @@ -11,8 +11,8 @@ add_task(async function test_full_page_translation() { page: TRANSLATIONS_TESTER_ES, prefs: [["browser.translations.autoTranslate", true]], languagePairs: [ - { fromLang: "es", toLang: "en" }, - { fromLang: "en", toLang: "es" }, + { fromLang: "es", toLang: "en", isBeta: false }, + { fromLang: "en", toLang: "es", isBeta: false }, ], runInPage: async TranslationsTest => { const selectors = TranslationsTest.getSelectors(); @@ -65,8 +65,8 @@ add_task(async function test_about_translations_enabled() { page: TRANSLATIONS_TESTER_EN, prefs: [["browser.translations.autoTranslate", true]], languagePairs: [ - { fromLang: "es", toLang: "en" }, - { fromLang: "en", toLang: "es" }, + { fromLang: "es", toLang: "en", isBeta: false }, + { fromLang: "en", toLang: "es", isBeta: false }, ], runInPage: async () => { const { document } = content; @@ -104,8 +104,8 @@ add_task(async function test_language_identification_for_page_translation() { detectedLanguageLabel: "es", detectedLanguageConfidence: 0.95, languagePairs: [ - { fromLang: "es", toLang: "en" }, - { fromLang: "en", toLang: "es" }, + { fromLang: "es", toLang: "en", isBeta: false }, + { fromLang: "en", toLang: "es", isBeta: false }, ], runInPage: async TranslationsTest => { const selectors = TranslationsTest.getSelectors(); diff --git a/toolkit/components/translations/tests/browser/browser_translations_actor.js b/toolkit/components/translations/tests/browser/browser_translations_actor.js index 55c92828a9f1..c34a7c192add 100644 --- a/toolkit/components/translations/tests/browser/browser_translations_actor.js +++ b/toolkit/components/translations/tests/browser/browser_translations_actor.js @@ -19,13 +19,13 @@ add_task(async function test_pivot_language_behavior() { const { actor, cleanup } = await setupActorTest({ languagePairs: [ - { fromLang: "en", toLang: "es" }, - { fromLang: "es", toLang: "en" }, + { fromLang: "en", toLang: "es", isBeta: false }, + { fromLang: "es", toLang: "en", isBeta: false }, // This is not a bi-directional translation. - { fromLang: "is", toLang: "en" }, + { fromLang: "is", toLang: "en", isBeta: false }, // These are non-pivot languages. - { fromLang: "zh", toLang: "ja" }, - { fromLang: "ja", toLang: "zh" }, + { fromLang: "zh", toLang: "ja", isBeta: true }, + { fromLang: "ja", toLang: "zh", isBeta: true }, ], }); @@ -34,9 +34,9 @@ add_task(async function test_pivot_language_behavior() { Assert.deepEqual( languagePairs, [ - { fromLang: "en", toLang: "es" }, - { fromLang: "es", toLang: "en" }, - { fromLang: "is", toLang: "en" }, + { fromLang: "en", toLang: "es", isBeta: false }, + { fromLang: "es", toLang: "en", isBeta: false }, + { fromLang: "is", toLang: "en", isBeta: false }, ], "Non-pivot languages were removed." ); diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js index 5e521a36d92d..e4dc1c6835da 100644 --- a/toolkit/components/translations/tests/browser/shared-head.js +++ b/toolkit/components/translations/tests/browser/shared-head.js @@ -45,7 +45,7 @@ const TRANSLATIONS_TESTER_NO_TAG = * This is the two-letter language label for the MockedLanguageIdEngine to return as * the mocked detected language. * - * @param {Array<{ fromLang: string, toLang: string}>} options.languagePairs + * @param {Array<{ fromLang: string, toLang: string, isBeta: boolean }>} options.languagePairs * The translation languages pairs to mock for the test. * * @param {Array<[string, string]>} options.prefs diff --git a/toolkit/components/translations/translations.d.ts b/toolkit/components/translations/translations.d.ts index 2c899c40e0c4..faab00dcb8d2 100644 --- a/toolkit/components/translations/translations.d.ts +++ b/toolkit/components/translations/translations.d.ts @@ -301,6 +301,6 @@ export interface LanguagePair { fromLang: string, toLang: string }; */ export interface SupportedLanguages { langPairs: LanguagePair[], - fromLanguages: Array<{ langTag: string, displayName: string }>, - toLanguages: Array<{ langTag: string, displayName: string }>, + fromLanguages: Array<{ langTag: string, isBeta: boolean, displayName: string, }>, + toLanguages: Array<{ langTag: string, isBeta: boolean, displayName: string }>, } diff --git a/toolkit/locales-preview/aboutTranslations.ftl b/toolkit/locales-preview/aboutTranslations.ftl index 4ef2c6e02a77..68e9d8aafd71 100644 --- a/toolkit/locales-preview/aboutTranslations.ftl +++ b/toolkit/locales-preview/aboutTranslations.ftl @@ -8,10 +8,18 @@ about-translations-header = { -translations-brand-name } about-translations-results-placeholder = Translation # Text displayed on from-language dropdown when no language is selected about-translations-detect = Detect language +# Text displayed on a language dropdown when the language is in beta +# Variables: +# $language (string) - The localized display name of the language +about-translations-displayname-beta = { $language } BETA # Text displayed on from-language dropdown when a language is detected # Variables: # $language (string) - The localized display name of the detected language about-translations-detect-lang = Detect language ({ $language }) +# Text displayed on from-language dropdown when a beta language is detected +# Variables: +# $language (string) - The localized display name of the detected language +about-translations-detect-lang-beta = Detect language ({ $language } BETA) # Text displayed on to-language dropdown when no language is selected about-translations-select = Select language about-translations-textarea =