Bug 1942349 - Add support for model variants; r=translations-reviewers,settings-reviewers,mossop,nordzilla

This is a big patch, but it adds the support for model variants by
defining a language pair struct instead of a fromLanguage and
toLanguage. This struct can also include a source variant and target
variant. The code will resolve the pivot models as needed.

This is not going to be release user facing, but can be used in a
Nightly setting. There is nothing enforcing this, but it's the intention
of the code.

This will allow us to test multiple variants of models in Nightly and
gather feedback from users and other engineers.

Differential Revision: https://phabricator.services.mozilla.com/D235495
This commit is contained in:
Greg Tatum
2025-01-28 22:12:37 +00:00
parent 1b13d61402
commit 65fb960486
24 changed files with 931 additions and 739 deletions

View File

@@ -87,7 +87,7 @@ export class nsContextMenu {
* A promise to retrieve the translations language pair * A promise to retrieve the translations language pair
* if the context menu was opened in a context relevant to * if the context menu was opened in a context relevant to
* open the SelectTranslationsPanel. * open the SelectTranslationsPanel.
* @type {Promise<{fromLanguage: string, toLanguage: string}>} * @type {Promise<{sourceLanguage: string, targetLanguage: string}>}
*/ */
#translationsLangPairPromise; #translationsLangPairPromise;
@@ -2570,22 +2570,22 @@ export class nsContextMenu {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async localizeTranslateSelectionItem(translateSelectionItem) { async localizeTranslateSelectionItem(translateSelectionItem) {
const { toLanguage } = await this.#translationsLangPairPromise; const { targetLanguage } = await this.#translationsLangPairPromise;
if (toLanguage) { if (targetLanguage) {
// A valid to-language exists, so localize the menuitem for that language. // A valid to-language exists, so localize the menuitem for that language.
let displayName; let displayName;
try { try {
const languageDisplayNames = const languageDisplayNames =
lazy.TranslationsParent.createLanguageDisplayNames(); lazy.TranslationsParent.createLanguageDisplayNames();
displayName = languageDisplayNames.of(toLanguage); displayName = languageDisplayNames.of(targetLanguage);
} catch { } catch {
// languageDisplayNames.of threw, do nothing. // languageDisplayNames.of threw, do nothing.
} }
if (displayName) { if (displayName) {
translateSelectionItem.setAttribute("target-language", toLanguage); translateSelectionItem.setAttribute("target-language", targetLanguage);
this.document.l10n.setAttributes( this.document.l10n.setAttributes(
translateSelectionItem, translateSelectionItem,
this.isTextSelected this.isTextSelected

View File

@@ -4,6 +4,10 @@
/* import-globals-from preferences.js */ /* import-globals-from preferences.js */
/**
* @typedef {import("../../../toolkit/components/translations/translations").SupportedLanguages} SupportedLanguages
*/
/** /**
* The permission type to give to Services.perms for Translations. * The permission type to give to Services.perms for Translations.
*/ */
@@ -53,9 +57,9 @@ let gTranslationsPane = {
downloadPhases: new Map(), downloadPhases: new Map(),
/** /**
* Object with details of languages supported by the browser namely * Object with details of languages supported by the browser.
* languagePairs, fromLanguages, toLanguages *
* @type {object} supportedLanguages * @type {SupportedLanguages}
*/ */
supportedLanguages: {}, supportedLanguages: {},
@@ -167,10 +171,10 @@ let gTranslationsPane = {
* Never translate settings list. * Never translate settings list.
*/ */
buildLanguageDropDowns() { buildLanguageDropDowns() {
const { fromLanguages } = this.supportedLanguages; const { sourceLanguages } = this.supportedLanguages;
const { alwaysTranslateMenuPopup, neverTranslateMenuPopup } = this.elements; const { alwaysTranslateMenuPopup, neverTranslateMenuPopup } = this.elements;
for (const { langTag, displayName } of fromLanguages) { for (const { langTag, displayName } of sourceLanguages) {
const alwaysLang = document.createXULElement("menuitem"); const alwaysLang = document.createXULElement("menuitem");
alwaysLang.setAttribute("value", langTag); alwaysLang.setAttribute("value", langTag);
alwaysLang.setAttribute("label", displayName); alwaysLang.setAttribute("label", displayName);

View File

@@ -2,6 +2,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* @typedef {typeof import("../../../../toolkit/components/translations/actors/TranslationsParent.sys.mjs").TranslationsParent} TranslationsParent
*/
/** @type {{ TranslationsParent: TranslationsParent }} */
const lazy = {}; const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, { ChromeUtils.defineESModuleGetters(lazy, {
@@ -173,7 +178,7 @@ export class TranslationsPanelShared {
); );
} }
/** @type {SupportedLanguages} */ /** @type {SupportedLanguages} */
const { languagePairs, fromLanguages, toLanguages } = const { languagePairs, sourceLanguages, targetLanguages } =
await lazy.TranslationsParent.getSupportedLanguages(); await lazy.TranslationsParent.getSupportedLanguages();
// Verify that we are in a proper state. // Verify that we are in a proper state.
@@ -199,9 +204,9 @@ export class TranslationsPanelShared {
while (popup.lastChild?.value) { while (popup.lastChild?.value) {
popup.lastChild.remove(); popup.lastChild.remove();
} }
for (const { langTag, displayName } of fromLanguages) { for (const { langTagKey, displayName } of sourceLanguages) {
const fromMenuItem = document.createXULElement("menuitem"); const fromMenuItem = document.createXULElement("menuitem");
fromMenuItem.setAttribute("value", langTag); fromMenuItem.setAttribute("value", langTagKey);
fromMenuItem.setAttribute("label", displayName); fromMenuItem.setAttribute("label", displayName);
popup.appendChild(fromMenuItem); popup.appendChild(fromMenuItem);
} }
@@ -211,9 +216,9 @@ export class TranslationsPanelShared {
while (popup.lastChild?.value) { while (popup.lastChild?.value) {
popup.lastChild.remove(); popup.lastChild.remove();
} }
for (const { langTag, displayName } of toLanguages) { for (const { langTagKey, displayName } of targetLanguages) {
const toMenuItem = document.createXULElement("menuitem"); const toMenuItem = document.createXULElement("menuitem");
toMenuItem.setAttribute("value", langTag); toMenuItem.setAttribute("value", langTagKey);
toMenuItem.setAttribute("label", displayName); toMenuItem.setAttribute("label", displayName);
popup.appendChild(toMenuItem); popup.appendChild(toMenuItem);
} }

View File

@@ -444,16 +444,20 @@ var FullPageTranslationsPanel = new (class {
this.elements; this.elements;
const { requestedLanguagePair, isEngineReady } = languageState; const { requestedLanguagePair, isEngineReady } = languageState;
// Remove the model variant. e.g. "ru,base" -> "ru"
const selectedFrom = fromMenuList.value.split(",")[0];
const selectedTo = toMenuList.value.split(",")[0];
if ( if (
requestedLanguagePair && requestedLanguagePair &&
!isEngineReady && !isEngineReady &&
TranslationsUtils.langTagsMatch( TranslationsUtils.langTagsMatch(
fromMenuList.value, selectedFrom,
requestedLanguagePair.fromLanguage requestedLanguagePair.sourceLanguage
) && ) &&
TranslationsUtils.langTagsMatch( TranslationsUtils.langTagsMatch(
toMenuList.value, selectedTo,
requestedLanguagePair.toLanguage requestedLanguagePair.targetLanguage
) )
) { ) {
// A translation has been requested, but is not ready yet. // A translation has been requested, but is not ready yet.
@@ -475,29 +479,29 @@ var FullPageTranslationsPanel = new (class {
// No "from" language was provided. // No "from" language was provided.
!fromMenuList.value || !fromMenuList.value ||
// The translation languages are the same, don't allow this translation. // The translation languages are the same, don't allow this translation.
TranslationsUtils.langTagsMatch(toMenuList.value, fromMenuList.value) || TranslationsUtils.langTagsMatch(selectedFrom, selectedTo) ||
// This is the requested language pair. // This is the requested language pair.
(requestedLanguagePair && (requestedLanguagePair &&
TranslationsUtils.langTagsMatch( TranslationsUtils.langTagsMatch(
requestedLanguagePair.fromLanguage, requestedLanguagePair.sourceLanguage,
fromMenuList.value selectedFrom
) && ) &&
TranslationsUtils.langTagsMatch( TranslationsUtils.langTagsMatch(
requestedLanguagePair.toLanguage, requestedLanguagePair.targetLanguage,
toMenuList.value selectedTo
)); ));
} }
if (requestedLanguagePair && isEngineReady) { if (requestedLanguagePair && isEngineReady) {
const { fromLanguage, toLanguage } = requestedLanguagePair; const { sourceLanguage, targetLanguage } = requestedLanguagePair;
const languageDisplayNames = const languageDisplayNames =
TranslationsParent.createLanguageDisplayNames(); TranslationsParent.createLanguageDisplayNames();
cancelButton.hidden = true; cancelButton.hidden = true;
this.updateUIForReTranslation(true /* isReTranslation */); this.updateUIForReTranslation(true /* isReTranslation */);
document.l10n.setAttributes(header, "translations-panel-revisit-header", { document.l10n.setAttributes(header, "translations-panel-revisit-header", {
fromLanguage: languageDisplayNames.of(fromLanguage), fromLanguage: languageDisplayNames.of(sourceLanguage),
toLanguage: languageDisplayNames.of(toLanguage), toLanguage: languageDisplayNames.of(targetLanguage),
}); });
} else { } else {
document.l10n.setAttributes(header, "translations-panel-header"); document.l10n.setAttributes(header, "translations-panel-header");
@@ -588,6 +592,8 @@ var FullPageTranslationsPanel = new (class {
fromMenuList.value = ""; fromMenuList.value = "";
error.hidden = true; error.hidden = true;
langSelection.hidden = false; langSelection.hidden = false;
// Remove the model variant. e.g. "ru,base" -> "ru"
const selectedSource = fromMenuList.value.split(",")[0];
const { userLangTag, docLangTag, isDocLangTagSupported } = const { userLangTag, docLangTag, isDocLangTagSupported } =
await this.#fetchDetectedLanguages().then(langTags => langTags ?? {}); await this.#fetchDetectedLanguages().then(langTags => langTags ?? {});
@@ -626,14 +632,15 @@ var FullPageTranslationsPanel = new (class {
// Avoid offering to translate into the original source language. // Avoid offering to translate into the original source language.
docLangTag, docLangTag,
// Avoid same-language to same-language translations if possible. // Avoid same-language to same-language translations if possible.
fromMenuList.value, selectedSource,
], ],
}); });
} }
if ( const resolvedSource = fromMenuList.value.split(",")[0];
TranslationsUtils.langTagsMatch(fromMenuList.value, toMenuList.value) const resolvedTarget = toMenuList.value.split(",")[0];
) {
if (TranslationsUtils.langTagsMatch(resolvedSource, resolvedTarget)) {
// The best possible user-preferred language tag that we were able to find for the // The best possible user-preferred language tag that we were able to find for the
// toMenuList is the same as the fromMenuList, but same-language to same-language // toMenuList is the same as the fromMenuList, but same-language to same-language
// translations are not allowed in Full Page Translations, so we will just show the // translations are not allowed in Full Page Translations, so we will just show the
@@ -848,9 +855,9 @@ var FullPageTranslationsPanel = new (class {
/** /**
* Configures the panel for the user to reset the page after it has been translated. * Configures the panel for the user to reset the page after it has been translated.
* *
* @param {TranslationPair} translationPair * @param {LanguagePair} languagePair
*/ */
async #showRevisitView({ fromLanguage, toLanguage }) { async #showRevisitView({ sourceLanguage, targetLanguage, sourceVariant }) {
const { fromMenuList, toMenuList, intro } = this.elements; const { fromMenuList, toMenuList, intro } = this.elements;
if (!this.#isShowingDefaultView()) { if (!this.#isShowingDefaultView()) {
await this.#showDefaultView( await this.#showDefaultView(
@@ -858,13 +865,17 @@ var FullPageTranslationsPanel = new (class {
); );
} }
intro.hidden = true; intro.hidden = true;
fromMenuList.value = fromLanguage; if (sourceVariant) {
fromMenuList.value = `${sourceLanguage},${sourceVariant}`;
} else {
fromMenuList.value = sourceLanguage;
}
toMenuList.value = await TranslationsParent.getTopPreferredSupportedToLang({ toMenuList.value = await TranslationsParent.getTopPreferredSupportedToLang({
excludeLangTags: [ excludeLangTags: [
// Avoid offering to translate into the original source language. // Avoid offering to translate into the original source language.
fromLanguage, sourceLanguage,
// Avoid offering to translate into current active target language. // Avoid offering to translate into current active target language.
toLanguage, targetLanguage,
], ],
}); });
this.onChangeLanguages(); this.onChangeLanguages();
@@ -1241,9 +1252,13 @@ var FullPageTranslationsPanel = new (class {
const actor = TranslationsParent.getTranslationsActor( const actor = TranslationsParent.getTranslationsActor(
gBrowser.selectedBrowser gBrowser.selectedBrowser
); );
const [sourceLanguage, sourceVariant] =
this.elements.fromMenuList.value.split(",");
const [targetLanguage, targetVariant] =
this.elements.toMenuList.value.split(",");
actor.translate( actor.translate(
this.elements.fromMenuList.value, { sourceLanguage, targetLanguage, sourceVariant, targetVariant },
this.elements.toMenuList.value,
false // reportAsAutoTranslate false // reportAsAutoTranslate
); );
} }
@@ -1604,10 +1619,10 @@ var FullPageTranslationsPanel = new (class {
"urlbar-translations-button-translated", "urlbar-translations-button-translated",
{ {
fromLanguage: languageDisplayNames.of( fromLanguage: languageDisplayNames.of(
requestedLanguagePair.fromLanguage requestedLanguagePair.sourceLanguage
), ),
toLanguage: languageDisplayNames.of( toLanguage: languageDisplayNames.of(
requestedLanguagePair.toLanguage requestedLanguagePair.targetLanguage
), ),
} }
); );
@@ -1615,7 +1630,7 @@ var FullPageTranslationsPanel = new (class {
buttonLocale.hidden = false; buttonLocale.hidden = false;
buttonCircleArrows.hidden = true; buttonCircleArrows.hidden = true;
buttonLocale.innerText = buttonLocale.innerText =
requestedLanguagePair.toLanguage.split("-")[0]; requestedLanguagePair.targetLanguage.split("-")[0];
} else { } else {
document.l10n.setAttributes( document.l10n.setAttributes(
button, button,

View File

@@ -6,6 +6,7 @@
/** /**
* @typedef {import("../../../../toolkit/components/translations/translations").SelectTranslationsPanelState} SelectTranslationsPanelState * @typedef {import("../../../../toolkit/components/translations/translations").SelectTranslationsPanelState} SelectTranslationsPanelState
* @typedef {import("../../../../toolkit/components/translations/translations").LanguagePair} LanguagePair
*/ */
ChromeUtils.defineESModuleGetters(this, { ChromeUtils.defineESModuleGetters(this, {
@@ -14,7 +15,7 @@ ChromeUtils.defineESModuleGetters(this, {
TranslationsPanelShared: TranslationsPanelShared:
"chrome://browser/content/translations/TranslationsPanelShared.sys.mjs", "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs",
TranslationsUtils: TranslationsUtils:
"chrome://global/content/translations/TranslationsUtils.sys.mjs", "chrome://global/content/translations/TranslationsUtils.mjs",
Translator: "chrome://global/content/translations/Translator.mjs", Translator: "chrome://global/content/translations/Translator.mjs",
}); });
@@ -313,7 +314,7 @@ var SelectTranslationsPanel = new (class {
// First see if any of the detected languages are supported and return it if so. // First see if any of the detected languages are supported and return it if so.
const { language, languages } = const { language, languages } =
await LanguageDetector.detectLanguage(textToTranslate); await LanguageDetector.detectLanguage(textToTranslate);
const languagePairs = await TranslationsParent.getLanguagePairs(); const languagePairs = await TranslationsParent.getNonPivotLanguagePairs();
for (const { languageCode } of languages) { for (const { languageCode } of languages) {
const compatibleLangTag = const compatibleLangTag =
TranslationsParent.findCompatibleSourceLangTagSync( TranslationsParent.findCompatibleSourceLangTagSync(
@@ -409,8 +410,8 @@ var SelectTranslationsPanel = new (class {
* based on user settings. * based on user settings.
* *
* @param {string} textToTranslate - The text for which the language detection and target language retrieval are performed. * @param {string} textToTranslate - The text for which the language detection and target language retrieval are performed.
* @returns {Promise<{fromLanguage?: string, toLanguage?: string}>} - An object containing the language pair for the translation. * @returns {Promise<{sourceLanguage?: string, targetLanguage?: string}>} - An object containing the language pair for the translation.
* The `fromLanguage` property is omitted if it is a language that is not currently supported by Firefox Translations. * The `sourceLanguage` property is omitted if it is a language that is not currently supported by Firefox Translations.
*/ */
async getLangPairPromise(textToTranslate) { async getLangPairPromise(textToTranslate) {
if ( if (
@@ -423,19 +424,20 @@ var SelectTranslationsPanel = new (class {
// we still need to ensure that the translate-selection menuitem in the context menu // we still need to ensure that the translate-selection menuitem in the context menu
// is compatible with all code in other tests, so we will return "en" for the purpose // is compatible with all code in other tests, so we will return "en" for the purpose
// of being able to localize and display the context-menu item in other test cases. // of being able to localize and display the context-menu item in other test cases.
return { toLang: "en" }; return { targetLanguage: "en" };
} }
const fromLanguage = const sourceLanguage =
await SelectTranslationsPanel.getTopSupportedDetectedLanguage( await SelectTranslationsPanel.getTopSupportedDetectedLanguage(
textToTranslate textToTranslate
); );
const toLanguage = await TranslationsParent.getTopPreferredSupportedToLang({ const targetLanguage =
await TranslationsParent.getTopPreferredSupportedToLang({
// Avoid offering a same-language to same-language translation if we can. // Avoid offering a same-language to same-language translation if we can.
excludeLangTags: [fromLanguage], excludeLangTags: [sourceLanguage],
}); });
return { fromLanguage, toLanguage }; return { sourceLanguage, targetLanguage };
} }
/** /**
@@ -485,12 +487,12 @@ var SelectTranslationsPanel = new (class {
* Initializes the selected values of the from-language and to-language menu * Initializes the selected values of the from-language and to-language menu
* lists based on the result of the given language pair promise. * lists based on the result of the given language pair promise.
* *
* @param {Promise<{fromLanguage?: string, toLanguage?: string}>} langPairPromise * @param {Promise<{sourceLanguage?: string, targetLanguage?: string}>} langPairPromise
* *
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async #initializeLanguageMenuLists(langPairPromise) { async #initializeLanguageMenuLists(langPairPromise) {
const { fromLanguage, toLanguage } = await langPairPromise; const { sourceLanguage, targetLanguage } = await langPairPromise;
const { const {
fromMenuList, fromMenuList,
fromMenuPopup, fromMenuPopup,
@@ -500,8 +502,8 @@ var SelectTranslationsPanel = new (class {
} = this.elements; } = this.elements;
await Promise.all([ await Promise.all([
this.#initializeLanguageMenuList(fromLanguage, fromMenuList), this.#initializeLanguageMenuList(sourceLanguage, fromMenuList),
this.#initializeLanguageMenuList(toLanguage, toMenuList), this.#initializeLanguageMenuList(targetLanguage, toMenuList),
this.#initializeLanguageMenuList(null, tryAnotherSourceMenuList), this.#initializeLanguageMenuList(null, tryAnotherSourceMenuList),
]); ]);
@@ -576,7 +578,7 @@ var SelectTranslationsPanel = new (class {
return; return;
} }
const { fromLanguage, toLanguage } = await langPairPromise; const { sourceLanguage, targetLanguage } = await langPairPromise;
const { docLangTag, topPreferredLanguage } = this.#getLanguageInfo(); const { docLangTag, topPreferredLanguage } = this.#getLanguageInfo();
TranslationsParent.telemetry() TranslationsParent.telemetry()
@@ -584,8 +586,8 @@ var SelectTranslationsPanel = new (class {
.onOpen({ .onOpen({
maintainFlow, maintainFlow,
docLangTag, docLangTag,
fromLanguage, sourceLanguage,
toLanguage, targetLanguage,
topPreferredLanguage, topPreferredLanguage,
textSource: isTextSelected ? "selection" : "hyperlink", textSource: isTextSelected ? "selection" : "hyperlink",
}); });
@@ -627,7 +629,7 @@ var SelectTranslationsPanel = new (class {
const { requestedLanguagePair } = TranslationsParent.getTranslationsActor( const { requestedLanguagePair } = TranslationsParent.getTranslationsActor(
gBrowser.selectedBrowser gBrowser.selectedBrowser
).languageState; ).languageState;
return requestedLanguagePair?.toLanguage; return requestedLanguagePair?.targetLanguage;
} catch { } catch {
this.console.warn("Failed to retrieve the TranslationsParent actor."); this.console.warn("Failed to retrieve the TranslationsParent actor.");
} }
@@ -718,27 +720,27 @@ var SelectTranslationsPanel = new (class {
* on the length of the text. * on the length of the text.
* *
* @param {string} sourceText - The text to translate. * @param {string} sourceText - The text to translate.
* @param {Promise<{fromLanguage?: string, toLanguage?: string}>} langPairPromise * @param {Promise<{sourceLanguage?: string, targetLanguage?: string}>} langPairPromise
* *
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async #registerSourceText(sourceText, langPairPromise) { async #registerSourceText(sourceText, langPairPromise) {
const { textArea } = this.elements; const { textArea } = this.elements;
const { fromLanguage, toLanguage } = await langPairPromise; const { sourceLanguage, targetLanguage } = await langPairPromise;
const compatibleFromLang = const compatibleFromLang =
await TranslationsParent.findCompatibleSourceLangTag(fromLanguage); await TranslationsParent.findCompatibleSourceLangTag(sourceLanguage);
if (compatibleFromLang) { if (compatibleFromLang) {
this.#changeStateTo("idle", /* retainEntries */ false, { this.#changeStateTo("idle", /* retainEntries */ false, {
sourceText, sourceText,
fromLanguage: compatibleFromLang, sourceLanguage: compatibleFromLang,
toLanguage, targetLanguage,
}); });
} else { } else {
this.#changeStateTo("unsupported", /* retainEntries */ false, { this.#changeStateTo("unsupported", /* retainEntries */ false, {
sourceText, sourceText,
detectedLanguage: fromLanguage, detectedLanguage: sourceLanguage,
toLanguage, targetLanguage,
}); });
} }
@@ -1286,14 +1288,14 @@ var SelectTranslationsPanel = new (class {
*/ */
onClickTranslateButton() { onClickTranslateButton() {
const { fromMenuList, tryAnotherSourceMenuList } = this.elements; const { fromMenuList, tryAnotherSourceMenuList } = this.elements;
const { detectedLanguage, toLanguage } = this.#translationState; const { detectedLanguage, targetLanguage } = this.#translationState;
fromMenuList.value = tryAnotherSourceMenuList.value; fromMenuList.value = tryAnotherSourceMenuList.value;
TranslationsParent.telemetry().selectTranslationsPanel().onTranslateButton({ TranslationsParent.telemetry().selectTranslationsPanel().onTranslateButton({
detectedLanguage, detectedLanguage,
fromLanguage: fromMenuList.value, sourceLanguage: fromMenuList.value,
toLanguage, targetLanguage,
}); });
this.#maybeRequestTranslation(); this.#maybeRequestTranslation();
@@ -1308,7 +1310,7 @@ var SelectTranslationsPanel = new (class {
.onTranslateFullPageButton(); .onTranslateFullPageButton();
const { panel } = this.elements; const { panel } = this.elements;
const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); const languagePair = this.#getSelectedLanguagePair();
try { try {
const actor = TranslationsParent.getTranslationsActor( const actor = TranslationsParent.getTranslationsActor(
@@ -1318,8 +1320,7 @@ var SelectTranslationsPanel = new (class {
"popuphidden", "popuphidden",
() => () =>
actor.translate( actor.translate(
fromLanguage, languagePair,
toLanguage,
false // reportAsAutoTranslate false // reportAsAutoTranslate
), ),
{ once: true } { once: true }
@@ -1468,30 +1469,36 @@ var SelectTranslationsPanel = new (class {
/** /**
* Checks if the given language pair matches the panel's currently selected language pair. * Checks if the given language pair matches the panel's currently selected language pair.
* *
* @param {string} fromLanguage - The from-language to compare. * @param {string} sourceLanguage - The from-language to compare.
* @param {string} toLanguage - The to-language to compare. * @param {string} targetLanguage - The to-language to compare.
* *
* @returns {boolean} - True if the given language pair matches the selected languages in the panel UI, otherwise false. * @returns {boolean} - True if the given language pair matches the selected languages in the panel UI, otherwise false.
*/ */
#isSelectedLangPair(fromLanguage, toLanguage) { #isSelectedLangPair(sourceLanguage, targetLanguage) {
const { fromLanguage: selectedFromLang, toLanguage: selectedToLang } = const selected = this.#getSelectedLanguagePair();
this.#getSelectedLanguagePair();
return ( return (
TranslationsUtils.langTagsMatch(fromLanguage, selectedFromLang) && TranslationsUtils.langTagsMatch(
TranslationsUtils.langTagsMatch(toLanguage, selectedToLang) sourceLanguage,
selected.sourceLanguage
) &&
TranslationsUtils.langTagsMatch(targetLanguage, selected.targetLanguage)
); );
} }
/** /**
* Retrieves the currently selected language pair from the menu lists. * Retrieves the currently selected language pair from the menu lists.
* *
* @returns {{fromLanguage: string, toLanguage: string}} An object containing the selected languages. * @returns {LanguagePair}
*/ */
#getSelectedLanguagePair() { #getSelectedLanguagePair() {
const { fromMenuList, toMenuList } = this.elements; const { fromMenuList, toMenuList } = this.elements;
const [sourceLanguage, sourceVariant] = fromMenuList.value.split(",");
const [targetLanguage, targetVariant] = toMenuList.value.split(",");
return { return {
fromLanguage: fromMenuList.value, sourceLanguage,
toLanguage: toMenuList.value, targetLanguage,
sourceVariant,
targetVariant,
}; };
} }
@@ -1584,12 +1591,11 @@ var SelectTranslationsPanel = new (class {
return; return;
} }
const { fromLanguage, toLanguage, detectedLanguage } = const { sourceLanguage, targetLanguage, detectedLanguage } =
this.#translationState; this.#translationState;
const sourceLanguage = fromLanguage ? fromLanguage : detectedLanguage;
this.console?.debug( this.console?.debug(
`SelectTranslationsPanel (${sourceLanguage ? sourceLanguage : "??"}-${ `SelectTranslationsPanel (${sourceLanguage ?? detectedLanguage ?? "??"}-${
toLanguage ? toLanguage : "??" targetLanguage ? targetLanguage : "??"
}) state change (${previousPhase} => ${phase})` }) state change (${previousPhase} => ${phase})`
); );
@@ -1681,18 +1687,18 @@ var SelectTranslationsPanel = new (class {
* Transitions the phase to "translatable" if the proper conditions are met, * Transitions the phase to "translatable" if the proper conditions are met,
* otherwise retains the same phase as before. * otherwise retains the same phase as before.
* *
* @param {string} fromLanguage - The BCP-47 from-language tag. * @param {string} sourceLanguage - The BCP-47 from-language tag.
* @param {string} toLanguage - The BCP-47 to-language tag. * @param {string} targetLanguage - The BCP-47 to-language tag.
*/ */
#maybeChangeStateToTranslatable(fromLanguage, toLanguage) { #maybeChangeStateToTranslatable(sourceLanguage, targetLanguage) {
const { const previous = this.#translationState;
fromLanguage: previousFromLanguage,
toLanguage: previousToLanguage,
} = this.#translationState;
const langSelectionChanged = () => const langSelectionChanged = () =>
!TranslationsUtils.langTagsMatch(previousFromLanguage, fromLanguage) || !TranslationsUtils.langTagsMatch(
!TranslationsUtils.langTagsMatch(previousToLanguage, toLanguage); previous.sourceLanguage,
sourceLanguage
) ||
!TranslationsUtils.langTagsMatch(previous.targetLanguage, targetLanguage);
const shouldTranslateEvenIfLangSelectionHasNotChanged = () => { const shouldTranslateEvenIfLangSelectionHasNotChanged = () => {
const phase = this.phase(); const phase = this.phase();
@@ -1705,18 +1711,18 @@ var SelectTranslationsPanel = new (class {
}; };
if ( if (
// A valid from-language is actively selected. // A valid source language is actively selected.
fromLanguage && sourceLanguage &&
// A valid to-language is actively selected. // A valid target language is actively selected.
toLanguage && targetLanguage &&
// The language selection has changed, requiring a new translation. // The language selection has changed, requiring a new translation.
(langSelectionChanged() || (langSelectionChanged() ||
// We should try to translate even if the language selection has not changed. // We should try to translate even if the language selection has not changed.
shouldTranslateEvenIfLangSelectionHasNotChanged()) shouldTranslateEvenIfLangSelectionHasNotChanged())
) { ) {
this.#changeStateTo("translatable", /* retainEntries */ true, { this.#changeStateTo("translatable", /* retainEntries */ true, {
fromLanguage, sourceLanguage,
toLanguage, targetLanguage,
}); });
} }
} }
@@ -1836,19 +1842,19 @@ var SelectTranslationsPanel = new (class {
* Determines whether translation should continue based on panel state and language pair. * Determines whether translation should continue based on panel state and language pair.
* *
* @param {number} translationId - The id of the translation request to match. * @param {number} translationId - The id of the translation request to match.
* @param {string} fromLanguage - The from-language to analyze. * @param {string} sourceLanguage - The source language to analyze.
* @param {string} toLanguage - The to-language to analyze. * @param {string} targetLanguage - The target language to analyze.
* *
* @returns {boolean} True if translation should continue with the given pair, otherwise false. * @returns {boolean} True if translation should continue with the given pair, otherwise false.
*/ */
#shouldContinueTranslation(translationId, fromLanguage, toLanguage) { #shouldContinueTranslation(translationId, sourceLanguage, targetLanguage) {
return ( return (
// Continue only if the panel is still open. // Continue only if the panel is still open.
this.#isOpen() && this.#isOpen() &&
// Continue only if the current translationId matches. // Continue only if the current translationId matches.
translationId === this.#translationId && translationId === this.#translationId &&
// Continue only if the given language pair is still the actively selected pair. // Continue only if the given language pair is still the actively selected pair.
this.#isSelectedLangPair(fromLanguage, toLanguage) this.#isSelectedLangPair(sourceLanguage, targetLanguage)
); );
} }
@@ -1884,10 +1890,10 @@ var SelectTranslationsPanel = new (class {
#displayTranslatedText() { #displayTranslatedText() {
this.#showMainContent(); this.#showMainContent();
const { toLanguage } = this.#getSelectedLanguagePair(); const { targetLanguage } = this.#getSelectedLanguagePair();
const { textArea } = SelectTranslationsPanel.elements; const { textArea } = SelectTranslationsPanel.elements;
textArea.value = this.getTranslatedText(); textArea.value = this.getTranslatedText();
this.#updateTextDirection(toLanguage); this.#updateTextDirection(targetLanguage);
this.#updateConditionalUIEnabledState(); this.#updateConditionalUIEnabledState();
this.#indicateTranslatedTextArea({ overflow: "auto" }); this.#indicateTranslatedTextArea({ overflow: "auto" });
this.#maybeEnableTextAreaResizer(); this.#maybeEnableTextAreaResizer();
@@ -1920,7 +1926,7 @@ var SelectTranslationsPanel = new (class {
* Enables or disables UI components that are conditional on a valid language pair being selected. * Enables or disables UI components that are conditional on a valid language pair being selected.
*/ */
#updateConditionalUIEnabledState() { #updateConditionalUIEnabledState() {
const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); const { sourceLanguage, targetLanguage } = this.#getSelectedLanguagePair();
const { const {
copyButton, copyButton,
textArea, textArea,
@@ -1929,7 +1935,7 @@ var SelectTranslationsPanel = new (class {
tryAnotherSourceMenuList, tryAnotherSourceMenuList,
} = this.elements; } = this.elements;
const invalidLangPairSelected = !fromLanguage || !toLanguage; const invalidLangPairSelected = !sourceLanguage || !targetLanguage;
const isTranslating = this.phase() === "translating"; const isTranslating = this.phase() === "translating";
textArea.disabled = invalidLangPairSelected; textArea.disabled = invalidLangPairSelected;
@@ -1937,7 +1943,7 @@ var SelectTranslationsPanel = new (class {
translateButton.disabled = !tryAnotherSourceMenuList.value; translateButton.disabled = !tryAnotherSourceMenuList.value;
translateFullPageButton.disabled = translateFullPageButton.disabled =
invalidLangPairSelected || invalidLangPairSelected ||
TranslationsUtils.langTagsMatch(fromLanguage, toLanguage) || TranslationsUtils.langTagsMatch(sourceLanguage, targetLanguage) ||
this.#shouldHideTranslateFullPageButton(); this.#shouldHideTranslateFullPageButton();
} }
@@ -2077,10 +2083,11 @@ var SelectTranslationsPanel = new (class {
*/ */
#displayTranslationFailureMessage() { #displayTranslationFailureMessage() {
if (this.#mostRecentUIPhase !== "translation-failure") { if (this.#mostRecentUIPhase !== "translation-failure") {
const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); const { sourceLanguage, targetLanguage } =
this.#getSelectedLanguagePair();
TranslationsParent.telemetry() TranslationsParent.telemetry()
.selectTranslationsPanel() .selectTranslationsPanel()
.onTranslationFailureMessage({ fromLanguage, toLanguage }); .onTranslationFailureMessage({ sourceLanguage, targetLanguage });
} }
const { const {
@@ -2184,37 +2191,31 @@ var SelectTranslationsPanel = new (class {
/** /**
* Requests a translations port for a given language pair. * Requests a translations port for a given language pair.
* *
* @param {string} fromLanguage - The from-language. * @param {LanguagePair} languagePair
* @param {string} toLanguage - The to-language.
*
* @returns {Promise<MessagePort | undefined>} The message port promise. * @returns {Promise<MessagePort | undefined>} The message port promise.
*/ */
async #requestTranslationsPort(fromLanguage, toLanguage) { async #requestTranslationsPort(languagePair) {
const port = await TranslationsParent.requestTranslationsPort( return TranslationsParent.requestTranslationsPort(languagePair);
fromLanguage,
toLanguage
);
return port;
} }
/** /**
* Retrieves the existing translator for the specified language pair if it matches, * Retrieves the existing translator for the specified language pair if it matches,
* otherwise creates a new translator. * otherwise creates a new translator.
* *
* @param {string} fromLanguage - The source language code. * @param {LanguagePair} languagePair
* @param {string} toLanguage - The target language code.
* *
* @returns {Promise<Translator>} A promise that resolves to a `Translator` instance for the given language pair. * @returns {Promise<Translator>} A promise that resolves to a `Translator` instance for the given language pair.
*/ */
async #createTranslator(fromLanguage, toLanguage) { async #createTranslator(languagePair) {
this.console?.log( this.console?.log(
`Creating new Translator (${fromLanguage}-${toLanguage})` `Creating new Translator (${TranslationsUtils.serializeLanguagePair(languagePair)})`
); );
const translator = await Translator.create(fromLanguage, toLanguage, { const translator = await Translator.create(
allowSameLanguage: true, languagePair,
requestTranslationsPort: this.#requestTranslationsPort, this.#requestTranslationsPort,
}); true /* allowSameLanguage */
);
return translator; return translator;
} }
@@ -2227,8 +2228,9 @@ var SelectTranslationsPanel = new (class {
return; return;
} }
const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); const languagePair = this.#getSelectedLanguagePair();
this.#maybeChangeStateToTranslatable(fromLanguage, toLanguage); const { sourceLanguage, targetLanguage } = languagePair;
this.#maybeChangeStateToTranslatable(sourceLanguage, targetLanguage);
if (this.phase() !== "translatable") { if (this.phase() !== "translatable") {
return; return;
@@ -2238,15 +2240,15 @@ var SelectTranslationsPanel = new (class {
const sourceText = this.getSourceText(); const sourceText = this.getSourceText();
const translationId = ++this.#translationId; const translationId = ++this.#translationId;
TranslationsParent.storeMostRecentTargetLanguage(toLanguage); TranslationsParent.storeMostRecentTargetLanguage(targetLanguage);
this.#createTranslator(fromLanguage, toLanguage) this.#createTranslator(languagePair)
.then(translator => { .then(translator => {
if ( if (
this.#shouldContinueTranslation( this.#shouldContinueTranslation(
translationId, translationId,
fromLanguage, sourceLanguage,
toLanguage targetLanguage
) )
) { ) {
this.#changeStateToTranslating(); this.#changeStateToTranslating();
@@ -2259,8 +2261,8 @@ var SelectTranslationsPanel = new (class {
translatedText && translatedText &&
this.#shouldContinueTranslation( this.#shouldContinueTranslation(
translationId, translationId,
fromLanguage, sourceLanguage,
toLanguage targetLanguage
) )
) { ) {
this.#changeStateToTranslated(translatedText); this.#changeStateToTranslated(translatedText);
@@ -2274,20 +2276,20 @@ var SelectTranslationsPanel = new (class {
try { try {
if (!this.#sourceTextWordCount) { if (!this.#sourceTextWordCount) {
this.#sourceTextWordCount = TranslationsParent.countWords( this.#sourceTextWordCount = TranslationsParent.countWords(
fromLanguage, sourceLanguage,
sourceText sourceText
); );
} }
} catch (error) { } catch (error) {
// Failed to create an Intl.Segmenter for the fromLanguage. // Failed to create an Intl.Segmenter for the sourceLanguage.
// Continue on to report undefined to telemetry. // Continue on to report undefined to telemetry.
this.console?.warn(error); this.console?.warn(error);
} }
TranslationsParent.telemetry().onTranslate({ TranslationsParent.telemetry().onTranslate({
docLangTag, docLangTag,
fromLanguage, sourceLanguage,
toLanguage, targetLanguage,
topPreferredLanguage, topPreferredLanguage,
autoTranslate: false, autoTranslate: false,
requestTarget: "select", requestTarget: "select",
@@ -2297,41 +2299,38 @@ var SelectTranslationsPanel = new (class {
} }
/** /**
* Reports to telemetry whether the from-language or the to-language has * Reports to telemetry whether the source language or the target language has
* changed based on whether the currently selected language is different * changed based on whether the currently selected language is different
* than the corresponding language that is stored in the panel's state. * than the corresponding language that is stored in the panel's state.
*/ */
#maybeReportLanguageChangeToTelemetry() { #maybeReportLanguageChangeToTelemetry() {
const { const previous = this.#translationState;
fromLanguage: previousFromLanguage, const selected = this.#getSelectedLanguagePair();
toLanguage: previousToLanguage,
} = this.#translationState;
const {
fromLanguage: selectedFromLanguage,
toLanguage: selectedToLanguage,
} = this.#getSelectedLanguagePair();
if ( if (
!TranslationsUtils.langTagsMatch( !TranslationsUtils.langTagsMatch(
selectedFromLanguage, selected.sourceLanguage,
previousFromLanguage previous.sourceLanguage
) )
) { ) {
const { docLangTag } = this.#getLanguageInfo(); const { docLangTag } = this.#getLanguageInfo();
TranslationsParent.telemetry() TranslationsParent.telemetry()
.selectTranslationsPanel() .selectTranslationsPanel()
.onChangeFromLanguage({ .onChangeFromLanguage({
previousLangTag: previousFromLanguage, previousLangTag: previous.sourceLanguage,
currentLangTag: selectedFromLanguage, currentLangTag: selected.sourceLanguage,
docLangTag, docLangTag,
}); });
} }
if ( if (
!TranslationsUtils.langTagsMatch(selectedToLanguage, previousToLanguage) !TranslationsUtils.langTagsMatch(
selected.targetLanguage,
previous.targetLanguage
)
) { ) {
TranslationsParent.telemetry() TranslationsParent.telemetry()
.selectTranslationsPanel() .selectTranslationsPanel()
.onChangeToLanguage(selectedToLanguage); .onChangeToLanguage(selected.targetLanguage);
} }
} }

View File

@@ -7,15 +7,12 @@
* Tests what happens when the web languages differ from the app language. * Tests what happens when the web languages differ from the app language.
*/ */
add_task(async function test_weblanguage_differs_app_locale() { add_task(async function test_weblanguage_differs_app_locale() {
const cleanupLocales = await mockLocales({
systemLocales: ["en"],
appLocales: ["en"],
webLanguages: ["fr"],
});
const { cleanup } = await loadTestPage({ const { cleanup } = await loadTestPage({
page: ENGLISH_PAGE_URL, page: ENGLISH_PAGE_URL,
languagePairs: LANGUAGE_PAIRS, languagePairs: LANGUAGE_PAIRS,
systemLocales: ["en"],
appLocales: ["en"],
webLanguages: ["fr"],
}); });
await FullPageTranslationsTestUtils.assertTranslationsButton( await FullPageTranslationsTestUtils.assertTranslationsButton(
@@ -42,5 +39,4 @@ add_task(async function test_weblanguage_differs_app_locale() {
await closeAllOpenPanelsAndMenus(); await closeAllOpenPanelsAndMenus();
await cleanup(); await cleanup();
await cleanupLocales();
}); });

View File

@@ -110,8 +110,8 @@ export class TranslationsTelemetry {
* @param {object} data * @param {object} data
* @param {boolean} data.autoTranslate * @param {boolean} data.autoTranslate
* @param {string} data.docLangTag * @param {string} data.docLangTag
* @param {string} data.fromLanguage * @param {string} data.sourceLanguage
* @param {string} data.toLanguage * @param {string} data.targetLanguage
* @param {string} data.topPreferredLanguage * @param {string} data.topPreferredLanguage
* @param {string} data.requestTarget * @param {string} data.requestTarget
* @param {number} data.sourceTextCodeUnits * @param {number} data.sourceTextCodeUnits
@@ -121,9 +121,9 @@ export class TranslationsTelemetry {
const { const {
autoTranslate, autoTranslate,
docLangTag, docLangTag,
fromLanguage, sourceLanguage,
requestTarget, requestTarget,
toLanguage, targetLanguage,
topPreferredLanguage, topPreferredLanguage,
sourceTextCodeUnits, sourceTextCodeUnits,
sourceTextWordCount, sourceTextWordCount,
@@ -132,8 +132,8 @@ export class TranslationsTelemetry {
Glean.translations.requestCount[requestTarget ?? "full_page"].add(1); Glean.translations.requestCount[requestTarget ?? "full_page"].add(1);
Glean.translations.translationRequest.record({ Glean.translations.translationRequest.record({
flow_id: TranslationsTelemetry.getOrCreateFlowId(), flow_id: TranslationsTelemetry.getOrCreateFlowId(),
from_language: fromLanguage, from_language: sourceLanguage,
to_language: toLanguage, to_language: targetLanguage,
auto_translate: autoTranslate, auto_translate: autoTranslate,
document_language: docLangTag, document_language: docLangTag,
top_preferred_language: topPreferredLanguage, top_preferred_language: topPreferredLanguage,
@@ -415,8 +415,8 @@ class SelectTranslationsPanelTelemetry {
* @param {object} data * @param {object} data
* @param {string} data.docLangTag * @param {string} data.docLangTag
* @param {boolean} data.maintainFlow * @param {boolean} data.maintainFlow
* @param {string} data.fromLanguage * @param {string} data.sourceLanguage
* @param {string} data.toLanguage * @param {string} data.targetLanguage
* @param {string} data.topPreferredLanguage * @param {string} data.topPreferredLanguage
* @param {string} data.textSource * @param {string} data.textSource
*/ */
@@ -426,8 +426,8 @@ class SelectTranslationsPanelTelemetry {
? TranslationsTelemetry.getOrCreateFlowId() ? TranslationsTelemetry.getOrCreateFlowId()
: TranslationsTelemetry.createFlowId(), : TranslationsTelemetry.createFlowId(),
document_language: data.docLangTag, document_language: data.docLangTag,
from_language: data.fromLanguage, from_language: data.sourceLanguage,
to_language: data.toLanguage, to_language: data.targetLanguage,
top_preferred_language: data.topPreferredLanguage, top_preferred_language: data.topPreferredLanguage,
text_source: data.textSource, text_source: data.textSource,
}); });
@@ -473,19 +473,23 @@ class SelectTranslationsPanelTelemetry {
); );
} }
static onTranslateButton({ detectedLanguage, fromLanguage, toLanguage }) { static onTranslateButton({
detectedLanguage,
sourceLanguage,
targetLanguage,
}) {
Glean.translationsSelectTranslationsPanel.translateButton.record({ Glean.translationsSelectTranslationsPanel.translateButton.record({
flow_id: TranslationsTelemetry.getOrCreateFlowId(), flow_id: TranslationsTelemetry.getOrCreateFlowId(),
detected_language: detectedLanguage, detected_language: detectedLanguage,
from_language: fromLanguage, from_language: sourceLanguage,
to_language: toLanguage, to_language: targetLanguage,
}); });
TranslationsTelemetry.logEventToConsole( TranslationsTelemetry.logEventToConsole(
SelectTranslationsPanelTelemetry.onTranslateButton, SelectTranslationsPanelTelemetry.onTranslateButton,
{ {
detectedLanguage, detectedLanguage,
fromLanguage, sourceLanguage,
toLanguage, targetLanguage,
} }
); );
} }
@@ -574,14 +578,14 @@ class SelectTranslationsPanelTelemetry {
* Records a telemetry event when the translation-failure message is displayed. * Records a telemetry event when the translation-failure message is displayed.
* *
* @param {object} data * @param {object} data
* @param {string} data.fromLanguage * @param {string} data.sourceLanguage
* @param {string} data.toLanguage * @param {string} data.targetLanguage
*/ */
static onTranslationFailureMessage(data) { static onTranslationFailureMessage(data) {
Glean.translationsSelectTranslationsPanel.translationFailureMessage.record({ Glean.translationsSelectTranslationsPanel.translationFailureMessage.record({
flow_id: TranslationsTelemetry.getOrCreateFlowId(), flow_id: TranslationsTelemetry.getOrCreateFlowId(),
from_language: data.fromLanguage, from_language: data.sourceLanguage,
to_language: data.toLanguage, to_language: data.targetLanguage,
}); });
TranslationsTelemetry.logEventToConsole( TranslationsTelemetry.logEventToConsole(
SelectTranslationsPanelTelemetry.onTranslationFailureMessage, SelectTranslationsPanelTelemetry.onTranslationFailureMessage,

View File

@@ -2,6 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* @typedef {import("./translations").LanguagePair} LanguagePair
*/
/** /**
* A set of global static utility functions that are useful throughout the * A set of global static utility functions that are useful throughout the
* Translations ecosystem within the Firefox code base. * Translations ecosystem within the Firefox code base.
@@ -93,4 +97,32 @@ export class TranslationsUtils {
return false; return false;
} }
/**
* Serializes a language pair into a unique key that is human readable. This is useful
* for caching, deduplicating, and logging.
*
* e.g.
* "en -> fr"
* "en -> fr,base"
* "zh-Hans,tiny -> fr,base"
*
* @param {LanguagePair} languagePair
*/
static serializeLanguagePair({
sourceLanguage,
targetLanguage,
sourceVariant,
targetVariant,
}) {
let key = sourceLanguage;
if (sourceVariant) {
key += `,${sourceVariant}`;
}
key += ` -> ${targetLanguage}`;
if (targetVariant) {
key += `,${targetVariant}`;
}
return key;
}
} }

View File

@@ -19,6 +19,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
/** /**
* @typedef {import("./TranslationsChild.sys.mjs").TranslationsEngine} TranslationsEngine * @typedef {import("./TranslationsChild.sys.mjs").TranslationsEngine} TranslationsEngine
* @typedef {import("./TranslationsChild.sys.mjs").SupportedLanguages} SupportedLanguages * @typedef {import("./TranslationsChild.sys.mjs").SupportedLanguages} SupportedLanguages
* @typedef {import("../translations").LanguagePair} LanguagePair
*/ */
/** /**
@@ -54,13 +55,12 @@ export class AboutTranslationsChild extends JSWindowActorChild {
receiveMessage({ name, data }) { receiveMessage({ name, data }) {
switch (name) { switch (name) {
case "AboutTranslations:SendTranslationsPort": { case "AboutTranslations:SendTranslationsPort": {
const { fromLanguage, toLanguage, port } = data; const { languagePair, port } = data;
const transferables = [port]; const transferables = [port];
this.contentWindow.postMessage( this.contentWindow.postMessage(
{ {
type: "GetTranslationsPort", type: "GetTranslationsPort",
fromLanguage, languagePair,
toLanguage,
port, port,
}, },
"*", "*",
@@ -214,14 +214,12 @@ export class AboutTranslationsChild extends JSWindowActorChild {
* created for that pair. The lifecycle of the engine is managed by the * created for that pair. The lifecycle of the engine is managed by the
* TranslationsEngine. * TranslationsEngine.
* *
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
* @returns {void} * @returns {void}
*/ */
AT_createTranslationsPort(fromLanguage, toLanguage) { AT_createTranslationsPort(languagePair) {
this.sendAsyncMessage("AboutTranslations:GetTranslationsPort", { this.sendAsyncMessage("AboutTranslations:GetTranslationsPort", {
fromLanguage, languagePair,
toLanguage,
}); });
} }

View File

@@ -57,12 +57,10 @@ export class AboutTranslationsParent extends JSWindowActorParent {
return undefined; return undefined;
} }
const { fromLanguage, toLanguage } = data; const { languagePair } = data;
try { try {
const port = await lazy.TranslationsParent.requestTranslationsPort( const port =
fromLanguage, await lazy.TranslationsParent.requestTranslationsPort(languagePair);
toLanguage
);
// At the time of writing, you can't return a port via the `sendQuery` API, // At the time of writing, you can't return a port via the `sendQuery` API,
// so results can't just be returned. The `sendAsyncMessage` method must be // so results can't just be returned. The `sendAsyncMessage` method must be
@@ -71,8 +69,7 @@ export class AboutTranslationsParent extends JSWindowActorParent {
this.sendAsyncMessage( this.sendAsyncMessage(
"AboutTranslations:SendTranslationsPort", "AboutTranslations:SendTranslationsPort",
{ {
fromLanguage, languagePair,
toLanguage,
port, port,
}, },
[port] // Mark the port as transferable. [port] // Mark the port as transferable.

View File

@@ -60,24 +60,20 @@ export class TranslationsChild extends JSWindowActorChild {
return undefined; return undefined;
} }
const { fromLanguage, toLanguage, port, translationsStart } = data; const { languagePair, port, translationsStart } = data;
if ( if (
!TranslationsChild.#translationsCache || !TranslationsChild.#translationsCache ||
!TranslationsChild.#translationsCache.matches( !TranslationsChild.#translationsCache.matches(languagePair)
fromLanguage,
toLanguage
)
) { ) {
TranslationsChild.#translationsCache = new lazy.LRUCache( TranslationsChild.#translationsCache = new lazy.LRUCache(
fromLanguage, languagePair
toLanguage
); );
} }
this.#translatedDoc = new lazy.TranslationsDocument( this.#translatedDoc = new lazy.TranslationsDocument(
this.document, this.document,
fromLanguage, languagePair.sourceLanguage,
toLanguage, languagePair.targetLanguage,
this.contentWindow.windowGlobalChild.innerWindowId, this.contentWindow.windowGlobalChild.innerWindowId,
port, port,
() => this.sendAsyncMessage("Translations:RequestPort"), () => this.sendAsyncMessage("Translations:RequestPort"),

View File

@@ -40,12 +40,11 @@ export class TranslationsEngineChild extends JSWindowActorChild {
async receiveMessage({ name, data }) { async receiveMessage({ name, data }) {
switch (name) { switch (name) {
case "TranslationsEngine:StartTranslation": { case "TranslationsEngine:StartTranslation": {
const { fromLanguage, toLanguage, innerWindowId, port } = data; const { languagePair, innerWindowId, port } = data;
const transferables = [port]; const transferables = [port];
const message = { const message = {
type: "StartTranslation", type: "StartTranslation",
fromLanguage, languagePair,
toLanguage,
innerWindowId, innerWindowId,
port, port,
}; };
@@ -177,14 +176,12 @@ export class TranslationsEngineChild extends JSWindowActorChild {
} }
/** /**
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
*/ */
TE_requestEnginePayload(fromLanguage, toLanguage) { TE_requestEnginePayload(languagePair) {
return this.#convertToContentPromise( return this.#convertToContentPromise(
this.sendQuery("TranslationsEngine:RequestEnginePayload", { this.sendQuery("TranslationsEngine:RequestEnginePayload", {
fromLanguage, languagePair,
toLanguage,
}) })
); );
} }

View File

@@ -8,6 +8,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
EngineProcess: "chrome://global/content/ml/EngineProcess.sys.mjs", EngineProcess: "chrome://global/content/ml/EngineProcess.sys.mjs",
}); });
/**
* @typedef {import("../translations").LanguagePair} LanguagePair
*/
/** /**
* The translations engine is in its own content process. This actor handles the * The translations engine is in its own content process. This actor handles the
* marshalling of the data such as the engine payload and port passing. * marshalling of the data such as the engine payload and port passing.
@@ -31,12 +35,9 @@ export class TranslationsEngineParent extends JSWindowActorParent {
lazy.EngineProcess.resolveTranslationsEngineParent(this); lazy.EngineProcess.resolveTranslationsEngineParent(this);
return undefined; return undefined;
case "TranslationsEngine:RequestEnginePayload": { case "TranslationsEngine:RequestEnginePayload": {
const { fromLanguage, toLanguage } = data; const { languagePair } = data;
const payloadPromise = const payloadPromise =
lazy.TranslationsParent.getTranslationsEnginePayload( lazy.TranslationsParent.getTranslationsEnginePayload(languagePair);
fromLanguage,
toLanguage
);
payloadPromise.catch(error => { payloadPromise.catch(error => {
lazy.TranslationsParent.telemetry().onError(String(error)); lazy.TranslationsParent.telemetry().onError(String(error));
}); });
@@ -73,12 +74,11 @@ export class TranslationsEngineParent extends JSWindowActorParent {
} }
/** /**
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
* @param {MessagePort} port * @param {MessagePort} port
* @param {TranslationsParent} [translationsParent] * @param {TranslationsParent} [translationsParent]
*/ */
startTranslation(fromLanguage, toLanguage, port, translationsParent) { startTranslation(languagePair, port, translationsParent) {
const innerWindowId = translationsParent?.innerWindowId; const innerWindowId = translationsParent?.innerWindowId;
if (translationsParent) { if (translationsParent) {
this.#translationsParents.set(innerWindowId, translationsParent); this.#translationsParents.set(innerWindowId, translationsParent);
@@ -90,8 +90,7 @@ export class TranslationsEngineParent extends JSWindowActorParent {
this.sendAsyncMessage( this.sendAsyncMessage(
"TranslationsEngine:StartTranslation", "TranslationsEngine:StartTranslation",
{ {
fromLanguage, languagePair,
toLanguage,
innerWindowId, innerWindowId,
port, port,
}, },

View File

@@ -210,18 +210,11 @@ const VERIFY_SIGNATURES_FROM_FS = false;
* @typedef {import("../translations").WasmRecord} WasmRecord * @typedef {import("../translations").WasmRecord} WasmRecord
* @typedef {import("../translations").LangTags} LangTags * @typedef {import("../translations").LangTags} LangTags
* @typedef {import("../translations").LanguagePair} LanguagePair * @typedef {import("../translations").LanguagePair} LanguagePair
* @typedef {import("../translations").ModelLanguages} ModelLanguages
* @typedef {import("../translations").SupportedLanguages} SupportedLanguages * @typedef {import("../translations").SupportedLanguages} SupportedLanguages
* @typedef {import("../translations").TranslationErrors} TranslationErrors * @typedef {import("../translations").TranslationErrors} TranslationErrors
*/ */
/**
* @typedef {object} TranslationPair
* @property {string} fromLanguage
* @property {string} toLanguage
* @property {string} [fromDisplayLanguage]
* @property {string} [toDisplayLanguage]
*/
/** /**
* The state that is stored per a "top" ChromeWindow. This "top" ChromeWindow is the JS * The state that is stored per a "top" ChromeWindow. This "top" ChromeWindow is the JS
* global associated with a browser window. Some state is unique to a browser window, and * global associated with a browser window. Some state is unique to a browser window, and
@@ -242,7 +235,7 @@ class StatePerTopChromeWindow {
/** /**
* When reloading the page, store the language pair that needs translating. * When reloading the page, store the language pair that needs translating.
* *
* @type {null | TranslationPair} * @type {null | LanguagePair}
*/ */
translateOnPageReload = null; translateOnPageReload = null;
@@ -476,16 +469,15 @@ export class TranslationsParent extends JSWindowActorParent {
if (windowState.translateOnPageReload) { if (windowState.translateOnPageReload) {
// The actor was recreated after a page reload, start the translation. // The actor was recreated after a page reload, start the translation.
const { fromLanguage, toLanguage } = windowState.translateOnPageReload; const languagePair = windowState.translateOnPageReload;
windowState.translateOnPageReload = null; windowState.translateOnPageReload = null;
lazy.console.log( lazy.console.log(
`Translating on a page reload from "${fromLanguage}" to "${toLanguage}".` `Translating on a page reload from "${lazy.TranslationsUtils.serializeLanguagePair(languagePair)}".`
); );
this.translate( this.translate(
fromLanguage, languagePair,
toLanguage,
false // reportAsAutoTranslate false // reportAsAutoTranslate
); );
} }
@@ -1214,8 +1206,7 @@ export class TranslationsParent extends JSWindowActorParent {
/** /**
* Requests a new translations port. * Requests a new translations port.
* *
* @param {string} fromLanguage - The BCP-47 from-language tag. * @param {LanguagePair} languagePair
* @param {string} toLanguage - The BCP-47 to-language tag.
* @param {TranslationsParent} [translationsParent] - A TranslationsParent actor instance. * @param {TranslationsParent} [translationsParent] - A TranslationsParent actor instance.
* NOTE: This value should be provided only if your port is associated with Full Page Translations. * NOTE: This value should be provided only if your port is associated with Full Page Translations.
* This will associate this translations port with the TranslationsParent actor instance, which will mean that changes * This will associate this translations port with the TranslationsParent actor instance, which will mean that changes
@@ -1223,11 +1214,7 @@ export class TranslationsParent extends JSWindowActorParent {
* *
* @returns {Promise<MessagePort | undefined>} The port for communication with the translation engine, or undefined on failure. * @returns {Promise<MessagePort | undefined>} The port for communication with the translation engine, or undefined on failure.
*/ */
static async requestTranslationsPort( static async requestTranslationsPort(languagePair, translationsParent) {
fromLanguage,
toLanguage,
translationsParent
) {
let translationsEngineParent; let translationsEngineParent;
try { try {
translationsEngineParent = translationsEngineParent =
@@ -1241,8 +1228,7 @@ export class TranslationsParent extends JSWindowActorParent {
// process and the engine's process. // process and the engine's process.
const { port1, port2 } = new MessageChannel(); const { port1, port2 } = new MessageChannel();
translationsEngineParent.startTranslation( translationsEngineParent.startTranslation(
fromLanguage, languagePair,
toLanguage,
port1, port1,
translationsParent translationsParent
); );
@@ -1275,8 +1261,10 @@ export class TranslationsParent extends JSWindowActorParent {
if (this.shouldAutoTranslate(detectedLanguages)) { if (this.shouldAutoTranslate(detectedLanguages)) {
this.translate( this.translate(
detectedLanguages.docLangTag, {
detectedLanguages.userLangTag, sourceLanguage: detectedLanguages.docLangTag,
targetLanguage: detectedLanguages.userLangTag,
},
true // reportAsAutoTranslate true // reportAsAutoTranslate
); );
} else { } else {
@@ -1304,16 +1292,14 @@ export class TranslationsParent extends JSWindowActorParent {
); );
} }
const { fromLanguage, toLanguage } = requestedLanguagePair;
const port = await TranslationsParent.requestTranslationsPort( const port = await TranslationsParent.requestTranslationsPort(
fromLanguage, requestedLanguagePair,
toLanguage,
this this
); );
if (!port) { if (!port) {
lazy.console.error( lazy.console.error(
`Failed to create a translations port for language pair: (${fromLanguage} -> ${toLanguage})` `Failed to create a translations port for language pair: ${lazy.TranslationsUtils.serializeLanguagePair(requestedLanguagePair)}`
); );
return undefined; return undefined;
} }
@@ -1334,10 +1320,9 @@ export class TranslationsParent extends JSWindowActorParent {
} }
/** /**
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
*/ */
static async getTranslationsEnginePayload(fromLanguage, toLanguage) { static async getTranslationsEnginePayload(languagePair) {
const wasmStartTime = Cu.now(); const wasmStartTime = Cu.now();
const bergamotWasmArrayBufferPromise = const bergamotWasmArrayBufferPromise =
TranslationsParent.#getBergamotWasmArrayBuffer(); TranslationsParent.#getBergamotWasmArrayBuffer();
@@ -1355,34 +1340,43 @@ export class TranslationsParent extends JSWindowActorParent {
const modelStartTime = Cu.now(); const modelStartTime = Cu.now();
let translationModelPayloads; /** @type {TranslationModelPayload[]} */
const nonPivotPayload = await TranslationsParent.getTranslationModelPayload( const translationModelPayloads = [];
fromLanguage, const { sourceLanguage, targetLanguage, sourceVariant, targetVariant } =
toLanguage languagePair;
if (sourceLanguage === PIVOT_LANGUAGE) {
translationModelPayloads.push(
await TranslationsParent.getTranslationModelPayload(
sourceLanguage,
targetLanguage,
targetVariant
)
);
} else if (targetLanguage === PIVOT_LANGUAGE) {
translationModelPayloads.push(
await TranslationsParent.getTranslationModelPayload(
sourceLanguage,
targetLanguage,
sourceVariant
)
); );
if (nonPivotPayload) {
// A direct translation model was found for the language pair.
translationModelPayloads = [nonPivotPayload];
} else { } else {
// No matching model was found, try to pivot between English. // No matching model was found, try to pivot between English.
const [payload1, payload2] = await Promise.all([ translationModelPayloads.push(
...(await Promise.all([
TranslationsParent.getTranslationModelPayload( TranslationsParent.getTranslationModelPayload(
fromLanguage, sourceLanguage,
PIVOT_LANGUAGE PIVOT_LANGUAGE,
sourceVariant
), ),
TranslationsParent.getTranslationModelPayload( TranslationsParent.getTranslationModelPayload(
PIVOT_LANGUAGE, PIVOT_LANGUAGE,
toLanguage targetLanguage,
targetVariant
), ),
]); ]))
if (!payload1 || !payload2) {
throw new Error(
`No language models were found for ${fromLanguage} to ${toLanguage}`
); );
} }
translationModelPayloads = [payload1, payload2];
}
ChromeUtils.addProfilerMarker( ChromeUtils.addProfilerMarker(
"TranslationsParent", "TranslationsParent",
@@ -1420,14 +1414,17 @@ export class TranslationsParent extends JSWindowActorParent {
} }
/** /**
* Creates a lookup key that is unique to each fromLanguage-toLanguage pair. * Creates a lookup key that is unique to each sourceLanguage-targetLanguage pair.
* *
* @param {string} fromLanguage * @param {string} sourceLanguage
* @param {string} toLanguage * @param {string} targetLanguage
* @param {string} [variant]
* @returns {string} * @returns {string}
*/ */
static languagePairKey(fromLanguage, toLanguage) { static nonPivotKey(sourceLanguage, targetLanguage, variant) {
return `${fromLanguage},${toLanguage}`; return variant
? `${sourceLanguage},${targetLanguage},${variant}`
: `${sourceLanguage},${targetLanguage}`;
} }
/** /**
@@ -1457,18 +1454,30 @@ export class TranslationsParent extends JSWindowActorParent {
/** /**
* Get the list of language pairs supported by the translations engine. * Get the list of language pairs supported by the translations engine.
* *
* @returns {Promise<Array<LanguagePair>>} * @returns {Promise<Array<NonPivotLanguagePair>>}
*/ */
static getLanguagePairs() { static getNonPivotLanguagePairs() {
if (!TranslationsParent.#languagePairs) { if (!TranslationsParent.#languagePairs) {
TranslationsParent.#languagePairs = TranslationsParent.#languagePairs =
TranslationsParent.#getTranslationModelRecords().then(records => { TranslationsParent.#getTranslationModelRecords().then(records => {
const languagePairMap = new Map(); const languagePairMap = new Map();
for (const { fromLang, toLang } of records.values()) { for (const {
const key = TranslationsParent.languagePairKey(fromLang, toLang); fromLang: sourceLanguage,
toLang: targetLanguage,
variant,
} of records.values()) {
const key = TranslationsParent.nonPivotKey(
sourceLanguage,
targetLanguage,
variant
);
if (!languagePairMap.has(key)) { if (!languagePairMap.has(key)) {
languagePairMap.set(key, { fromLang, toLang }); languagePairMap.set(key, {
sourceLanguage,
targetLanguage,
variant,
});
} }
} }
return Array.from(languagePairMap.values()); return Array.from(languagePairMap.values());
@@ -1482,8 +1491,8 @@ export class TranslationsParent extends JSWindowActorParent {
/** /**
* Get the list of languages and their display names, sorted by their display names. * Get the list of languages and their display names, sorted by their display names.
* This is more expensive of a call than getLanguagePairs since the display names * This is more expensive of a call than getNonPivotLanguagePairs since the display
* are looked up. * names are looked up.
* *
* This is all of the information needed to render dropdowns for translation * This is all of the information needed to render dropdowns for translation
* language selection. * language selection.
@@ -1492,16 +1501,25 @@ export class TranslationsParent extends JSWindowActorParent {
*/ */
static async getSupportedLanguages() { static async getSupportedLanguages() {
await chaosMode(1 / 4); await chaosMode(1 / 4);
const languagePairs = await TranslationsParent.getLanguagePairs(); const languagePairs = await TranslationsParent.getNonPivotLanguagePairs();
/** @type {Set<string>} */ /** @type {Set<string>} */
const fromLanguages = new Set(); const sourceLanguageKeys = new Set();
/** @type {Set<string>} */ /** @type {Set<string>} */
const toLanguages = new Set(); const targetLanguageKeys = new Set();
for (const { fromLang, toLang } of languagePairs) { for (const { sourceLanguage, targetLanguage, variant } of languagePairs) {
fromLanguages.add(fromLang); if (sourceLanguage === PIVOT_LANGUAGE) {
toLanguages.add(toLang); // Ignore variants for the pivot language, as every variant targets English.
sourceLanguageKeys.add(PIVOT_LANGUAGE);
} else {
sourceLanguageKeys.add(
variant ? `${sourceLanguage},${variant}` : sourceLanguage
);
}
targetLanguageKeys.add(
variant ? `${targetLanguage},${variant}` : targetLanguage
);
} }
// Build a map of the langTag to the display name. // Build a map of the langTag to the display name.
@@ -1511,8 +1529,9 @@ export class TranslationsParent extends JSWindowActorParent {
const languageDisplayNames = const languageDisplayNames =
TranslationsParent.createLanguageDisplayNames(); TranslationsParent.createLanguageDisplayNames();
for (const langTagSet of [fromLanguages, toLanguages]) { for (const langTagSet of [sourceLanguageKeys, targetLanguageKeys]) {
for (const langTag of langTagSet.keys()) { for (const langTagKey of langTagSet) {
const [langTag] = langTagKey.split(",");
if (displayNames.has(langTag)) { if (displayNames.has(langTag)) {
continue; continue;
} }
@@ -1521,19 +1540,31 @@ export class TranslationsParent extends JSWindowActorParent {
} }
} }
const addDisplayName = langTag => ({ const addDisplayName = langTagKey => {
langTag, const [langTag, variant] = langTagKey.split(",");
displayName: displayNames.get(langTag), let displayName = displayNames.get(langTag);
}); if (variant) {
// Right now if there is a variant always append the variant name, but in the
// future it might be a good idea to not show the variant name if there is only
// 1 variant for a language. For now this is only developer facing. This is also
// why Fluent isn't used here, as it's not exposed to end users.
//
// The display needs to work with languages that use script tags,
// e.g. "Chinese (Traditional) - base".
// "Spanish - decoder-bigger-embeddings".
displayName = `${displayName} - ${variant}`;
}
return { langTag, variant, langTagKey, displayName };
};
const sort = (a, b) => a.displayName.localeCompare(b.displayName); const sort = (a, b) => a.displayName.localeCompare(b.displayName);
return { return {
languagePairs, languagePairs,
fromLanguages: Array.from(fromLanguages.keys()) sourceLanguages: Array.from(sourceLanguageKeys.keys())
.map(addDisplayName) .map(addDisplayName)
.sort(sort), .sort(sort),
toLanguages: Array.from(toLanguages.keys()) targetLanguages: Array.from(targetLanguageKeys.keys())
.map(addDisplayName) .map(addDisplayName)
.sort(sort), .sort(sort),
}; };
@@ -1548,8 +1579,8 @@ export class TranslationsParent extends JSWindowActorParent {
static getLanguageList(supportedLanguages) { static getLanguageList(supportedLanguages) {
const displayNames = new Map(); const displayNames = new Map();
for (const languages of [ for (const languages of [
supportedLanguages.fromLanguages, supportedLanguages.sourceLanguages,
supportedLanguages.toLanguages, supportedLanguages.targetLanguages,
]) { ]) {
for (const { langTag, displayName } of languages) { for (const { langTag, displayName } of languages) {
displayNames.set(langTag, displayName); displayNames.set(langTag, displayName);
@@ -1985,9 +2016,10 @@ export class TranslationsParent extends JSWindowActorParent {
// Names in this collection are not unique, so we are appending the languagePairKey // Names in this collection are not unique, so we are appending the languagePairKey
// to guarantee uniqueness. // to guarantee uniqueness.
lookupKey: record => lookupKey: record =>
`${record.name}${TranslationsParent.languagePairKey( `${record.name}${TranslationsParent.nonPivotKey(
record.fromLang, record.fromLang,
record.toLang record.toLang,
record.variant
)}`, )}`,
}); });
@@ -2363,19 +2395,19 @@ export class TranslationsParent extends JSWindowActorParent {
* This will delete a downloaded model set when it is incomplete, for example en->es (downloaded) and es->en * This will delete a downloaded model set when it is incomplete, for example en->es (downloaded) and es->en
* (not-downloaded) will delete en->es to clear the lingering one-sided package. * (not-downloaded) will delete en->es to clear the lingering one-sided package.
* *
* @returns {Set<string>} Directional language pairs in the form of "fromLang,toLang" that indicates language pairs that were deleted. * @returns {Set<string>} Directional language pairs in the form of "sourceLanguage,targetLanguage" that indicates language pairs that were deleted.
*/ */
static async deleteCachedLanguageFiles() { static async deleteCachedLanguageFiles() {
const languagePairs = await TranslationsParent.getLanguagePairs(); const languagePairs = await TranslationsParent.getNonPivotLanguagePairs();
const deletionRequest = []; const deletionRequest = [];
let deletedPairs = new Set(); let deletedPairs = new Set();
for (const { fromLang, toLang } of languagePairs) { for (const { sourceLanguage, targetLanguage } of languagePairs) {
const { downloadedPairs, nonDownloadedPairs } = const { downloadedPairs, nonDownloadedPairs } =
await TranslationsParent.getDownloadedFileStatusToAndFromPair( await TranslationsParent.getDownloadedFileStatusToAndFromPair(
fromLang, sourceLanguage,
toLang targetLanguage
); );
if (downloadedPairs.size && nonDownloadedPairs.size) { if (downloadedPairs.size && nonDownloadedPairs.size) {
@@ -2384,8 +2416,8 @@ export class TranslationsParent extends JSWindowActorParent {
downloadedPairs.forEach(langPair => deletedPairs.add(langPair)); downloadedPairs.forEach(langPair => deletedPairs.add(langPair));
deletionRequest.push( deletionRequest.push(
TranslationsParent.deleteLanguageFilesToAndFromPair( TranslationsParent.deleteLanguageFilesToAndFromPair(
fromLang, sourceLanguage,
toLang, targetLanguage,
/* deletePivots */ false /* deletePivots */ false
) )
); );
@@ -2405,9 +2437,9 @@ export class TranslationsParent extends JSWindowActorParent {
* *
* @returns {object} status The status between the pairs. * @returns {object} status The status between the pairs.
* @returns {Set<string>} status.downloadedPairs A set of strings that has directionality about what side * @returns {Set<string>} status.downloadedPairs A set of strings that has directionality about what side
* is downloaded, in the format "fromLang,toLang". * is downloaded, in the format "sourceLanguage,targetLanguage".
* @returns {Set<string>} status.nonDownloadedPairs A set of strings that has directionality about what side * @returns {Set<string>} status.nonDownloadedPairs A set of strings that has directionality about what side
* is not downloaded, in the format "fromLang,toLang". It is possible to have files both in nonDownloadedFiles * is not downloaded, in the format "sourceLanguage,targetLanguage". It is possible to have files both in nonDownloadedFiles
* and downloadedFiles in the case of incomplete downloads. * and downloadedFiles in the case of incomplete downloads.
*/ */
@@ -2430,11 +2462,19 @@ export class TranslationsParent extends JSWindowActorParent {
if (isDownloaded) { if (isDownloaded) {
downloadedPairs.add( downloadedPairs.add(
TranslationsParent.languagePairKey(record.fromLang, record.toLang) TranslationsParent.nonPivotKey(
record.fromLang,
record.toLang,
record.variant
)
); );
} else { } else {
nonDownloadedPairs.add( nonDownloadedPairs.add(
TranslationsParent.languagePairKey(record.fromLang, record.toLang) TranslationsParent.nonPivotKey(
record.fromLang,
record.toLang,
record.variant
)
); );
} }
} }
@@ -2492,12 +2532,15 @@ export class TranslationsParent extends JSWindowActorParent {
return matchedRecords; return matchedRecords;
} }
const addLanguagePair = (fromLang, toLang) => { const addLanguagePair = (sourceLanguage, targetLanguage) => {
let matchFound = false; let matchFound = false;
for (const record of records.values()) { for (const record of records.values()) {
if ( if (
lazy.TranslationsUtils.langTagsMatch(record.fromLang, fromLang) && lazy.TranslationsUtils.langTagsMatch(
lazy.TranslationsUtils.langTagsMatch(record.toLang, toLang) record.fromLang,
sourceLanguage
) &&
lazy.TranslationsUtils.langTagsMatch(record.toLang, targetLanguage)
) { ) {
matchedRecords.add(record); matchedRecords.add(record);
matchFound = true; matchFound = true;
@@ -2566,15 +2609,24 @@ export class TranslationsParent extends JSWindowActorParent {
* *
* Results are only returned if the model is found. * Results are only returned if the model is found.
* *
* @param {string} fromLanguage * @param {string} sourceLanguage
* @param {string} toLanguage * @param {string} targetLanguage
* @returns {null | TranslationModelPayload} * @param {string} [variant]
* @returns {TranslationModelPayload}
*/ */
static async getTranslationModelPayload(fromLanguage, toLanguage) { static async getTranslationModelPayload(
sourceLanguage,
targetLanguage,
variant
) {
if (!sourceLanguage || !targetLanguage) {
console.error({ sourceLanguage, targetLanguage });
throw new Error("A source or target language was not provided.");
}
const client = TranslationsParent.#getTranslationModelsRemoteClient(); const client = TranslationsParent.#getTranslationModelsRemoteClient();
lazy.console.log( lazy.console.log(
`Beginning model downloads: "${fromLanguage}" to "${toLanguage}"` `Beginning model downloads: "${sourceLanguage}" to "${targetLanguage}"`
); );
const records = [ const records = [
@@ -2582,7 +2634,7 @@ export class TranslationsParent extends JSWindowActorParent {
]; ];
/** @type {LanguageTranslationModelFiles} */ /** @type {LanguageTranslationModelFiles} */
let results; const results = {};
// Use Promise.all to download (or retrieve from cache) the model files in parallel. // Use Promise.all to download (or retrieve from cache) the model files in parallel.
await Promise.all( await Promise.all(
@@ -2595,18 +2647,18 @@ export class TranslationsParent extends JSWindowActorParent {
if ( if (
!lazy.TranslationsUtils.langTagsMatch( !lazy.TranslationsUtils.langTagsMatch(
record.fromLang, record.fromLang,
fromLanguage sourceLanguage
) || ) ||
!lazy.TranslationsUtils.langTagsMatch(record.toLang, toLanguage) !lazy.TranslationsUtils.langTagsMatch(
record.toLang,
targetLanguage
) ||
record.variant !== variant
) { ) {
// Only use models that match. // Only use models that match.
return; return;
} }
if (!results) {
results = {};
}
const start = Date.now(); const start = Date.now();
// Download or retrieve from the local cache: // Download or retrieve from the local cache:
@@ -2626,53 +2678,50 @@ export class TranslationsParent extends JSWindowActorParent {
`Translation model fetched in ${duration / 1000} seconds:`, `Translation model fetched in ${duration / 1000} seconds:`,
record.fromLang, record.fromLang,
record.toLang, record.toLang,
record.variant,
record.fileType, record.fileType,
record.version record.version
); );
}) })
); );
if (!results) {
// No model files were found, pivoting will be required.
return null;
}
// Validate that all of the files we expected were actually available and // Validate that all of the files we expected were actually available and
// downloaded. // downloaded.
if (!results.model) { if (!results.model) {
throw new Error( throw new Error(
`No model file was found for "${fromLanguage}" to "${toLanguage}."` `No model file was found for "${sourceLanguage}" to "${targetLanguage}."`
); );
} }
if (!results.lex && lazy.useLexicalShortlist) { if (!results.lex && lazy.useLexicalShortlist) {
throw new Error( throw new Error(
`No lex file was found for "${fromLanguage}" to "${toLanguage}."` `No lex file was found for "${sourceLanguage}" to "${targetLanguage}."`
); );
} }
if (results.vocab) { if (results.vocab) {
if (results.srcvocab) { if (results.srcvocab) {
throw new Error( throw new Error(
`A srcvocab and vocab file were both included for "${fromLanguage}" to "${toLanguage}." Only one is needed.` `A srcvocab and vocab file were both included for "${sourceLanguage}" to "${targetLanguage}." Only one is needed.`
); );
} }
if (results.trgvocab) { if (results.trgvocab) {
throw new Error( throw new Error(
`A trgvocab and vocab file were both included for "${fromLanguage}" to "${toLanguage}." Only one is needed.` `A trgvocab and vocab file were both included for "${sourceLanguage}" to "${targetLanguage}." Only one is needed.`
); );
} }
} else if (!results.srcvocab || !results.trgvocab) { } else if (!results.srcvocab || !results.trgvocab) {
throw new Error( throw new Error(
`No vocab files were provided for "${fromLanguage}" to "${toLanguage}."` `No vocab files were provided for "${sourceLanguage}" to "${targetLanguage}."`
); );
} }
/** @type {TranslationModelPayload} */ /** @type {TranslationModelPayload} */
return { return {
sourceLanguage: fromLanguage, sourceLanguage,
targetLanguage: toLanguage, targetLanguage,
variant,
languageModelFiles: results, languageModelFiles: results,
}; };
} }
@@ -2700,26 +2749,29 @@ export class TranslationsParent extends JSWindowActorParent {
/** /**
* Gets the expected download size that will occur (if any) if translate is called on two given languages for display purposes. * Gets the expected download size that will occur (if any) if translate is called on two given languages for display purposes.
* *
* @param {string} fromLanguage * @param {string} sourceLanguage
* @param {string} toLanguage * @param {string} targetLanguage
* @returns {Promise<long>} Size in bytes of the expected download. A result of 0 indicates no download is expected for the request. * @returns {Promise<long>} Size in bytes of the expected download. A result of 0 indicates no download is expected for the request.
*/ */
static async getExpectedTranslationDownloadSize(fromLanguage, toLanguage) { static async getExpectedTranslationDownloadSize(
sourceLanguage,
targetLanguage
) {
const directSize = await this.#getModelDownloadSize( const directSize = await this.#getModelDownloadSize(
fromLanguage, sourceLanguage,
toLanguage targetLanguage
); );
// If a direct model is not found, then check pivots. // If a direct model is not found, then check pivots.
if (directSize.downloadSize == 0 && !directSize.modelFound) { if (directSize.downloadSize == 0 && !directSize.modelFound) {
const indirectFrom = await TranslationsParent.#getModelDownloadSize( const indirectFrom = await TranslationsParent.#getModelDownloadSize(
fromLanguage, sourceLanguage,
PIVOT_LANGUAGE PIVOT_LANGUAGE
); );
const indirectTo = await TranslationsParent.#getModelDownloadSize( const indirectTo = await TranslationsParent.#getModelDownloadSize(
PIVOT_LANGUAGE, PIVOT_LANGUAGE,
toLanguage targetLanguage
); );
// Note, will also return 0 due to the models not being available as well. // Note, will also return 0 due to the models not being available as well.
@@ -2733,14 +2785,14 @@ export class TranslationsParent extends JSWindowActorParent {
/** /**
* Determines the language model download size for a specified translation for display purposes. * Determines the language model download size for a specified translation for display purposes.
* *
* @param {string} fromLanguage * @param {string} sourceLanguage
* @param {string} toLanguage * @param {string} targetLanguage
* @returns {Promise<{downloadSize: long, modelFound: boolean}>} Download size is the * @returns {Promise<{downloadSize: long, modelFound: boolean}>} Download size is the
* size in bytes of the estimated download for display purposes. Model found indicates * size in bytes of the estimated download for display purposes. Model found indicates
* a model was found. e.g., a result of {size: 0, modelFound: false} indicates no * a model was found. e.g., a result of {size: 0, modelFound: false} indicates no
* bytes to download, because a model wasn't located. * bytes to download, because a model wasn't located.
*/ */
static async #getModelDownloadSize(fromLanguage, toLanguage) { static async #getModelDownloadSize(sourceLanguage, targetLanguage) {
const client = TranslationsParent.#getTranslationModelsRemoteClient(); const client = TranslationsParent.#getTranslationModelsRemoteClient();
const records = [ const records = [
...(await TranslationsParent.#getTranslationModelRecords()).values(), ...(await TranslationsParent.#getTranslationModelRecords()).values(),
@@ -2764,9 +2816,9 @@ export class TranslationsParent extends JSWindowActorParent {
if ( if (
!lazy.TranslationsUtils.langTagsMatch( !lazy.TranslationsUtils.langTagsMatch(
record.fromLang, record.fromLang,
fromLanguage sourceLanguage
) || ) ||
!lazy.TranslationsUtils.langTagsMatch(record.toLang, toLanguage) !lazy.TranslationsUtils.langTagsMatch(record.toLang, targetLanguage)
) { ) {
return; return;
} }
@@ -2864,23 +2916,27 @@ export class TranslationsParent extends JSWindowActorParent {
} }
/** /**
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
* @param {boolean} reportAsAutoTranslate - In telemetry, report this as * @param {boolean} reportAsAutoTranslate - In telemetry, report this as
* an auto-translate. * an auto-translate.
*/ */
async translate(fromLanguage, toLanguage, reportAsAutoTranslate) { async translate(languagePair, reportAsAutoTranslate) {
if (!fromLanguage || !toLanguage) { const { sourceLanguage, targetLanguage } = languagePair;
if (!sourceLanguage || !targetLanguage) {
lazy.console.error( lazy.console.error(
"A translation was requested but the fromLanguage or toLanguage was not set.", new Error(
{ fromLanguage, toLanguage, reportAsAutoTranslate } "A translation was requested but the sourceLanguage or targetLanguage was not set."
),
{ sourceLanguage, targetLanguage, reportAsAutoTranslate }
); );
return; return;
} }
if (lazy.TranslationsUtils.langTagsMatch(fromLanguage, toLanguage)) { if (lazy.TranslationsUtils.langTagsMatch(sourceLanguage, targetLanguage)) {
lazy.console.error( lazy.console.error(
"A translation was requested where the from and to language match.", new Error(
{ fromLanguage, toLanguage, reportAsAutoTranslate } "A translation was requested where the source and target languages match."
),
{ sourceLanguage, targetLanguage, reportAsAutoTranslate }
); );
return; return;
} }
@@ -2888,8 +2944,8 @@ export class TranslationsParent extends JSWindowActorParent {
// This page has already been translated, restore it and translate it // This page has already been translated, restore it and translate it
// again once the actor has been recreated. // again once the actor has been recreated.
const windowState = this.getWindowState(); const windowState = this.getWindowState();
windowState.translateOnPageReload = { fromLanguage, toLanguage }; windowState.translateOnPageReload = languagePair;
this.restorePage(fromLanguage); this.restorePage(sourceLanguage);
} else { } else {
const { docLangTag } = this.languageState.detectedLanguages; const { docLangTag } = this.languageState.detectedLanguages;
@@ -2902,22 +2958,18 @@ export class TranslationsParent extends JSWindowActorParent {
// The MessageChannel will be used for communicating directly between the content // The MessageChannel will be used for communicating directly between the content
// process and the engine's process. // process and the engine's process.
const port = await TranslationsParent.requestTranslationsPort( const port = await TranslationsParent.requestTranslationsPort(
fromLanguage, languagePair,
toLanguage,
this this
); );
if (!port) { if (!port) {
lazy.console.error( lazy.console.error(
`Failed to create a translations port for language pair: (${fromLanguage} -> ${toLanguage})` `Failed to create a translations port for language pair: (${lazy.TranslationsUtils.serializeLanguagePair(languagePair)})`
); );
return; return;
} }
this.languageState.requestedLanguagePair = { this.languageState.requestedLanguagePair = languagePair;
fromLanguage,
toLanguage,
};
const preferredLanguages = TranslationsParent.getPreferredLanguages(); const preferredLanguages = TranslationsParent.getPreferredLanguages();
const topPreferredLanguage = const topPreferredLanguage =
@@ -2927,20 +2979,19 @@ export class TranslationsParent extends JSWindowActorParent {
TranslationsParent.telemetry().onTranslate({ TranslationsParent.telemetry().onTranslate({
docLangTag, docLangTag,
fromLanguage, sourceLanguage,
toLanguage, targetLanguage,
topPreferredLanguage, topPreferredLanguage,
autoTranslate: reportAsAutoTranslate, autoTranslate: reportAsAutoTranslate,
requestTarget: "full_page", requestTarget: "full_page",
}); });
TranslationsParent.storeMostRecentTargetLanguage(toLanguage); TranslationsParent.storeMostRecentTargetLanguage(targetLanguage);
this.sendAsyncMessage( this.sendAsyncMessage(
"Translations:TranslatePage", "Translations:TranslatePage",
{ {
fromLanguage, languagePair,
toLanguage,
port, port,
}, },
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
@@ -3030,8 +3081,8 @@ export class TranslationsParent extends JSWindowActorParent {
* Searches the provided language pairs for a match based on the given language tag. * Searches the provided language pairs for a match based on the given language tag.
* *
* @param {string} langTag - A BCP-47 language tag to match against source languages. * @param {string} langTag - A BCP-47 language tag to match against source languages.
* @param {Array<{ fromLang: string, toLang: string }>} languagePairs - An array of language pair objects, * @param {Array<{ sourceLanguage: string, targetLanguage: string }>} languagePairs - An array of language pair objects,
* where each object contains `fromLang` and `toLang` properties. * where each object contains `sourceLanguage` and `targetLanguage` properties.
* @returns {string | null} - The compatible source language tag, or `null` if no match is found. * @returns {string | null} - The compatible source language tag, or `null` if no match is found.
*/ */
static findCompatibleSourceLangTagSync(langTag, languagePairs) { static findCompatibleSourceLangTagSync(langTag, languagePairs) {
@@ -3039,11 +3090,11 @@ export class TranslationsParent extends JSWindowActorParent {
return null; return null;
} }
const langPair = languagePairs.find(({ fromLang }) => const langPair = languagePairs.find(({ sourceLanguage }) =>
lazy.TranslationsUtils.langTagsMatch(fromLang, langTag) lazy.TranslationsUtils.langTagsMatch(sourceLanguage, langTag)
); );
return langPair?.fromLang; return langPair?.sourceLanguage;
} }
/** /**
@@ -3055,7 +3106,7 @@ export class TranslationsParent extends JSWindowActorParent {
* or `null` if no match is found. * or `null` if no match is found.
*/ */
static async findCompatibleSourceLangTag(langTag) { static async findCompatibleSourceLangTag(langTag) {
const languagePairs = await TranslationsParent.getLanguagePairs(); const languagePairs = await TranslationsParent.getNonPivotLanguagePairs();
return TranslationsParent.findCompatibleSourceLangTagSync( return TranslationsParent.findCompatibleSourceLangTagSync(
langTag, langTag,
languagePairs languagePairs
@@ -3067,8 +3118,8 @@ export class TranslationsParent extends JSWindowActorParent {
* Searches the provided language pairs for a match based on the given language tag. * Searches the provided language pairs for a match based on the given language tag.
* *
* @param {string} langTag - A BCP-47 language tag to match against target languages. * @param {string} langTag - A BCP-47 language tag to match against target languages.
* @param {Array<{ fromLang: string, toLang: string }>} languagePairs - An array of language pair objects, * @param {Array<{ sourceLanguage: string, targetLanguage: string }>} languagePairs - An array of language pair objects,
* where each object contains `fromLang` and `toLang` properties. * where each object contains `sourceLanguage` and `targetLanguage` properties.
* @returns {string | null} - The compatible target language tag, or `null` if no match is found. * @returns {string | null} - The compatible target language tag, or `null` if no match is found.
*/ */
static findCompatibleTargetLangTagSync(langTag, languagePairs) { static findCompatibleTargetLangTagSync(langTag, languagePairs) {
@@ -3076,11 +3127,11 @@ export class TranslationsParent extends JSWindowActorParent {
return null; return null;
} }
const langPair = languagePairs.find(({ toLang }) => const langPair = languagePairs.find(({ targetLanguage }) =>
lazy.TranslationsUtils.langTagsMatch(toLang, langTag) lazy.TranslationsUtils.langTagsMatch(targetLanguage, langTag)
); );
return langPair?.toLang; return langPair?.targetLanguage;
} }
/** /**
@@ -3092,7 +3143,7 @@ export class TranslationsParent extends JSWindowActorParent {
* or `null` if no match is found. * or `null` if no match is found.
*/ */
static async findCompatibleTargetLangTag(langTag) { static async findCompatibleTargetLangTag(langTag) {
const languagePairs = await TranslationsParent.getLanguagePairs(); const languagePairs = await TranslationsParent.getNonPivotLanguagePairs();
return TranslationsParent.findCompatibleTargetLangTagSync( return TranslationsParent.findCompatibleTargetLangTagSync(
langTag, langTag,
languagePairs languagePairs
@@ -3111,7 +3162,7 @@ export class TranslationsParent extends JSWindowActorParent {
excludeLangTags, excludeLangTags,
}); });
const languagePairs = await TranslationsParent.getLanguagePairs(); const languagePairs = await TranslationsParent.getNonPivotLanguagePairs();
for (const langTag of preferredLanguages) { for (const langTag of preferredLanguages) {
const compatibleLangTag = const compatibleLangTag =
TranslationsParent.findCompatibleTargetLangTagSync( TranslationsParent.findCompatibleTargetLangTagSync(
@@ -3176,7 +3227,7 @@ export class TranslationsParent extends JSWindowActorParent {
documentElementLang = this.maybeRefineMacroLanguageTag(documentElementLang); documentElementLang = this.maybeRefineMacroLanguageTag(documentElementLang);
let languagePairs = await TranslationsParent.getLanguagePairs(); let languagePairs = await TranslationsParent.getNonPivotLanguagePairs();
if (this.#isDestroyed) { if (this.#isDestroyed) {
return null; return null;
} }
@@ -3737,7 +3788,7 @@ class TranslationsLanguageState {
/** @type {TranslationsParent} */ /** @type {TranslationsParent} */
#actor; #actor;
/** @type {TranslationPair | null} */ /** @type {LanguagePair | null} */
#requestedLanguagePair = null; #requestedLanguagePair = null;
/** @type {LangTags | null} */ /** @type {LangTags | null} */
@@ -3776,7 +3827,7 @@ class TranslationsLanguageState {
* that the TranslationsChild should be creating a TranslationsDocument and keep * that the TranslationsChild should be creating a TranslationsDocument and keep
* the page updated with the target language. * the page updated with the target language.
* *
* @returns {TranslationPair | null} * @returns {LanguagePair | null}
*/ */
get requestedLanguagePair() { get requestedLanguagePair() {
return this.#requestedLanguagePair; return this.#requestedLanguagePair;

View File

@@ -2,6 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* @typedef {typeof import("../translations")} Translations
*/
/**
* @typedef {import("../translations").LanguagePair} LanguagePair
* @typedef {import("../translations").RequestTranslationsPort} RequestTranslationsPort
*/
/** /**
* This class manages the communications to the translations engine via MessagePort. * This class manages the communications to the translations engine via MessagePort.
*/ */
@@ -29,24 +38,17 @@ export class Translator {
#ready = Promise.reject; #ready = Promise.reject;
/** /**
* The BCP-47 language tag for the from-language. * The current language pair to use for translation.
* *
* @type {string} * @type {LanguagePair}
*/ */
#fromLanguage; #languagePair;
/**
* The BCP-47 language tag for the to-language.
*
* @type {string}
*/
#toLanguage;
/** /**
* The callback function to request a new port, provided at construction time * The callback function to request a new port, provided at construction time
* by the caller. * by the caller.
* *
* @type {Function} * @type {RequestTranslationsPort}
*/ */
#requestTranslationsPort; #requestTranslationsPort;
@@ -71,13 +73,11 @@ export class Translator {
* *
* @see Translator.create * @see Translator.create
* *
* @param {string} fromLanguage - The BCP-47 from-language tag. * @param {LanguagePair} languagePair
* @param {string} toLanguage - The BCP-47 to-language tag. * @param {RequestTranslationsPort} requestTranslationsPort - A callback function to request a new MessagePort.
* @param {Function} requestTranslationsPort - A callback function to request a new MessagePort.
*/ */
constructor(fromLanguage, toLanguage, requestTranslationsPort) { constructor(languagePair, requestTranslationsPort) {
this.#fromLanguage = fromLanguage; this.#languagePair = languagePair;
this.#toLanguage = toLanguage;
this.#requestTranslationsPort = requestTranslationsPort; this.#requestTranslationsPort = requestTranslationsPort;
} }
@@ -95,52 +95,30 @@ export class Translator {
return this.#portClosed; return this.#portClosed;
} }
/**
* @returns {string} The BCP-47 language tag of the from-language.
*/
get fromLanguage() {
return this.#fromLanguage;
}
/**
* @returns {string} The BCP-47 language tag of the to-language.
*/
get toLanguage() {
return this.#toLanguage;
}
/** /**
* Opens up a port and creates a new translator. * Opens up a port and creates a new translator.
* *
* @param {string} fromLanguage - The BCP-47 language tag of the from-language. * @param {LanguagePair} languagePair
* @param {string} toLanguage - The BCP-47 language tag of the to-language. * @param {RequestTranslationsPort} [requestTranslationsPort]
* @param {object} data - Data for creating a translator.
* @param {Function} [data.requestTranslationsPort]
* - A function to request a translations port for communication with the Translations engine. * - A function to request a translations port for communication with the Translations engine.
* This is required in all cases except if allowSameLanguage is true and the fromLanguage * This is required in all cases except if allowSameLanguage is true and the sourceLanguage
* is the same as the toLanguage. * is the same as the targetLanguage.
* @param {boolean} [data.allowSameLanguage] * @param {boolean} [allowSameLanguage]
* - Whether to allow or disallow the creation of a PassthroughTranslator in the event * - Whether to allow or disallow the creation of a PassthroughTranslator in the event
* that the fromLanguage and the toLanguage are the same language. * that the sourceLanguage and the targetLanguage are the same language.
* *
* @returns {Promise<Translator | PassthroughTranslator>} * @returns {Promise<Translator | PassthroughTranslator>}
*/ */
static async create( static async create(
fromLanguage, languagePair,
toLanguage, requestTranslationsPort,
{ requestTranslationsPort, allowSameLanguage } allowSameLanguage
) { ) {
if (!fromLanguage || !toLanguage) { if (languagePair.sourceLanguage === languagePair.targetLanguage) {
throw new Error(
"Attempt to create Translator with missing language tags."
);
}
if (fromLanguage === toLanguage) {
if (!allowSameLanguage) { if (!allowSameLanguage) {
throw new Error("Attempt to create disallowed PassthroughTranslator"); throw new Error("Attempt to create disallowed PassthroughTranslator");
} }
return new PassthroughTranslator(fromLanguage, toLanguage); return new PassthroughTranslator(languagePair);
} }
if (!requestTranslationsPort) { if (!requestTranslationsPort) {
@@ -149,11 +127,7 @@ export class Translator {
); );
} }
const translator = new Translator( const translator = new Translator(languagePair, requestTranslationsPort);
fromLanguage,
toLanguage,
requestTranslationsPort
);
await translator.#createNewPortIfClosed(); await translator.#createNewPortIfClosed();
return translator; return translator;
@@ -169,10 +143,7 @@ export class Translator {
return; return;
} }
this.#port = await this.#requestTranslationsPort( this.#port = await this.#requestTranslationsPort(this.#languagePair);
this.#fromLanguage,
this.#toLanguage
);
this.#portClosed = false; this.#portClosed = false;
// Create a promise that will be resolved when the engine is ready. // Create a promise that will be resolved when the engine is ready.
@@ -193,7 +164,7 @@ export class Translator {
resolve(); resolve();
} else { } else {
this.#portClosed = true; this.#portClosed = true;
reject(); reject(new Error(data.error));
} }
break; break;
} }
@@ -256,18 +227,18 @@ export class Translator {
/** /**
* The PassthroughTranslator class mimics the same API as the Translator class, * The PassthroughTranslator class mimics the same API as the Translator class,
* but it does not create any message ports for actual translation. This class * but it does not create any message ports for actual translation. This class
* may only be constructed with the same fromLanguage and toLanguage value, and * may only be constructed with the same sourceLanguage and targetLanguage value, and
* instead of translating, it just passes through the source text as the translated * instead of translating, it just passes through the source text as the translated
* text. * text.
* *
* The Translator class may return a PassthroughTranslator instance if the fromLanguage * The Translator class may return a PassthroughTranslator instance if the sourceLanguage
* and toLanguage passed to the create() method are the same. * and targetLanguage passed to the create() method are the same.
* *
* @see Translator.create * @see Translator.create
*/ */
class PassthroughTranslator { class PassthroughTranslator {
/** /**
* The BCP-47 language tag for the from-language and the to-language. * The BCP-47 language tag for the source and target language..
* *
* @type {string} * @type {string}
*/ */
@@ -288,16 +259,16 @@ class PassthroughTranslator {
} }
/** /**
* @returns {string} The BCP-47 language tag of the from-language. * @returns {string} The BCP-47 language tag of the source language.
*/ */
get fromLanguage() { get sourceLanguage() {
return this.#language; return this.#language;
} }
/** /**
* @returns {string} The BCP-47 language tag of the to-language. * @returns {string} The BCP-47 language tag of the source language.
*/ */
get toLanguage() { get targetLanguage() {
return this.#language; return this.#language;
} }
@@ -308,16 +279,15 @@ class PassthroughTranslator {
* *
* @see Translator.create * @see Translator.create
* *
* @param {string} fromLanguage - The BCP-47 from-language tag. * @param {LanguagePair} languagePair
* @param {string} toLanguage - The BCP-47 to-language tag.
*/ */
constructor(fromLanguage, toLanguage) { constructor(languagePair) {
if (fromLanguage !== toLanguage) { if (languagePair.sourceLanguage !== languagePair.targetLanguage) {
throw new Error( throw new Error(
"Attempt to create PassthroughTranslator with different fromLanguage and toLanguage." "Attempt to create PassthroughTranslator with different sourceLanguage and targetLanguage."
); );
} }
this.#language = fromLanguage; this.#language = languagePair.sourceLanguage;
} }
/** /**

View File

@@ -53,10 +53,8 @@ export class LRUCache {
#htmlCache = new Map(); #htmlCache = new Map();
/** @type {Map<string, string>} */ /** @type {Map<string, string>} */
#textCache = new Map(); #textCache = new Map();
/** @type {string} */ /** @type {LanguagePair} */
#fromLanguage; #languagePair;
/** @type {string} */
#toLanguage;
/** /**
* This limit is used twice, once for Text translations, and once for HTML translations. * This limit is used twice, once for Text translations, and once for HTML translations.
@@ -69,12 +67,10 @@ export class LRUCache {
#cacheExpirationMS = 10 * 60_000; #cacheExpirationMS = 10 * 60_000;
/** /**
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
*/ */
constructor(fromLanguage, toLanguage) { constructor(languagePair) {
this.#fromLanguage = fromLanguage; this.#languagePair = languagePair;
this.#toLanguage = toLanguage;
} }
/** /**
@@ -129,13 +125,18 @@ export class LRUCache {
} }
/** /**
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
*/ */
matches(fromLanguage, toLanguage) { matches(languagePair) {
return ( return (
lazy.TranslationsUtils.langTagsMatch(this.#fromLanguage, fromLanguage) && lazy.TranslationsUtils.langTagsMatch(
lazy.TranslationsUtils.langTagsMatch(this.#toLanguage, toLanguage) this.#languagePair.sourceLanguage,
languagePair.sourceLanguage
) &&
lazy.TranslationsUtils.langTagsMatch(
this.#languagePair.targetLanguage,
languagePair.targetLanguage
)
); );
} }
@@ -496,7 +497,7 @@ export class TranslationsDocument {
* *
* @param {Document} document * @param {Document} document
* @param {string} documentLanguage - The BCP 47 tag of the source language. * @param {string} documentLanguage - The BCP 47 tag of the source language.
* @param {string} toLanguage - The BCP 47 tag of the destination language. * @param {string} targetLanguage - The BCP 47 tag of the destination language.
* @param {number} innerWindowId - This is used for better profiler marker reporting. * @param {number} innerWindowId - This is used for better profiler marker reporting.
* @param {MessagePort} port - The port to the translations engine. * @param {MessagePort} port - The port to the translations engine.
* @param {() => void} requestNewPort - Used when an engine times out and a new * @param {() => void} requestNewPort - Used when an engine times out and a new
@@ -510,7 +511,7 @@ export class TranslationsDocument {
constructor( constructor(
document, document,
documentLanguage, documentLanguage,
toLanguage, targetLanguage,
innerWindowId, innerWindowId,
port, port,
requestNewPort, requestNewPort,
@@ -660,7 +661,7 @@ export class TranslationsDocument {
); );
}); });
document.documentElement.lang = toLanguage; document.documentElement.lang = targetLanguage;
lazy.console.log( lazy.console.log(
"Beginning to translate.", "Beginning to translate.",
@@ -1140,7 +1141,7 @@ export class TranslationsDocument {
} }
if (!this.matchesDocumentLanguage(node)) { if (!this.matchesDocumentLanguage(node)) {
// Exclude nodes that don't match the fromLanguage. // Exclude nodes that don't match the sourceLanguage.
return true; return true;
} }

View File

@@ -71,8 +71,15 @@ const CACHE_TIMEOUT_MS = 15_000;
/** /**
* @typedef {import("./translations-document.sys.mjs").TranslationsDocument} TranslationsDocument * @typedef {import("./translations-document.sys.mjs").TranslationsDocument} TranslationsDocument
* @typedef {import("../translations.js").TranslationsEnginePayload} TranslationsEnginePayload * @typedef {import("../translations.js").TranslationsEnginePayload} TranslationsEnginePayload
* @typedef {import("../translations.js").LanguagePair} LanguagePair
*/ */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TranslationsUtils:
"chrome://global/content/translations/TranslationsUtils.mjs",
});
/** /**
* The TranslationsEngine encapsulates the logic for translating messages. It can * The TranslationsEngine encapsulates the logic for translating messages. It can
* only be set up for a single language pair. In order to change languages * only be set up for a single language pair. In order to change languages
@@ -112,33 +119,29 @@ export class TranslationsEngine {
* call, and then return the cached one. After a timeout when the engine hasn't * call, and then return the cached one. After a timeout when the engine hasn't
* been used, it is destroyed. * been used, it is destroyed.
* *
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
* @param {number} innerWindowId * @param {number} innerWindowId
* @returns {Promise<TranslationsEngine>} * @returns {Promise<TranslationsEngine>}
*/ */
static getOrCreate(fromLanguage, toLanguage, innerWindowId) { static getOrCreate(languagePair, innerWindowId) {
const languagePairKey = getLanguagePairKey(fromLanguage, toLanguage); const languagePairKey =
lazy.TranslationsUtils.serializeLanguagePair(languagePair);
let enginePromise = TranslationsEngine.#cachedEngines.get(languagePairKey); let enginePromise = TranslationsEngine.#cachedEngines.get(languagePairKey);
if (enginePromise) { if (enginePromise) {
return enginePromise; return enginePromise;
} }
TE_log(`Creating a new engine for "${fromLanguage}" to "${toLanguage}".`); TE_log(`Creating a new engine for "${languagePairKey}".`);
// A new engine needs to be created. // A new engine needs to be created.
enginePromise = TranslationsEngine.create( enginePromise = TranslationsEngine.create(languagePair, innerWindowId);
fromLanguage,
toLanguage,
innerWindowId
);
TranslationsEngine.#cachedEngines.set(languagePairKey, enginePromise); TranslationsEngine.#cachedEngines.set(languagePairKey, enginePromise);
enginePromise.catch(error => { enginePromise.catch(error => {
TE_logError( TE_logError(
`The engine failed to load for translating "${fromLanguage}" to "${toLanguage}". Removing it from the cache.`, `The engine failed to load for translating "${languagePairKey}". Removing it from the cache.`,
error error
); );
// Remove the engine if it fails to initialize. // Remove the engine if it fails to initialize.
@@ -166,25 +169,28 @@ export class TranslationsEngine {
/** /**
* Create a TranslationsEngine and bypass the cache. * Create a TranslationsEngine and bypass the cache.
* *
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
* @param {number} innerWindowId * @param {number} innerWindowId
* @returns {Promise<TranslationsEngine>} * @returns {Promise<TranslationsEngine>}
*/ */
static async create(fromLanguage, toLanguage, innerWindowId) { static async create(languagePair, innerWindowId) {
const startTime = performance.now(); const startTime = performance.now();
if (!languagePair.sourceLanguage || !languagePair.targetLanguage) {
throw new Error(
"Attempt to create Translator with missing language tags."
);
}
const engine = new TranslationsEngine( const engine = new TranslationsEngine(
fromLanguage, languagePair,
toLanguage, await TE_requestEnginePayload(languagePair)
await TE_requestEnginePayload(fromLanguage, toLanguage)
); );
await engine.isReady; await engine.isReady;
TE_addProfilerMarker({ TE_addProfilerMarker({
startTime, startTime,
message: `Translations engine loaded for "${fromLanguage}" to "${toLanguage}"`, message: `Translations engine loaded for "${lazy.TranslationsUtils.serializeLanguagePair(languagePair)}"`,
innerWindowId, innerWindowId,
}); });
@@ -221,10 +227,10 @@ export class TranslationsEngine {
clearTimeout(this.#keepAliveTimeout); clearTimeout(this.#keepAliveTimeout);
} }
for (const [innerWindowId, data] of ports) { for (const [innerWindowId, data] of ports) {
const { fromLanguage, toLanguage, port } = data; const { sourceLanguage, targetLanguage, port } = data;
if ( if (
fromLanguage === this.fromLanguage && sourceLanguage === this.sourceLanguage &&
toLanguage === this.toLanguage targetLanguage === this.targetLanguage
) { ) {
// This port is still active but being closed. // This port is still active but being closed.
ports.delete(innerWindowId); ports.delete(innerWindowId);
@@ -252,17 +258,15 @@ export class TranslationsEngine {
/** /**
* Construct and initialize the worker. * Construct and initialize the worker.
* *
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
* @param {TranslationsEnginePayload} enginePayload - If there is no engine payload * @param {TranslationsEnginePayload} enginePayload - If there is no engine payload
* then the engine will be mocked. This allows this class to be used in tests. * then the engine will be mocked. This allows this class to be used in tests.
*/ */
constructor(fromLanguage, toLanguage, enginePayload) { constructor(languagePair, enginePayload) {
/** @type {string} */ /** @type {LanguagePair} */
this.fromLanguage = fromLanguage; this.languagePair = languagePair;
/** @type {string} */ this.languagePairKey =
this.toLanguage = toLanguage; lazy.TranslationsUtils.serializeLanguagePair(languagePair);
this.languagePairKey = getLanguagePairKey(fromLanguage, toLanguage);
this.#worker = new Worker( this.#worker = new Worker(
"chrome://global/content/translations/translations-engine.worker.js" "chrome://global/content/translations/translations-engine.worker.js"
); );
@@ -297,11 +301,13 @@ export class TranslationsEngine {
} }
} }
const { sourceLanguage, targetLanguage } = languagePair;
this.#worker.postMessage( this.#worker.postMessage(
{ {
type: "initialize", type: "initialize",
fromLanguage, sourceLanguage,
toLanguage, targetLanguage,
enginePayload, enginePayload,
messageId: this.#messageId++, messageId: this.#messageId++,
logLevel: TE_getLogLevel(), logLevel: TE_getLogLevel(),
@@ -370,13 +376,12 @@ export class TranslationsEngine {
/** /**
* Applies a function only if a cached engine exists. * Applies a function only if a cached engine exists.
* *
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
* @param {(engine: TranslationsEngine) => void} fn * @param {(engine: TranslationsEngine) => void} fn
*/ */
static withCachedEngine(fromLanguage, toLanguage, fn) { static withCachedEngine(languagePair, fn) {
const engine = TranslationsEngine.#cachedEngines.get( const engine = TranslationsEngine.#cachedEngines.get(
getLanguagePairKey(fromLanguage, toLanguage) lazy.TranslationsUtils.serializeLanguagePair(languagePair)
); );
if (engine) { if (engine) {
@@ -409,37 +414,15 @@ export class TranslationsEngine {
translationId, translationId,
}); });
} }
/**
* Pause or resume the translations from a cached engine.
*
* @param {boolean} pause
* @param {string} fromLanguage
* @param {string} toLanguage
* @param {number} innerWindowId
*/
static pause(pause, fromLanguage, toLanguage, innerWindowId) {
TranslationsEngine.withCachedEngine(fromLanguage, toLanguage, engine => {
engine.pause(pause, innerWindowId);
});
}
}
/**
* Creates a lookup key that is unique to each fromLanguage-toLanguage pair.
*
* @param {string} fromLanguage
* @param {string} toLanguage
* @returns {string}
*/
function getLanguagePairKey(fromLanguage, toLanguage) {
return `${fromLanguage},${toLanguage}`;
} }
/** /**
* Maps the innerWindowId to the port. * Maps the innerWindowId to the port.
* *
* @type {Map<number, { fromLanguage: string, toLanguage: string, port: MessagePort }>} * @type {Map<number, {
* languagePair: LanguagePair,
* port: MessagePort
* }>}
*/ */
const ports = new Map(); const ports = new Map();
@@ -448,23 +431,18 @@ const ports = new Map();
* them to the TranslationsEngine manager. The other end of the port is held * them to the TranslationsEngine manager. The other end of the port is held
* in the content process by the TranslationsDocument. * in the content process by the TranslationsDocument.
* *
* @param {string} fromLanguage * @param {LanguagePair} languagePair
* @param {string} toLanguage
* @param {number} innerWindowId * @param {number} innerWindowId
* @param {MessagePort} port * @param {MessagePort} port
*/ */
function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) { function listenForPortMessages(languagePair, innerWindowId, port) {
async function handleMessage({ data }) { async function handleMessage({ data }) {
switch (data.type) { switch (data.type) {
case "TranslationsPort:GetEngineStatusRequest": { case "TranslationsPort:GetEngineStatusRequest": {
// This message gets sent first before the translation queue is processed. // This message gets sent first before the translation queue is processed.
// The engine is most likely to fail on the initial invocation. Any failure // The engine is most likely to fail on the initial invocation. Any failure
// past the first one is not reported to the UI. // past the first one is not reported to the UI.
TranslationsEngine.getOrCreate( TranslationsEngine.getOrCreate(languagePair, innerWindowId).then(
fromLanguage,
toLanguage,
innerWindowId
).then(
() => { () => {
TE_log("The engine is ready for translations.", { TE_log("The engine is ready for translations.", {
innerWindowId, innerWindowId,
@@ -475,11 +453,13 @@ function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) {
status: "ready", status: "ready",
}); });
}, },
() => { error => {
console.error(error);
TE_reportEngineStatus(innerWindowId, "error"); TE_reportEngineStatus(innerWindowId, "error");
port.postMessage({ port.postMessage({
type: "TranslationsPort:GetEngineStatusResponse", type: "TranslationsPort:GetEngineStatusResponse",
status: "error", status: "error",
error: String(error),
}); });
// After an error no more translation requests will be sent. Go ahead // After an error no more translation requests will be sent. Go ahead
// and close the port. // and close the port.
@@ -492,8 +472,7 @@ function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) {
case "TranslationsPort:TranslationRequest": { case "TranslationsPort:TranslationRequest": {
const { sourceText, isHTML, translationId } = data; const { sourceText, isHTML, translationId } = data;
const engine = await TranslationsEngine.getOrCreate( const engine = await TranslationsEngine.getOrCreate(
fromLanguage, languagePair,
toLanguage,
innerWindowId innerWindowId
); );
const targetText = await engine.translate( const targetText = await engine.translate(
@@ -511,13 +490,9 @@ function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) {
} }
case "TranslationsPort:CancelSingleTranslation": { case "TranslationsPort:CancelSingleTranslation": {
const { translationId } = data; const { translationId } = data;
TranslationsEngine.withCachedEngine( TranslationsEngine.withCachedEngine(languagePair, engine => {
fromLanguage,
toLanguage,
engine => {
engine.cancelSingleTranslation(innerWindowId, translationId); engine.cancelSingleTranslation(innerWindowId, translationId);
} });
);
break; break;
} }
case "TranslationsPort:DiscardTranslations": { case "TranslationsPort:DiscardTranslations": {
@@ -551,11 +526,11 @@ function discardTranslations(innerWindowId) {
const portData = ports.get(innerWindowId); const portData = ports.get(innerWindowId);
if (portData) { if (portData) {
const { port, fromLanguage, toLanguage } = portData; const { port, languagePair } = portData;
port.close(); port.close();
ports.delete(innerWindowId); ports.delete(innerWindowId);
TranslationsEngine.withCachedEngine(fromLanguage, toLanguage, engine => { TranslationsEngine.withCachedEngine(languagePair, engine => {
engine.discardTranslationQueue(innerWindowId); engine.discardTranslationQueue(innerWindowId);
}); });
} }
@@ -567,10 +542,14 @@ function discardTranslations(innerWindowId) {
window.addEventListener("message", ({ data }) => { window.addEventListener("message", ({ data }) => {
switch (data.type) { switch (data.type) {
case "StartTranslation": { case "StartTranslation": {
const { fromLanguage, toLanguage, innerWindowId, port } = data; const { languagePair, innerWindowId, port } = data;
TE_log("Starting translation", innerWindowId); TE_log(
listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port); "Starting translation",
ports.set(innerWindowId, { port, fromLanguage, toLanguage }); lazy.TranslationsUtils.serializeLanguagePair(languagePair),
innerWindowId
);
listenForPortMessages(languagePair, innerWindowId, port);
ports.set(innerWindowId, { port, languagePair });
break; break;
} }
case "DiscardTranslations": { case "DiscardTranslations": {

View File

@@ -136,14 +136,19 @@ async function handleInitializationMessage({ data }) {
} }
try { try {
const { fromLanguage, toLanguage, enginePayload, logLevel, innerWindowId } = const {
data; sourceLanguage,
targetLanguage,
enginePayload,
logLevel,
innerWindowId,
} = data;
if (!fromLanguage) { if (!sourceLanguage) {
throw new Error('Worker initialization missing "fromLanguage"'); throw new Error('Worker initialization missing "sourceLanguage"');
} }
if (!toLanguage) { if (!targetLanguage) {
throw new Error('Worker initialization missing "toLanguage"'); throw new Error('Worker initialization missing "targetLanguage"');
} }
if (logLevel) { if (logLevel) {
@@ -154,7 +159,7 @@ async function handleInitializationMessage({ data }) {
let engine; let engine;
if (enginePayload.isMocked) { if (enginePayload.isMocked) {
// The engine is testing mode, and no Bergamot wasm is available. // The engine is testing mode, and no Bergamot wasm is available.
engine = new MockedEngine(fromLanguage, toLanguage); engine = new MockedEngine(sourceLanguage, targetLanguage);
} else { } else {
const { bergamotWasmArrayBuffer, translationModelPayloads } = const { bergamotWasmArrayBuffer, translationModelPayloads } =
enginePayload; enginePayload;
@@ -162,8 +167,8 @@ async function handleInitializationMessage({ data }) {
bergamotWasmArrayBuffer bergamotWasmArrayBuffer
); );
engine = new Engine( engine = new Engine(
fromLanguage, sourceLanguage,
toLanguage, targetLanguage,
bergamot, bergamot,
translationModelPayloads translationModelPayloads
); );
@@ -220,7 +225,7 @@ function handleMessages(engine) {
} }
try { try {
const { whitespaceBefore, whitespaceAfter, cleanedSourceText } = const { whitespaceBefore, whitespaceAfter, cleanedSourceText } =
cleanText(engine.fromLanguage, sourceText); cleanText(engine.sourceLanguage, sourceText);
// Add a translation to the work queue, and when it returns, post the message // Add a translation to the work queue, and when it returns, post the message
// back. The translation may never return if the translations are discarded // back. The translation may never return if the translations are discarded
@@ -318,16 +323,21 @@ function handleMessages(engine) {
*/ */
class Engine { class Engine {
/** /**
* @param {string} fromLanguage * @param {string} sourceLanguage
* @param {string} toLanguage * @param {string} targetLanguage
* @param {Bergamot} bergamot * @param {Bergamot} bergamot
* @param {Array<TranslationModelPayload>} translationModelPayloads * @param {Array<TranslationModelPayload>} translationModelPayloads
*/ */
constructor(fromLanguage, toLanguage, bergamot, translationModelPayloads) { constructor(
sourceLanguage,
targetLanguage,
bergamot,
translationModelPayloads
) {
/** @type {string} */ /** @type {string} */
this.fromLanguage = fromLanguage; this.sourceLanguage = sourceLanguage;
/** @type {string} */ /** @type {string} */
this.toLanguage = toLanguage; this.targetLanguage = targetLanguage;
/** @type {Bergamot} */ /** @type {Bergamot} */
this.bergamot = bergamot; this.bergamot = bergamot;
/** @type {Bergamot["TranslationModel"][]} */ /** @type {Bergamot["TranslationModel"][]} */
@@ -696,14 +706,14 @@ class BergamotUtils {
*/ */
class MockedEngine { class MockedEngine {
/** /**
* @param {string} fromLanguage * @param {string} sourceLanguage
* @param {string} toLanguage * @param {string} targetLanguage
*/ */
constructor(fromLanguage, toLanguage) { constructor(sourceLanguage, targetLanguage) {
/** @type {string} */ /** @type {string} */
this.fromLanguage = fromLanguage; this.sourceLanguage = sourceLanguage;
/** @type {string} */ /** @type {string} */
this.toLanguage = toLanguage; this.targetLanguage = targetLanguage;
} }
/** /**
@@ -717,7 +727,7 @@ class MockedEngine {
// Note when an HTML translations is requested. // Note when an HTML translations is requested.
let html = isHTML ? ", html" : ""; let html = isHTML ? ", html" : "";
const targetText = sourceText.toUpperCase(); const targetText = sourceText.toUpperCase();
return `${targetText} [${this.fromLanguage} to ${this.toLanguage}${html}]`; return `${targetText} [${this.sourceLanguage} to ${this.targetLanguage}${html}]`;
} }
discardTranslations() {} discardTranslations() {}

View File

@@ -11,6 +11,7 @@
AT_isTranslationEngineSupported, AT_identifyLanguage, AT_telemetry */ AT_isTranslationEngineSupported, AT_identifyLanguage, AT_telemetry */
import { Translator } from "chrome://global/content/translations/Translator.mjs"; import { Translator } from "chrome://global/content/translations/Translator.mjs";
import { TranslationsUtils } from "chrome://global/content/translations/TranslationsUtils.mjs";
// Allow tests to override this value so that they can run faster. // Allow tests to override this value so that they can run faster.
// This is the delay in milliseconds. // This is the delay in milliseconds.
@@ -44,7 +45,7 @@ class TranslationsState {
* *
* @type {string} * @type {string}
*/ */
fromLanguage = ""; sourceLanguage = "";
/** /**
* The language to translate to, in the form of a BCP 47 language tag, * The language to translate to, in the form of a BCP 47 language tag,
@@ -52,7 +53,26 @@ class TranslationsState {
* *
* @type {string} * @type {string}
*/ */
toLanguage = ""; targetLanguage = "";
/**
* The model variant.
*
* @type {string | undefined}
*/
sourceVariant;
/**
* The model variant.
*
* @type {string | undefined}
*/
targetVariant;
/**
* @type {LanguagePair | null}
*/
languagePair = null;
/** /**
* The message to translate, cached so that it can be determined if the text * The message to translate, cached so that it can be determined if the text
@@ -138,16 +158,16 @@ class TranslationsState {
* in a new translation request. * in a new translation request.
*/ */
onDebounce: async () => { onDebounce: async () => {
// The contents of "this" can change between async steps, store a local variable
// binding of these values.
const { fromLanguage, toLanguage, messageToTranslate, translator } = this;
if (!this.isTranslationEngineSupported) { if (!this.isTranslationEngineSupported) {
// Never translate when the engine isn't supported. // Never translate when the engine isn't supported.
return; return;
} }
if (!fromLanguage || !toLanguage || !messageToTranslate || !translator) { // The contents of "this" can change between async steps, store a local variable
// binding of these values.
const { messageToTranslate, translator, languagePair } = this;
if (!languagePair || !messageToTranslate || !translator) {
// Not everything is set for translation. // Not everything is set for translation.
this.ui.updateTranslation(""); this.ui.updateTranslation("");
return; return;
@@ -162,8 +182,7 @@ class TranslationsState {
// then skip this request, as there is already a newer request with more up to // then skip this request, as there is already a newer request with more up to
// date information. // date information.
this.translator !== translator || this.translator !== translator ||
this.fromLanguage !== fromLanguage || this.languagePair !== languagePair ||
this.toLanguage !== toLanguage ||
this.messageToTranslate !== messageToTranslate this.messageToTranslate !== messageToTranslate
) { ) {
return; return;
@@ -180,7 +199,7 @@ class TranslationsState {
// The measure events will show up in the Firefox Profiler. // The measure events will show up in the Firefox Profiler.
performance.measure( performance.measure(
`Translations: Translate "${this.fromLanguage}" to "${this.toLanguage}" with ${messageToTranslate.length} characters.`, `Translations: Translate "${this.languagePairKey}" with ${messageToTranslate.length} characters.`,
{ {
start, start,
end: performance.now(), end: performance.now(),
@@ -210,37 +229,41 @@ class TranslationsState {
// These are cases in which it wouldn't make sense or be possible to load any translations models. // These are cases in which it wouldn't make sense or be possible to load any translations models.
if ( if (
// If fromLanguage or toLanguage are unpopulated we cannot load anything. // If sourceLanguage or targetLanguage are unpopulated we cannot load anything.
!this.fromLanguage || !this.sourceLanguage ||
!this.toLanguage || !this.targetLanguage ||
// If fromLanguage's value is "detect", rather than a BCP 47 language tag, then no language // If sourceLanguage's value is "detect", rather than a BCP 47 language tag, then no language
// has been detected yet. // has been detected yet.
this.fromLanguage === "detect" || this.sourceLanguage === "detect" ||
// If fromLanguage and toLanguage are the same, this means that the detected language // If sourceLanguage and targetLanguage are the same, this means that the detected language
// is the same as the toLanguage, and we do not want to translate from one language to itself. // is the same as the targetLanguage, and we do not want to translate from one language to itself.
this.fromLanguage === this.toLanguage this.sourceLanguage === this.targetLanguage
) { ) {
if (this.translator) { if (this.translator) {
// The engine is no longer needed. // The engine is no longer needed.
this.translator.destroy(); this.translator.destroy();
this.translator = null; this.translator = null;
this.languagePair = null;
this.languagePairKey = null;
} }
return; return;
} }
const start = performance.now(); const start = performance.now();
AT_log( AT_log(
`Creating a new translator for "${this.fromLanguage}" to "${this.toLanguage}"` `Creating a new translator for "${this.sourceLanguage}" to "${this.targetLanguage}"`
); );
const translationPortPromise = (fromLanguage, toLanguage) => { const requestTranslationsPort = languagePair => {
const { promise, resolve } = Promise.withResolvers(); const { promise, resolve } = Promise.withResolvers();
const getResponse = ({ data }) => { const getResponse = ({ data }) => {
if ( if (
data.type == "GetTranslationsPort" && data.type == "GetTranslationsPort" &&
data.fromLanguage === fromLanguage && data.languagePair.sourceLanguage === languagePair.sourceLanguage &&
data.toLanguage === toLanguage data.languagePair.targetLanguage === languagePair.targetLanguage &&
data.languagePair.sourceVariant == languagePair.sourceVariant &&
data.languagePair.targetVariant == languagePair.targetVariant
) { ) {
window.removeEventListener("message", getResponse); window.removeEventListener("message", getResponse);
resolve(data.port); resolve(data.port);
@@ -248,29 +271,38 @@ class TranslationsState {
}; };
window.addEventListener("message", getResponse); window.addEventListener("message", getResponse);
AT_createTranslationsPort(fromLanguage, toLanguage); AT_createTranslationsPort(languagePair);
return promise; return promise;
}; };
this.languagePair = {
sourceLanguage: this.sourceLanguage,
targetLanguage: this.targetLanguage,
sourceVariant: this.sourceVariant,
targetVariant: this.targetVariant,
};
this.languagePairKey = TranslationsUtils.serializeLanguagePair(
this.languagePair
);
try { try {
const translatorPromise = Translator.create( const translatorPromise = Translator.create(
this.fromLanguage, this.languagePair,
this.toLanguage, requestTranslationsPort
{
allowSameLanguage: false,
requestTranslationsPort: translationPortPromise,
}
); );
const duration = performance.now() - start; const duration = performance.now() - start;
// Signal to tests that the translator was created so they can exit. // Signal to tests that the translator was created so they can exit.
window.postMessage("translator-ready"); window.postMessage("translator-ready");
AT_log(`Created a new Translator in ${duration / 1000} seconds`);
this.translator = await translatorPromise; this.translator = await translatorPromise;
AT_log(`Created a new Translator in ${duration / 1000} seconds`);
this.maybeRequestTranslation(); this.maybeRequestTranslation();
} catch (error) { } catch (error) {
this.languagePair = null;
this.languagePairKey = null;
this.ui.showInfo("about-translations-engine-error"); this.ui.showInfo("about-translations-engine-error");
this.ui.setResultPlaceholderTextContent(l10nIds.resultsPlaceholder); this.ui.setResultPlaceholderTextContent(l10nIds.resultsPlaceholder);
AT_logError("Failed to get the Translations worker", error); AT_logError("Failed to get the Translations worker", error);
@@ -278,10 +310,10 @@ class TranslationsState {
} }
/** /**
* Updates the fromLanguage to match the detected language only if the * Updates the sourceLanguage to match the detected language only if the
* about-translations-detect option is selected in the language-from dropdown. * about-translations-detect option is selected in the language-from dropdown.
* *
* If the new fromLanguage is different than the previous fromLanguage this * If the new sourceLanguage is different than the previous sourceLanguage this
* may update the UI to display the new language and may rebuild the translations * may update the UI to display the new language and may rebuild the translations
* worker if there is a valid selected target language. * worker if there is a valid selected target language.
*/ */
@@ -301,32 +333,36 @@ class TranslationsState {
// Only update the language if the detected language matches // Only update the language if the detected language matches
// one of our supported languages. // one of our supported languages.
const entry = supportedLanguages.fromLanguages.find( const entry = supportedLanguages.sourceLanguages.find(
({ langTag: existingTag }) => existingTag === langTag ({ langTag: existingTag }) => existingTag === langTag
); );
if (entry) { if (entry) {
const { displayName } = entry; const { displayName } = entry;
await this.setFromLanguage(langTag); await this.setSourceLanguage(langTag);
this.ui.setDetectOptionTextContent(displayName); this.ui.setDetectOptionTextContent(displayName);
} }
} }
/** /**
* @param {string} lang * @param {string} langTagKey
*/ */
async setFromLanguage(lang) { async setSourceLanguage(langTagKey) {
if (lang !== this.fromLanguage) { const [langTag, variant] = langTagKey.split(",");
this.fromLanguage = lang; if (langTag !== this.sourceLanguage || variant !== this.sourceVariant) {
this.sourceLanguage = langTag;
this.sourceVariant = variant;
await this.maybeCreateNewTranslator(); await this.maybeCreateNewTranslator();
} }
} }
/** /**
* @param {string} lang * @param {string} langTagKey
*/ */
setToLanguage(lang) { setTargetLanguage(langTagKey) {
if (lang !== this.toLanguage) { const [langTag, variant] = langTagKey.split(",");
this.toLanguage = lang; if (langTag !== this.targetLanguage || this.targetVariant !== variant) {
this.targetLanguage = langTag;
this.targetVariant = variant;
this.maybeCreateNewTranslator(); this.maybeCreateNewTranslator();
} }
} }
@@ -348,9 +384,9 @@ class TranslationsState {
*/ */
class TranslationsUI { class TranslationsUI {
/** @type {HTMLSelectElement} */ /** @type {HTMLSelectElement} */
languageFrom = document.getElementById("language-from"); sourceLanguage = document.getElementById("language-from");
/** @type {HTMLSelectElement} */ /** @type {HTMLSelectElement} */
languageTo = document.getElementById("language-to"); targetLanguage = document.getElementById("language-to");
/** @type {HTMLButtonElement} */ /** @type {HTMLButtonElement} */
languageSwap = document.getElementById("language-swap"); languageSwap = document.getElementById("language-swap");
/** @type {HTMLTextAreaElement} */ /** @type {HTMLTextAreaElement} */
@@ -419,45 +455,51 @@ class TranslationsUI {
const supportedLanguages = await this.state.supportedLanguages; const supportedLanguages = await this.state.supportedLanguages;
// Update the DOM elements with the display names. // Update the DOM elements with the display names.
for (const { langTag, displayName } of supportedLanguages.toLanguages) { for (const {
langTagKey,
displayName,
} of supportedLanguages.targetLanguages) {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = langTag; option.value = langTagKey;
option.text = displayName; option.text = displayName;
this.languageTo.add(option); this.targetLanguage.add(option);
} }
for (const { langTag, displayName } of supportedLanguages.fromLanguages) { for (const {
langTagKey,
displayName,
} of supportedLanguages.sourceLanguages) {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = langTag; option.value = langTagKey;
option.text = displayName; option.text = displayName;
this.languageFrom.add(option); this.sourceLanguage.add(option);
} }
// Enable the controls. // Enable the controls.
this.languageFrom.disabled = false; this.sourceLanguage.disabled = false;
this.languageTo.disabled = false; this.targetLanguage.disabled = false;
// Focus the language dropdowns if they are empty. // Focus the language dropdowns if they are empty.
if (this.languageFrom.value == "") { if (this.sourceLanguage.value == "") {
this.languageFrom.focus(); this.sourceLanguage.focus();
} else if (this.languageTo.value == "") { } else if (this.targetLanguage.value == "") {
this.languageTo.focus(); this.targetLanguage.focus();
} }
this.state.setFromLanguage(this.languageFrom.value); this.state.setSourceLanguage(this.sourceLanguage.value);
this.state.setToLanguage(this.languageTo.value); this.state.setTargetLanguage(this.targetLanguage.value);
await this.updateOnLanguageChange(); await this.updateOnLanguageChange();
this.languageFrom.addEventListener("input", async () => { this.sourceLanguage.addEventListener("input", async () => {
this.state.setFromLanguage(this.languageFrom.value); this.state.setSourceLanguage(this.sourceLanguage.value);
await this.updateOnLanguageChange(); await this.updateOnLanguageChange();
}); });
this.languageTo.addEventListener("input", async () => { this.targetLanguage.addEventListener("input", async () => {
this.state.setToLanguage(this.languageTo.value); this.state.setTargetLanguage(this.targetLanguage.value);
await this.updateOnLanguageChange(); await this.updateOnLanguageChange();
this.translationTo.setAttribute("lang", this.languageTo.value); this.translationTo.setAttribute("lang", this.targetLanguage.value);
}); });
} }
@@ -470,19 +512,20 @@ class TranslationsUI {
this.languageSwap.addEventListener("click", async () => { this.languageSwap.addEventListener("click", async () => {
const translationToValue = this.translationTo.innerText; const translationToValue = this.translationTo.innerText;
const newFromLanguage = this.sanitizeTargetLangTagAsSourceLangTag( const newSourceLanguage = this.sanitizeTargetLangTagAsSourceLangTag(
this.state.toLanguage this.targetLanguage.value
); );
const newToLanguage = this.sanitizeSourceLangTagAsTargetLangTag( const newTargetLanguage =
this.state.fromLanguage this.sanitizeSourceLangTagAsTargetLangTag(this.sourceLanguage.value) ||
); this.state.sourceLanguage;
this.state.setFromLanguage(newFromLanguage);
this.state.setToLanguage(newToLanguage);
this.languageFrom.value = newFromLanguage; this.state.setSourceLanguage(newSourceLanguage);
this.languageTo.value = newToLanguage; this.state.setTargetLanguage(newTargetLanguage);
this.sourceLanguage.value = newSourceLanguage;
this.targetLanguage.value = newTargetLanguage;
await this.updateOnLanguageChange(); await this.updateOnLanguageChange();
this.translationTo.setAttribute("lang", this.languageTo.value); this.translationTo.setAttribute("lang", this.targetLanguage.value);
this.translationFrom.value = translationToValue; this.translationFrom.value = translationToValue;
this.state.setMessageToTranslate(translationToValue); this.state.setMessageToTranslate(translationToValue);
@@ -539,7 +582,7 @@ class TranslationsUI {
* @returns {boolean} * @returns {boolean}
*/ */
detectOptionIsSelected() { detectOptionIsSelected() {
return this.languageFrom.value === "detect"; return this.sourceLanguage.value === "detect";
} }
/** /**
@@ -588,23 +631,23 @@ class TranslationsUI {
* if this is the case. * if this is the case.
*/ */
#updateDropdownLanguages() { #updateDropdownLanguages() {
for (const option of this.languageFrom.options) { for (const option of this.sourceLanguage.options) {
option.hidden = false; option.hidden = false;
} }
for (const option of this.languageTo.options) { for (const option of this.targetLanguage.options) {
option.hidden = false; option.hidden = false;
} }
if (this.state.toLanguage) { if (this.state.targetLanguage) {
const option = this.languageFrom.querySelector( const option = this.sourceLanguage.querySelector(
`[value=${this.state.toLanguage}]` `[value=${this.state.targetLanguage}]`
); );
if (option) { if (option) {
option.hidden = true; option.hidden = true;
} }
} }
if (this.state.fromLanguage) { if (this.state.sourceLanguage) {
const option = this.languageTo.querySelector( const option = this.targetLanguage.querySelector(
`[value=${this.state.fromLanguage}]` `[value=${this.state.sourceLanguage}]`
); );
if (option) { if (option) {
option.hidden = true; option.hidden = true;
@@ -636,18 +679,18 @@ class TranslationsUI {
* The effects are similar, but reversed for RTL text in an LTR UI. * The effects are similar, but reversed for RTL text in an LTR UI.
*/ */
#updateMessageDirections() { #updateMessageDirections() {
if (this.state.toLanguage) { if (this.state.targetLanguage) {
this.translationTo.setAttribute( this.translationTo.setAttribute(
"dir", "dir",
AT_getScriptDirection(this.state.toLanguage) AT_getScriptDirection(this.state.targetLanguage)
); );
} else { } else {
this.translationTo.removeAttribute("dir"); this.translationTo.removeAttribute("dir");
} }
if (this.state.fromLanguage) { if (this.state.sourceLanguage) {
this.translationFrom.setAttribute( this.translationFrom.setAttribute(
"dir", "dir",
AT_getScriptDirection(this.state.fromLanguage) AT_getScriptDirection(this.state.sourceLanguage)
); );
} else { } else {
this.translationFrom.removeAttribute("dir"); this.translationFrom.removeAttribute("dir");
@@ -655,11 +698,11 @@ class TranslationsUI {
} }
/** /**
* Disable the language swap button if fromLanguage is equivalent to toLanguage, or if the languages are not a valid option in the opposite direction * Disable the language swap button if sourceLanguage is equivalent to targetLanguage, or if the languages are not a valid option in the opposite direction
*/ */
async #updateLanguageSwapButton() { async #updateLanguageSwapButton() {
const sourceLanguage = this.state.fromLanguage; const sourceLanguage = this.state.sourceLanguage;
const targetLanguage = this.state.toLanguage; const targetLanguage = this.state.targetLanguage;
if ( if (
sourceLanguage === sourceLanguage ===
@@ -679,12 +722,12 @@ class TranslationsUI {
const isSourceLanguageValidAsTargetLanguage = const isSourceLanguageValidAsTargetLanguage =
sourceLanguage === "detect" || sourceLanguage === "detect" ||
supportedLanguages.languagePairs.some( supportedLanguages.languagePairs.some(
({ toLang }) => toLang === sourceLanguage ({ targetLanguage }) => targetLanguage === sourceLanguage
); );
const isTargetLanguageValidAsSourceLanguage = const isTargetLanguageValidAsSourceLanguage =
targetLanguage === "" || targetLanguage === "" ||
supportedLanguages.languagePairs.some( supportedLanguages.languagePairs.some(
({ fromLang }) => fromLang === targetLanguage ({ sourceLanguage }) => sourceLanguage === targetLanguage
); );
this.languageSwap.disabled = this.languageSwap.disabled =
@@ -702,8 +745,8 @@ class TranslationsUI {
disableUI() { disableUI() {
this.translationFrom.disabled = true; this.translationFrom.disabled = true;
this.languageFrom.disabled = true; this.sourceLanguage.disabled = true;
this.languageTo.disabled = true; this.targetLanguage.disabled = true;
this.languageSwap.disabled = true; this.languageSwap.disabled = true;
} }

View File

@@ -32,7 +32,9 @@ add_task(async function test_pivot_language_behavior() {
// Sort the language pairs, as the order is not guaranteed. // Sort the language pairs, as the order is not guaranteed.
function sort(list) { function sort(list) {
return list.sort((a, b) => return list.sort((a, b) =>
`${a.fromLang}-${a.toLang}`.localeCompare(`${b.fromLang}-${b.toLang}`) `${a.fromLang ?? a.sourceLanguage}-${a.toLang ?? a.targetLanguage}`.localeCompare(
`${b.fromLang ?? b.sourceLanguage}-${b.toLang ?? b.targetLanguage}`
)
); );
} }
@@ -44,8 +46,12 @@ add_task(async function test_pivot_language_behavior() {
// The pairs aren't guaranteed to be sorted. // The pairs aren't guaranteed to be sorted.
languagePairs.sort((a, b) => languagePairs.sort((a, b) =>
TranslationsParent.languagePairKey(a.fromLang, a.toLang).localeCompare( TranslationsParent.nonPivotKey(
TranslationsParent.languagePairKey(b.fromLang, b.toLang) a.fromLang,
a.toLang,
a.variant
).localeCompare(
TranslationsParent.nonPivotKey(b.fromLang, b.toLang, b.variant)
) )
); );
@@ -53,18 +59,24 @@ add_task(async function test_pivot_language_behavior() {
Assert.deepEqual( Assert.deepEqual(
sort(languagePairs), sort(languagePairs),
sort([ sort([
{ fromLang: "en", toLang: "es" }, { sourceLanguage: "en", targetLanguage: "es", variant: undefined },
{ fromLang: "en", toLang: "yue" }, { sourceLanguage: "en", targetLanguage: "yue", variant: undefined },
{ fromLang: "es", toLang: "en" }, { sourceLanguage: "es", targetLanguage: "en", variant: undefined },
{ fromLang: "is", toLang: "en" }, { sourceLanguage: "is", targetLanguage: "en", variant: undefined },
{ fromLang: "yue", toLang: "en" }, { sourceLanguage: "yue", targetLanguage: "en", variant: undefined },
]), ]),
"Non-pivot languages were removed on debug builds." "Non-pivot languages were removed on debug builds."
); );
} else { } else {
Assert.deepEqual( Assert.deepEqual(
sort(languagePairs), sort(languagePairs),
sort(fromLanguagePairs), sort(
fromLanguagePairs.map(({ fromLang, toLang }) => ({
sourceLanguage: fromLang,
targetLanguage: toLang,
varient: undefined,
}))
),
"Non-pivot languages are retained on non-debug builds." "Non-pivot languages are retained on non-debug builds."
); );
} }
@@ -93,29 +105,33 @@ add_task(async function test_language_support_checks() {
}); });
const { languagePairs } = await TranslationsParent.getSupportedLanguages(); const { languagePairs } = await TranslationsParent.getSupportedLanguages();
for (const { fromLang, toLang } of languagePairs) { for (const { sourceLanguage, targetLanguage } of languagePairs) {
ok( ok(
await TranslationsParent.findCompatibleSourceLangTag(fromLang), await TranslationsParent.findCompatibleSourceLangTag(sourceLanguage),
"Each from-language should be supported as a translation source language." "Each from-language should be supported as a translation source language."
); );
ok( ok(
await TranslationsParent.findCompatibleTargetLangTag(toLang), await TranslationsParent.findCompatibleTargetLangTag(targetLanguage),
"Each to-language should be supported as a translation target language." "Each to-language should be supported as a translation target language."
); );
is( is(
Boolean(await TranslationsParent.findCompatibleTargetLangTag(fromLang)), Boolean(
languagePairs.some(({ toLang }) => await TranslationsParent.findCompatibleTargetLangTag(sourceLanguage)
TranslationsUtils.langTagsMatch(toLang, fromLang) ),
languagePairs.some(({ targetLanguage }) =>
TranslationsUtils.langTagsMatch(sourceLanguage, targetLanguage)
), ),
"A from-language should be supported as a to-language if it also exists in the to-language list." "A from-language should be supported as a to-language if it also exists in the to-language list."
); );
is( is(
Boolean(await TranslationsParent.findCompatibleSourceLangTag(toLang)), Boolean(
languagePairs.some(({ fromLang }) => await TranslationsParent.findCompatibleSourceLangTag(targetLanguage)
TranslationsUtils.langTagsMatch(fromLang, toLang) ),
languagePairs.some(({ sourceLanguage }) =>
TranslationsUtils.langTagsMatch(sourceLanguage, targetLanguage)
), ),
"A to-language should be supported as a from-language if it also exists in the from-language list." "A to-language should be supported as a from-language if it also exists in the from-language list."
); );
@@ -212,8 +228,8 @@ add_task(async function test_translating_to_and_from_app_language() {
*/ */
function getUniqueLanguagePairs(records) { function getUniqueLanguagePairs(records) {
const langPairs = new Set(); const langPairs = new Set();
for (const { fromLang, toLang } of records) { for (const { fromLang, toLang, variant } of records) {
langPairs.add(TranslationsParent.languagePairKey(fromLang, toLang)); langPairs.add(TranslationsParent.nonPivotKey(fromLang, toLang, variant));
} }
return Array.from(langPairs) return Array.from(langPairs)
.sort() .sort()

View File

@@ -17,7 +17,10 @@ add_task(async function test_translations_actor_sync_update_wasm() {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const { bergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(bergamotWasmArrayBuffer), decoder.decode(bergamotWasmArrayBuffer),
@@ -45,7 +48,10 @@ add_task(async function test_translations_actor_sync_update_wasm() {
}); });
const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(updatedBergamotWasmArrayBuffer), decoder.decode(updatedBergamotWasmArrayBuffer),
@@ -67,7 +73,10 @@ add_task(async function test_translations_actor_sync_delete_wasm() {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const { bergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(bergamotWasmArrayBuffer), decoder.decode(bergamotWasmArrayBuffer),
@@ -91,11 +100,12 @@ add_task(async function test_translations_actor_sync_delete_wasm() {
}); });
let errorMessage; let errorMessage;
await TranslationsParent.getTranslationsEnginePayload("en", "es").catch( await TranslationsParent.getTranslationsEnginePayload({
error => { sourceLanguage: "en",
targetLanguage: "es",
}).catch(error => {
errorMessage = error?.message; errorMessage = error?.message;
} });
);
is( is(
errorMessage, errorMessage,
@@ -118,7 +128,10 @@ add_task(
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const { bergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(bergamotWasmArrayBuffer), decoder.decode(bergamotWasmArrayBuffer),
@@ -135,7 +148,10 @@ add_task(
}); });
const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(updatedBergamotWasmArrayBuffer), decoder.decode(updatedBergamotWasmArrayBuffer),
@@ -159,7 +175,10 @@ add_task(
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const { bergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(bergamotWasmArrayBuffer), decoder.decode(bergamotWasmArrayBuffer),
@@ -178,7 +197,10 @@ add_task(
}); });
const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(updatedBergamotWasmArrayBuffer), decoder.decode(updatedBergamotWasmArrayBuffer),
@@ -202,7 +224,10 @@ add_task(async function test_translations_actor_sync_rollback_wasm() {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const { bergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(bergamotWasmArrayBuffer), decoder.decode(bergamotWasmArrayBuffer),
@@ -219,7 +244,10 @@ add_task(async function test_translations_actor_sync_rollback_wasm() {
}); });
const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(updatedBergamotWasmArrayBuffer), decoder.decode(updatedBergamotWasmArrayBuffer),
@@ -233,7 +261,10 @@ add_task(async function test_translations_actor_sync_rollback_wasm() {
}); });
const { bergamotWasmArrayBuffer: rolledBackBergamotWasmArrayBuffer } = const { bergamotWasmArrayBuffer: rolledBackBergamotWasmArrayBuffer } =
await TranslationsParent.getTranslationsEnginePayload("en", "es"); await TranslationsParent.getTranslationsEnginePayload({
sourceLanguage: "en",
targetLanguage: "es",
});
is( is(
decoder.decode(rolledBackBergamotWasmArrayBuffer), decoder.decode(rolledBackBergamotWasmArrayBuffer),

View File

@@ -143,9 +143,10 @@ add_task(async function test_get_records_with_multiple_versions() {
); );
const lookupKey = record => const lookupKey = record =>
`${record.name}${TranslationsParent.languagePairKey( `${record.name}${TranslationsParent.nonPivotKey(
record.fromLang, record.fromLang,
record.toLang record.toLang,
record.variant
)}`; )}`;
// A mapping of each record name to its max version. // A mapping of each record name to its max version.

View File

@@ -1004,6 +1004,9 @@ async function loadTestPage({
prefs, prefs,
autoOffer, autoOffer,
permissionsUrls, permissionsUrls,
systemLocales = ["en"],
appLocales,
webLanguages,
win = window, win = window,
}) { }) {
info(`Loading test page starting at url: ${page}`); info(`Loading test page starting at url: ${page}`);
@@ -1069,6 +1072,15 @@ async function loadTestPage({
TranslationsParent.testAutomaticPopup = true; TranslationsParent.testAutomaticPopup = true;
} }
let cleanupLocales;
if (systemLocales || appLocales || webLanguages) {
cleanupLocales = await mockLocales({
systemLocales,
appLocales,
webLanguages,
});
}
// Start the tab at a blank page. // Start the tab at a blank page.
const tab = await BrowserTestUtils.openNewForegroundTab( const tab = await BrowserTestUtils.openNewForegroundTab(
win.gBrowser, win.gBrowser,
@@ -1175,6 +1187,9 @@ async function loadTestPage({
await loadBlankPage(); await loadBlankPage();
await EngineProcess.destroyTranslationsEngine(); await EngineProcess.destroyTranslationsEngine();
await removeMocks(); await removeMocks();
if (cleanupLocales) {
await cleanupLocales();
}
restoreA11yUtils(); restoreA11yUtils();
Services.fog.testResetFOG(); Services.fog.testResetFOG();
TranslationsParent.testAutomaticPopup = false; TranslationsParent.testAutomaticPopup = false;

View File

@@ -36,6 +36,9 @@ export interface TranslationModelRecord {
fromLang: string; fromLang: string;
// The BCP 47 language tag, e.g. "en" // The BCP 47 language tag, e.g. "en"
toLang: string; toLang: string;
// A model variant. This is a developer-only property that can be used in Nightly or
// local builds to test different types of models.
variant?: string;
// The semver number, used for handling future format changes. e.g. 1.0 // The semver number, used for handling future format changes. e.g. 1.0
version: string; version: string;
// e.g. "lex" // e.g. "lex"
@@ -209,6 +212,7 @@ interface LanguageTranslationModelFile {
interface TranslationModelPayload { interface TranslationModelPayload {
sourceLanguage: string, sourceLanguage: string,
targetLanguage: string, targetLanguage: string,
variant?: string,
languageModelFiles: LanguageTranslationModelFiles, languageModelFiles: LanguageTranslationModelFiles,
}; };
@@ -264,16 +268,43 @@ export interface LangTags {
userLangTag: string | null, userLangTag: string | null,
} }
export interface LanguagePair { fromLang: string, toLang: string }; /**
* All of the necessary information to pick models for doing a translation. This pair
* should be solvable by picking model variants, and pivoting through English.
*/
export interface LanguagePair {
sourceLanguage: string,
targetLanguage: string,
sourceVariant?: string,
targetVariant?: string
};
/**
* In the case of a single model, there will only be a single potential model variant.
* A LanguagePair can resolve into 1 or 2 NonPivotLanguagePair depending on the pivoting
* needs and how they are resolved.
*/
export interface NonPivotLanguagePair {
sourceLanguage: string,
targetLanguage: string,
variant?: string,
}
export interface SupportedLanguage {
langTag: string,
langTagKey: string,
variant: string
displayName: string,
}
/** /**
* A structure that contains all of the information needed to render dropdowns * A structure that contains all of the information needed to render dropdowns
* for translation language selection. * for translation language selection.
*/ */
export interface SupportedLanguages { export interface SupportedLanguages {
languagePairs: LanguagePair[], languagePairs: NonPivotLanguagePair[],
fromLanguages: Array<{ langTag: string, displayName: string, }>, sourceLanguages: Array<SupportedLanguage>,
toLanguages: Array<{ langTag: string, displayName: string }>, targetLanguages: Array<SupportedLanguage>,
} }
export type TranslationErrors = "engine-load-error"; export type TranslationErrors = "engine-load-error";
@@ -283,23 +314,25 @@ export type SelectTranslationsPanelState =
| { phase: "closed"; } | { phase: "closed"; }
// The panel is idle after successful initialization and ready to attempt translation. // The panel is idle after successful initialization and ready to attempt translation.
| { phase: "idle"; fromLanguage: string; toLanguage: string, sourceText: string, } | { phase: "idle"; sourceLanguage: string; targetLanguage: string, sourceText: string, }
// The language dropdown menus failed to populate upon opening the panel. // The language dropdown menus failed to populate upon opening the panel.
// This state contains all of the information for the try-again button to close and re-open the panel. // This state contains all of the information for the try-again button to close and re-open the panel.
| { phase: "init-failure"; event: Event, screenX: number, screenY: number, sourceText: string, isTextSelected: boolean, langPairPromise: Promise<{fromLang?: string, toLang?: string}> } | { phase: "init-failure"; event: Event, screenX: number, screenY: number, sourceText: string, isTextSelected: boolean, langPairPromise: Promise<{sourceLanguage?: string, targetLanguage?: string}> }
// The translation failed to complete. // The translation failed to complete.
| { phase: "translation-failure"; fromLanguage: string; toLanguage: string, sourceText: string, } | { phase: "translation-failure"; sourceLanguage: string; targetLanguage: string, sourceText: string, }
// The selected language pair is determined to be translatable. // The selected language pair is determined to be translatable.
| { phase: "translatable"; fromLanguage: string; toLanguage: string, sourceText: string, } | { phase: "translatable"; sourceLanguage: string; targetLanguage: string, sourceText: string, }
// The panel is actively translating the source text. // The panel is actively translating the source text.
| { phase: "translating"; fromLanguage: string; toLanguage: string, sourceText: string, } | { phase: "translating"; sourceLanguage: string; targetLanguage: string, sourceText: string, }
// The source text has been translated successfully. // The source text has been translated successfully.
| { phase: "translated"; fromLanguage: string; toLanguage: string, sourceText: string, translatedText: string, } | { phase: "translated"; sourceLanguage: string; targetLanguage: string, sourceText: string, translatedText: string, }
// The source language is not currently supported by Translations in Firefox. // The source language is not currently supported by Translations in Firefox.
| { phase: "unsupported"; detectedLanguage: string; toLanguage: string, sourceText: string } | { phase: "unsupported"; detectedLanguage: string; targetLanguage: string, sourceText: string }
export type RequestTranslationsPort = (languagePair: LanguagePair) => Promise<MessagePort>