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
This commit is contained in:
Erik Nordin 2023-04-18 16:25:22 +00:00
parent 6829a78aa2
commit b6ddd10aad
11 changed files with 223 additions and 71 deletions

View File

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

View File

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

View File

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

View File

@ -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<string>} */
const fromLanguages = new Set();
/** @type {Set<string>} */
const toLanguages = new Set();
/** @type {Map<string, boolean>} */
const fromLanguages = new Map();
/** @type {Map<string, boolean>} */
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),
};
}

View File

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

View File

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

View File

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

View File

@ -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."
);

View File

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

View File

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

View File

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