diff --git a/browser/base/content/nsContextMenu.sys.mjs b/browser/base/content/nsContextMenu.sys.mjs index 8e126c7023d7..f60d2eb7f857 100644 --- a/browser/base/content/nsContextMenu.sys.mjs +++ b/browser/base/content/nsContextMenu.sys.mjs @@ -87,7 +87,7 @@ export class nsContextMenu { * A promise to retrieve the translations language pair * if the context menu was opened in a context relevant to * open the SelectTranslationsPanel. - * @type {Promise<{fromLanguage: string, toLanguage: string}>} + * @type {Promise<{sourceLanguage: string, targetLanguage: string}>} */ #translationsLangPairPromise; @@ -2570,22 +2570,22 @@ export class nsContextMenu { * @returns {Promise} */ 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. let displayName; try { const languageDisplayNames = lazy.TranslationsParent.createLanguageDisplayNames(); - displayName = languageDisplayNames.of(toLanguage); + displayName = languageDisplayNames.of(targetLanguage); } catch { // languageDisplayNames.of threw, do nothing. } if (displayName) { - translateSelectionItem.setAttribute("target-language", toLanguage); + translateSelectionItem.setAttribute("target-language", targetLanguage); this.document.l10n.setAttributes( translateSelectionItem, this.isTextSelected diff --git a/browser/components/preferences/translations.js b/browser/components/preferences/translations.js index e0115556699e..9e8348aff18a 100644 --- a/browser/components/preferences/translations.js +++ b/browser/components/preferences/translations.js @@ -4,6 +4,10 @@ /* import-globals-from preferences.js */ +/** + * @typedef {import("../../../toolkit/components/translations/translations").SupportedLanguages} SupportedLanguages + */ + /** * The permission type to give to Services.perms for Translations. */ @@ -53,9 +57,9 @@ let gTranslationsPane = { downloadPhases: new Map(), /** - * Object with details of languages supported by the browser namely - * languagePairs, fromLanguages, toLanguages - * @type {object} supportedLanguages + * Object with details of languages supported by the browser. + * + * @type {SupportedLanguages} */ supportedLanguages: {}, @@ -167,10 +171,10 @@ let gTranslationsPane = { * Never translate settings list. */ buildLanguageDropDowns() { - const { fromLanguages } = this.supportedLanguages; + const { sourceLanguages } = this.supportedLanguages; const { alwaysTranslateMenuPopup, neverTranslateMenuPopup } = this.elements; - for (const { langTag, displayName } of fromLanguages) { + for (const { langTag, displayName } of sourceLanguages) { const alwaysLang = document.createXULElement("menuitem"); alwaysLang.setAttribute("value", langTag); alwaysLang.setAttribute("label", displayName); diff --git a/browser/components/translations/content/TranslationsPanelShared.sys.mjs b/browser/components/translations/content/TranslationsPanelShared.sys.mjs index 8e59ed95ad17..0bfec642950e 100644 --- a/browser/components/translations/content/TranslationsPanelShared.sys.mjs +++ b/browser/components/translations/content/TranslationsPanelShared.sys.mjs @@ -2,6 +2,11 @@ * 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/. */ +/** + * @typedef {typeof import("../../../../toolkit/components/translations/actors/TranslationsParent.sys.mjs").TranslationsParent} TranslationsParent + */ + +/** @type {{ TranslationsParent: TranslationsParent }} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -173,7 +178,7 @@ export class TranslationsPanelShared { ); } /** @type {SupportedLanguages} */ - const { languagePairs, fromLanguages, toLanguages } = + const { languagePairs, sourceLanguages, targetLanguages } = await lazy.TranslationsParent.getSupportedLanguages(); // Verify that we are in a proper state. @@ -199,9 +204,9 @@ export class TranslationsPanelShared { while (popup.lastChild?.value) { popup.lastChild.remove(); } - for (const { langTag, displayName } of fromLanguages) { + for (const { langTagKey, displayName } of sourceLanguages) { const fromMenuItem = document.createXULElement("menuitem"); - fromMenuItem.setAttribute("value", langTag); + fromMenuItem.setAttribute("value", langTagKey); fromMenuItem.setAttribute("label", displayName); popup.appendChild(fromMenuItem); } @@ -211,9 +216,9 @@ export class TranslationsPanelShared { while (popup.lastChild?.value) { popup.lastChild.remove(); } - for (const { langTag, displayName } of toLanguages) { + for (const { langTagKey, displayName } of targetLanguages) { const toMenuItem = document.createXULElement("menuitem"); - toMenuItem.setAttribute("value", langTag); + toMenuItem.setAttribute("value", langTagKey); toMenuItem.setAttribute("label", displayName); popup.appendChild(toMenuItem); } diff --git a/browser/components/translations/content/fullPageTranslationsPanel.js b/browser/components/translations/content/fullPageTranslationsPanel.js index a3eef547153c..294ba86ed472 100644 --- a/browser/components/translations/content/fullPageTranslationsPanel.js +++ b/browser/components/translations/content/fullPageTranslationsPanel.js @@ -444,16 +444,20 @@ var FullPageTranslationsPanel = new (class { this.elements; 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 ( requestedLanguagePair && !isEngineReady && TranslationsUtils.langTagsMatch( - fromMenuList.value, - requestedLanguagePair.fromLanguage + selectedFrom, + requestedLanguagePair.sourceLanguage ) && TranslationsUtils.langTagsMatch( - toMenuList.value, - requestedLanguagePair.toLanguage + selectedTo, + requestedLanguagePair.targetLanguage ) ) { // A translation has been requested, but is not ready yet. @@ -475,29 +479,29 @@ var FullPageTranslationsPanel = new (class { // No "from" language was provided. !fromMenuList.value || // 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. (requestedLanguagePair && TranslationsUtils.langTagsMatch( - requestedLanguagePair.fromLanguage, - fromMenuList.value + requestedLanguagePair.sourceLanguage, + selectedFrom ) && TranslationsUtils.langTagsMatch( - requestedLanguagePair.toLanguage, - toMenuList.value + requestedLanguagePair.targetLanguage, + selectedTo )); } if (requestedLanguagePair && isEngineReady) { - const { fromLanguage, toLanguage } = requestedLanguagePair; + const { sourceLanguage, targetLanguage } = requestedLanguagePair; const languageDisplayNames = TranslationsParent.createLanguageDisplayNames(); cancelButton.hidden = true; this.updateUIForReTranslation(true /* isReTranslation */); document.l10n.setAttributes(header, "translations-panel-revisit-header", { - fromLanguage: languageDisplayNames.of(fromLanguage), - toLanguage: languageDisplayNames.of(toLanguage), + fromLanguage: languageDisplayNames.of(sourceLanguage), + toLanguage: languageDisplayNames.of(targetLanguage), }); } else { document.l10n.setAttributes(header, "translations-panel-header"); @@ -588,6 +592,8 @@ var FullPageTranslationsPanel = new (class { fromMenuList.value = ""; error.hidden = true; langSelection.hidden = false; + // Remove the model variant. e.g. "ru,base" -> "ru" + const selectedSource = fromMenuList.value.split(",")[0]; const { userLangTag, docLangTag, isDocLangTagSupported } = await this.#fetchDetectedLanguages().then(langTags => langTags ?? {}); @@ -626,14 +632,15 @@ var FullPageTranslationsPanel = new (class { // Avoid offering to translate into the original source language. docLangTag, // Avoid same-language to same-language translations if possible. - fromMenuList.value, + selectedSource, ], }); } - if ( - TranslationsUtils.langTagsMatch(fromMenuList.value, toMenuList.value) - ) { + const resolvedSource = fromMenuList.value.split(",")[0]; + 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 // 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 @@ -848,9 +855,9 @@ var FullPageTranslationsPanel = new (class { /** * 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; if (!this.#isShowingDefaultView()) { await this.#showDefaultView( @@ -858,13 +865,17 @@ var FullPageTranslationsPanel = new (class { ); } intro.hidden = true; - fromMenuList.value = fromLanguage; + if (sourceVariant) { + fromMenuList.value = `${sourceLanguage},${sourceVariant}`; + } else { + fromMenuList.value = sourceLanguage; + } toMenuList.value = await TranslationsParent.getTopPreferredSupportedToLang({ excludeLangTags: [ // Avoid offering to translate into the original source language. - fromLanguage, + sourceLanguage, // Avoid offering to translate into current active target language. - toLanguage, + targetLanguage, ], }); this.onChangeLanguages(); @@ -1241,9 +1252,13 @@ var FullPageTranslationsPanel = new (class { const actor = TranslationsParent.getTranslationsActor( gBrowser.selectedBrowser ); + const [sourceLanguage, sourceVariant] = + this.elements.fromMenuList.value.split(","); + const [targetLanguage, targetVariant] = + this.elements.toMenuList.value.split(","); + actor.translate( - this.elements.fromMenuList.value, - this.elements.toMenuList.value, + { sourceLanguage, targetLanguage, sourceVariant, targetVariant }, false // reportAsAutoTranslate ); } @@ -1604,10 +1619,10 @@ var FullPageTranslationsPanel = new (class { "urlbar-translations-button-translated", { fromLanguage: languageDisplayNames.of( - requestedLanguagePair.fromLanguage + requestedLanguagePair.sourceLanguage ), toLanguage: languageDisplayNames.of( - requestedLanguagePair.toLanguage + requestedLanguagePair.targetLanguage ), } ); @@ -1615,7 +1630,7 @@ var FullPageTranslationsPanel = new (class { buttonLocale.hidden = false; buttonCircleArrows.hidden = true; buttonLocale.innerText = - requestedLanguagePair.toLanguage.split("-")[0]; + requestedLanguagePair.targetLanguage.split("-")[0]; } else { document.l10n.setAttributes( button, diff --git a/browser/components/translations/content/selectTranslationsPanel.js b/browser/components/translations/content/selectTranslationsPanel.js index 11b1aec4e701..2cee4ba91a9c 100644 --- a/browser/components/translations/content/selectTranslationsPanel.js +++ b/browser/components/translations/content/selectTranslationsPanel.js @@ -6,6 +6,7 @@ /** * @typedef {import("../../../../toolkit/components/translations/translations").SelectTranslationsPanelState} SelectTranslationsPanelState + * @typedef {import("../../../../toolkit/components/translations/translations").LanguagePair} LanguagePair */ ChromeUtils.defineESModuleGetters(this, { @@ -14,7 +15,7 @@ ChromeUtils.defineESModuleGetters(this, { TranslationsPanelShared: "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs", TranslationsUtils: - "chrome://global/content/translations/TranslationsUtils.sys.mjs", + "chrome://global/content/translations/TranslationsUtils.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. const { language, languages } = await LanguageDetector.detectLanguage(textToTranslate); - const languagePairs = await TranslationsParent.getLanguagePairs(); + const languagePairs = await TranslationsParent.getNonPivotLanguagePairs(); for (const { languageCode } of languages) { const compatibleLangTag = TranslationsParent.findCompatibleSourceLangTagSync( @@ -409,8 +410,8 @@ var SelectTranslationsPanel = new (class { * based on user settings. * * @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. - * The `fromLanguage` property is omitted if it is a language that is not currently supported by Firefox Translations. + * @returns {Promise<{sourceLanguage?: string, targetLanguage?: string}>} - An object containing the language pair for the translation. + * The `sourceLanguage` property is omitted if it is a language that is not currently supported by Firefox Translations. */ async getLangPairPromise(textToTranslate) { if ( @@ -423,19 +424,20 @@ var SelectTranslationsPanel = new (class { // 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 // 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( textToTranslate ); - const toLanguage = await TranslationsParent.getTopPreferredSupportedToLang({ - // Avoid offering a same-language to same-language translation if we can. - excludeLangTags: [fromLanguage], - }); + const targetLanguage = + await TranslationsParent.getTopPreferredSupportedToLang({ + // Avoid offering a same-language to same-language translation if we can. + 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 * 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} */ async #initializeLanguageMenuLists(langPairPromise) { - const { fromLanguage, toLanguage } = await langPairPromise; + const { sourceLanguage, targetLanguage } = await langPairPromise; const { fromMenuList, fromMenuPopup, @@ -500,8 +502,8 @@ var SelectTranslationsPanel = new (class { } = this.elements; await Promise.all([ - this.#initializeLanguageMenuList(fromLanguage, fromMenuList), - this.#initializeLanguageMenuList(toLanguage, toMenuList), + this.#initializeLanguageMenuList(sourceLanguage, fromMenuList), + this.#initializeLanguageMenuList(targetLanguage, toMenuList), this.#initializeLanguageMenuList(null, tryAnotherSourceMenuList), ]); @@ -576,7 +578,7 @@ var SelectTranslationsPanel = new (class { return; } - const { fromLanguage, toLanguage } = await langPairPromise; + const { sourceLanguage, targetLanguage } = await langPairPromise; const { docLangTag, topPreferredLanguage } = this.#getLanguageInfo(); TranslationsParent.telemetry() @@ -584,8 +586,8 @@ var SelectTranslationsPanel = new (class { .onOpen({ maintainFlow, docLangTag, - fromLanguage, - toLanguage, + sourceLanguage, + targetLanguage, topPreferredLanguage, textSource: isTextSelected ? "selection" : "hyperlink", }); @@ -627,7 +629,7 @@ var SelectTranslationsPanel = new (class { const { requestedLanguagePair } = TranslationsParent.getTranslationsActor( gBrowser.selectedBrowser ).languageState; - return requestedLanguagePair?.toLanguage; + return requestedLanguagePair?.targetLanguage; } catch { this.console.warn("Failed to retrieve the TranslationsParent actor."); } @@ -718,27 +720,27 @@ var SelectTranslationsPanel = new (class { * on the length of the text. * * @param {string} sourceText - The text to translate. - * @param {Promise<{fromLanguage?: string, toLanguage?: string}>} langPairPromise + * @param {Promise<{sourceLanguage?: string, targetLanguage?: string}>} langPairPromise * * @returns {Promise} */ async #registerSourceText(sourceText, langPairPromise) { const { textArea } = this.elements; - const { fromLanguage, toLanguage } = await langPairPromise; + const { sourceLanguage, targetLanguage } = await langPairPromise; const compatibleFromLang = - await TranslationsParent.findCompatibleSourceLangTag(fromLanguage); + await TranslationsParent.findCompatibleSourceLangTag(sourceLanguage); if (compatibleFromLang) { this.#changeStateTo("idle", /* retainEntries */ false, { sourceText, - fromLanguage: compatibleFromLang, - toLanguage, + sourceLanguage: compatibleFromLang, + targetLanguage, }); } else { this.#changeStateTo("unsupported", /* retainEntries */ false, { sourceText, - detectedLanguage: fromLanguage, - toLanguage, + detectedLanguage: sourceLanguage, + targetLanguage, }); } @@ -1286,14 +1288,14 @@ var SelectTranslationsPanel = new (class { */ onClickTranslateButton() { const { fromMenuList, tryAnotherSourceMenuList } = this.elements; - const { detectedLanguage, toLanguage } = this.#translationState; + const { detectedLanguage, targetLanguage } = this.#translationState; fromMenuList.value = tryAnotherSourceMenuList.value; TranslationsParent.telemetry().selectTranslationsPanel().onTranslateButton({ detectedLanguage, - fromLanguage: fromMenuList.value, - toLanguage, + sourceLanguage: fromMenuList.value, + targetLanguage, }); this.#maybeRequestTranslation(); @@ -1308,7 +1310,7 @@ var SelectTranslationsPanel = new (class { .onTranslateFullPageButton(); const { panel } = this.elements; - const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); + const languagePair = this.#getSelectedLanguagePair(); try { const actor = TranslationsParent.getTranslationsActor( @@ -1318,8 +1320,7 @@ var SelectTranslationsPanel = new (class { "popuphidden", () => actor.translate( - fromLanguage, - toLanguage, + languagePair, false // reportAsAutoTranslate ), { once: true } @@ -1468,30 +1469,36 @@ var SelectTranslationsPanel = new (class { /** * Checks if the given language pair matches the panel's currently selected language pair. * - * @param {string} fromLanguage - The from-language to compare. - * @param {string} toLanguage - The to-language to compare. + * @param {string} sourceLanguage - The from-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. */ - #isSelectedLangPair(fromLanguage, toLanguage) { - const { fromLanguage: selectedFromLang, toLanguage: selectedToLang } = - this.#getSelectedLanguagePair(); + #isSelectedLangPair(sourceLanguage, targetLanguage) { + const selected = this.#getSelectedLanguagePair(); return ( - TranslationsUtils.langTagsMatch(fromLanguage, selectedFromLang) && - TranslationsUtils.langTagsMatch(toLanguage, selectedToLang) + TranslationsUtils.langTagsMatch( + sourceLanguage, + selected.sourceLanguage + ) && + TranslationsUtils.langTagsMatch(targetLanguage, selected.targetLanguage) ); } /** * Retrieves the currently selected language pair from the menu lists. * - * @returns {{fromLanguage: string, toLanguage: string}} An object containing the selected languages. + * @returns {LanguagePair} */ #getSelectedLanguagePair() { const { fromMenuList, toMenuList } = this.elements; + const [sourceLanguage, sourceVariant] = fromMenuList.value.split(","); + const [targetLanguage, targetVariant] = toMenuList.value.split(","); return { - fromLanguage: fromMenuList.value, - toLanguage: toMenuList.value, + sourceLanguage, + targetLanguage, + sourceVariant, + targetVariant, }; } @@ -1584,12 +1591,11 @@ var SelectTranslationsPanel = new (class { return; } - const { fromLanguage, toLanguage, detectedLanguage } = + const { sourceLanguage, targetLanguage, detectedLanguage } = this.#translationState; - const sourceLanguage = fromLanguage ? fromLanguage : detectedLanguage; this.console?.debug( - `SelectTranslationsPanel (${sourceLanguage ? sourceLanguage : "??"}-${ - toLanguage ? toLanguage : "??" + `SelectTranslationsPanel (${sourceLanguage ?? detectedLanguage ?? "??"}-${ + targetLanguage ? targetLanguage : "??" }) state change (${previousPhase} => ${phase})` ); @@ -1681,18 +1687,18 @@ var SelectTranslationsPanel = new (class { * Transitions the phase to "translatable" if the proper conditions are met, * otherwise retains the same phase as before. * - * @param {string} fromLanguage - The BCP-47 from-language tag. - * @param {string} toLanguage - The BCP-47 to-language tag. + * @param {string} sourceLanguage - The BCP-47 from-language tag. + * @param {string} targetLanguage - The BCP-47 to-language tag. */ - #maybeChangeStateToTranslatable(fromLanguage, toLanguage) { - const { - fromLanguage: previousFromLanguage, - toLanguage: previousToLanguage, - } = this.#translationState; + #maybeChangeStateToTranslatable(sourceLanguage, targetLanguage) { + const previous = this.#translationState; const langSelectionChanged = () => - !TranslationsUtils.langTagsMatch(previousFromLanguage, fromLanguage) || - !TranslationsUtils.langTagsMatch(previousToLanguage, toLanguage); + !TranslationsUtils.langTagsMatch( + previous.sourceLanguage, + sourceLanguage + ) || + !TranslationsUtils.langTagsMatch(previous.targetLanguage, targetLanguage); const shouldTranslateEvenIfLangSelectionHasNotChanged = () => { const phase = this.phase(); @@ -1705,18 +1711,18 @@ var SelectTranslationsPanel = new (class { }; if ( - // A valid from-language is actively selected. - fromLanguage && - // A valid to-language is actively selected. - toLanguage && + // A valid source language is actively selected. + sourceLanguage && + // A valid target language is actively selected. + targetLanguage && // The language selection has changed, requiring a new translation. (langSelectionChanged() || // We should try to translate even if the language selection has not changed. shouldTranslateEvenIfLangSelectionHasNotChanged()) ) { this.#changeStateTo("translatable", /* retainEntries */ true, { - fromLanguage, - toLanguage, + sourceLanguage, + targetLanguage, }); } } @@ -1836,19 +1842,19 @@ var SelectTranslationsPanel = new (class { * Determines whether translation should continue based on panel state and language pair. * * @param {number} translationId - The id of the translation request to match. - * @param {string} fromLanguage - The from-language to analyze. - * @param {string} toLanguage - The to-language to analyze. + * @param {string} sourceLanguage - The source language to analyze. + * @param {string} targetLanguage - The target language to analyze. * * @returns {boolean} True if translation should continue with the given pair, otherwise false. */ - #shouldContinueTranslation(translationId, fromLanguage, toLanguage) { + #shouldContinueTranslation(translationId, sourceLanguage, targetLanguage) { return ( // Continue only if the panel is still open. this.#isOpen() && // Continue only if the current translationId matches. translationId === this.#translationId && // 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() { this.#showMainContent(); - const { toLanguage } = this.#getSelectedLanguagePair(); + const { targetLanguage } = this.#getSelectedLanguagePair(); const { textArea } = SelectTranslationsPanel.elements; textArea.value = this.getTranslatedText(); - this.#updateTextDirection(toLanguage); + this.#updateTextDirection(targetLanguage); this.#updateConditionalUIEnabledState(); this.#indicateTranslatedTextArea({ overflow: "auto" }); 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. */ #updateConditionalUIEnabledState() { - const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); + const { sourceLanguage, targetLanguage } = this.#getSelectedLanguagePair(); const { copyButton, textArea, @@ -1929,7 +1935,7 @@ var SelectTranslationsPanel = new (class { tryAnotherSourceMenuList, } = this.elements; - const invalidLangPairSelected = !fromLanguage || !toLanguage; + const invalidLangPairSelected = !sourceLanguage || !targetLanguage; const isTranslating = this.phase() === "translating"; textArea.disabled = invalidLangPairSelected; @@ -1937,7 +1943,7 @@ var SelectTranslationsPanel = new (class { translateButton.disabled = !tryAnotherSourceMenuList.value; translateFullPageButton.disabled = invalidLangPairSelected || - TranslationsUtils.langTagsMatch(fromLanguage, toLanguage) || + TranslationsUtils.langTagsMatch(sourceLanguage, targetLanguage) || this.#shouldHideTranslateFullPageButton(); } @@ -2077,10 +2083,11 @@ var SelectTranslationsPanel = new (class { */ #displayTranslationFailureMessage() { if (this.#mostRecentUIPhase !== "translation-failure") { - const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); + const { sourceLanguage, targetLanguage } = + this.#getSelectedLanguagePair(); TranslationsParent.telemetry() .selectTranslationsPanel() - .onTranslationFailureMessage({ fromLanguage, toLanguage }); + .onTranslationFailureMessage({ sourceLanguage, targetLanguage }); } const { @@ -2184,37 +2191,31 @@ var SelectTranslationsPanel = new (class { /** * Requests a translations port for a given language pair. * - * @param {string} fromLanguage - The from-language. - * @param {string} toLanguage - The to-language. - * + * @param {LanguagePair} languagePair * @returns {Promise} The message port promise. */ - async #requestTranslationsPort(fromLanguage, toLanguage) { - const port = await TranslationsParent.requestTranslationsPort( - fromLanguage, - toLanguage - ); - return port; + async #requestTranslationsPort(languagePair) { + return TranslationsParent.requestTranslationsPort(languagePair); } /** * Retrieves the existing translator for the specified language pair if it matches, * otherwise creates a new translator. * - * @param {string} fromLanguage - The source language code. - * @param {string} toLanguage - The target language code. + * @param {LanguagePair} languagePair * * @returns {Promise} A promise that resolves to a `Translator` instance for the given language pair. */ - async #createTranslator(fromLanguage, toLanguage) { + async #createTranslator(languagePair) { this.console?.log( - `Creating new Translator (${fromLanguage}-${toLanguage})` + `Creating new Translator (${TranslationsUtils.serializeLanguagePair(languagePair)})` ); - const translator = await Translator.create(fromLanguage, toLanguage, { - allowSameLanguage: true, - requestTranslationsPort: this.#requestTranslationsPort, - }); + const translator = await Translator.create( + languagePair, + this.#requestTranslationsPort, + true /* allowSameLanguage */ + ); return translator; } @@ -2227,8 +2228,9 @@ var SelectTranslationsPanel = new (class { return; } - const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); - this.#maybeChangeStateToTranslatable(fromLanguage, toLanguage); + const languagePair = this.#getSelectedLanguagePair(); + const { sourceLanguage, targetLanguage } = languagePair; + this.#maybeChangeStateToTranslatable(sourceLanguage, targetLanguage); if (this.phase() !== "translatable") { return; @@ -2238,15 +2240,15 @@ var SelectTranslationsPanel = new (class { const sourceText = this.getSourceText(); const translationId = ++this.#translationId; - TranslationsParent.storeMostRecentTargetLanguage(toLanguage); + TranslationsParent.storeMostRecentTargetLanguage(targetLanguage); - this.#createTranslator(fromLanguage, toLanguage) + this.#createTranslator(languagePair) .then(translator => { if ( this.#shouldContinueTranslation( translationId, - fromLanguage, - toLanguage + sourceLanguage, + targetLanguage ) ) { this.#changeStateToTranslating(); @@ -2259,8 +2261,8 @@ var SelectTranslationsPanel = new (class { translatedText && this.#shouldContinueTranslation( translationId, - fromLanguage, - toLanguage + sourceLanguage, + targetLanguage ) ) { this.#changeStateToTranslated(translatedText); @@ -2274,20 +2276,20 @@ var SelectTranslationsPanel = new (class { try { if (!this.#sourceTextWordCount) { this.#sourceTextWordCount = TranslationsParent.countWords( - fromLanguage, + sourceLanguage, sourceText ); } } 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. this.console?.warn(error); } TranslationsParent.telemetry().onTranslate({ docLangTag, - fromLanguage, - toLanguage, + sourceLanguage, + targetLanguage, topPreferredLanguage, autoTranslate: false, 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 * than the corresponding language that is stored in the panel's state. */ #maybeReportLanguageChangeToTelemetry() { - const { - fromLanguage: previousFromLanguage, - toLanguage: previousToLanguage, - } = this.#translationState; - const { - fromLanguage: selectedFromLanguage, - toLanguage: selectedToLanguage, - } = this.#getSelectedLanguagePair(); + const previous = this.#translationState; + const selected = this.#getSelectedLanguagePair(); if ( !TranslationsUtils.langTagsMatch( - selectedFromLanguage, - previousFromLanguage + selected.sourceLanguage, + previous.sourceLanguage ) ) { const { docLangTag } = this.#getLanguageInfo(); TranslationsParent.telemetry() .selectTranslationsPanel() .onChangeFromLanguage({ - previousLangTag: previousFromLanguage, - currentLangTag: selectedFromLanguage, + previousLangTag: previous.sourceLanguage, + currentLangTag: selected.sourceLanguage, docLangTag, }); } if ( - !TranslationsUtils.langTagsMatch(selectedToLanguage, previousToLanguage) + !TranslationsUtils.langTagsMatch( + selected.targetLanguage, + previous.targetLanguage + ) ) { TranslationsParent.telemetry() .selectTranslationsPanel() - .onChangeToLanguage(selectedToLanguage); + .onChangeToLanguage(selected.targetLanguage); } } diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_weblanguage_differs_from_app.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_weblanguage_differs_from_app.js index 18079508952e..029c0df5457d 100644 --- a/browser/components/translations/tests/browser/browser_translations_full_page_panel_weblanguage_differs_from_app.js +++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_weblanguage_differs_from_app.js @@ -7,15 +7,12 @@ * Tests what happens when the web languages differ from the app language. */ add_task(async function test_weblanguage_differs_app_locale() { - const cleanupLocales = await mockLocales({ - systemLocales: ["en"], - appLocales: ["en"], - webLanguages: ["fr"], - }); - const { cleanup } = await loadTestPage({ page: ENGLISH_PAGE_URL, languagePairs: LANGUAGE_PAIRS, + systemLocales: ["en"], + appLocales: ["en"], + webLanguages: ["fr"], }); await FullPageTranslationsTestUtils.assertTranslationsButton( @@ -42,5 +39,4 @@ add_task(async function test_weblanguage_differs_app_locale() { await closeAllOpenPanelsAndMenus(); await cleanup(); - await cleanupLocales(); }); diff --git a/toolkit/components/translations/TranslationsTelemetry.sys.mjs b/toolkit/components/translations/TranslationsTelemetry.sys.mjs index 9c2c8bab49f5..edd74730c06e 100644 --- a/toolkit/components/translations/TranslationsTelemetry.sys.mjs +++ b/toolkit/components/translations/TranslationsTelemetry.sys.mjs @@ -110,8 +110,8 @@ export class TranslationsTelemetry { * @param {object} data * @param {boolean} data.autoTranslate * @param {string} data.docLangTag - * @param {string} data.fromLanguage - * @param {string} data.toLanguage + * @param {string} data.sourceLanguage + * @param {string} data.targetLanguage * @param {string} data.topPreferredLanguage * @param {string} data.requestTarget * @param {number} data.sourceTextCodeUnits @@ -121,9 +121,9 @@ export class TranslationsTelemetry { const { autoTranslate, docLangTag, - fromLanguage, + sourceLanguage, requestTarget, - toLanguage, + targetLanguage, topPreferredLanguage, sourceTextCodeUnits, sourceTextWordCount, @@ -132,8 +132,8 @@ export class TranslationsTelemetry { Glean.translations.requestCount[requestTarget ?? "full_page"].add(1); Glean.translations.translationRequest.record({ flow_id: TranslationsTelemetry.getOrCreateFlowId(), - from_language: fromLanguage, - to_language: toLanguage, + from_language: sourceLanguage, + to_language: targetLanguage, auto_translate: autoTranslate, document_language: docLangTag, top_preferred_language: topPreferredLanguage, @@ -415,8 +415,8 @@ class SelectTranslationsPanelTelemetry { * @param {object} data * @param {string} data.docLangTag * @param {boolean} data.maintainFlow - * @param {string} data.fromLanguage - * @param {string} data.toLanguage + * @param {string} data.sourceLanguage + * @param {string} data.targetLanguage * @param {string} data.topPreferredLanguage * @param {string} data.textSource */ @@ -426,8 +426,8 @@ class SelectTranslationsPanelTelemetry { ? TranslationsTelemetry.getOrCreateFlowId() : TranslationsTelemetry.createFlowId(), document_language: data.docLangTag, - from_language: data.fromLanguage, - to_language: data.toLanguage, + from_language: data.sourceLanguage, + to_language: data.targetLanguage, top_preferred_language: data.topPreferredLanguage, text_source: data.textSource, }); @@ -473,19 +473,23 @@ class SelectTranslationsPanelTelemetry { ); } - static onTranslateButton({ detectedLanguage, fromLanguage, toLanguage }) { + static onTranslateButton({ + detectedLanguage, + sourceLanguage, + targetLanguage, + }) { Glean.translationsSelectTranslationsPanel.translateButton.record({ flow_id: TranslationsTelemetry.getOrCreateFlowId(), detected_language: detectedLanguage, - from_language: fromLanguage, - to_language: toLanguage, + from_language: sourceLanguage, + to_language: targetLanguage, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onTranslateButton, { detectedLanguage, - fromLanguage, - toLanguage, + sourceLanguage, + targetLanguage, } ); } @@ -574,14 +578,14 @@ class SelectTranslationsPanelTelemetry { * Records a telemetry event when the translation-failure message is displayed. * * @param {object} data - * @param {string} data.fromLanguage - * @param {string} data.toLanguage + * @param {string} data.sourceLanguage + * @param {string} data.targetLanguage */ static onTranslationFailureMessage(data) { Glean.translationsSelectTranslationsPanel.translationFailureMessage.record({ flow_id: TranslationsTelemetry.getOrCreateFlowId(), - from_language: data.fromLanguage, - to_language: data.toLanguage, + from_language: data.sourceLanguage, + to_language: data.targetLanguage, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onTranslationFailureMessage, diff --git a/toolkit/components/translations/TranslationsUtils.mjs b/toolkit/components/translations/TranslationsUtils.mjs index 8951795e6582..3a03d871d950 100644 --- a/toolkit/components/translations/TranslationsUtils.mjs +++ b/toolkit/components/translations/TranslationsUtils.mjs @@ -2,6 +2,10 @@ * 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/. */ +/** + * @typedef {import("./translations").LanguagePair} LanguagePair + */ + /** * A set of global static utility functions that are useful throughout the * Translations ecosystem within the Firefox code base. @@ -93,4 +97,32 @@ export class TranslationsUtils { 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; + } } diff --git a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs index abbb2e400bb6..3037f89a98ec 100644 --- a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs +++ b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs @@ -19,6 +19,7 @@ ChromeUtils.defineESModuleGetters(lazy, { /** * @typedef {import("./TranslationsChild.sys.mjs").TranslationsEngine} TranslationsEngine * @typedef {import("./TranslationsChild.sys.mjs").SupportedLanguages} SupportedLanguages + * @typedef {import("../translations").LanguagePair} LanguagePair */ /** @@ -54,13 +55,12 @@ export class AboutTranslationsChild extends JSWindowActorChild { receiveMessage({ name, data }) { switch (name) { case "AboutTranslations:SendTranslationsPort": { - const { fromLanguage, toLanguage, port } = data; + const { languagePair, port } = data; const transferables = [port]; this.contentWindow.postMessage( { type: "GetTranslationsPort", - fromLanguage, - toLanguage, + languagePair, port, }, "*", @@ -214,14 +214,12 @@ export class AboutTranslationsChild extends JSWindowActorChild { * created for that pair. The lifecycle of the engine is managed by the * TranslationsEngine. * - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair * @returns {void} */ - AT_createTranslationsPort(fromLanguage, toLanguage) { + AT_createTranslationsPort(languagePair) { this.sendAsyncMessage("AboutTranslations:GetTranslationsPort", { - fromLanguage, - toLanguage, + languagePair, }); } diff --git a/toolkit/components/translations/actors/AboutTranslationsParent.sys.mjs b/toolkit/components/translations/actors/AboutTranslationsParent.sys.mjs index 6c6e01882dae..c6b5c3433162 100644 --- a/toolkit/components/translations/actors/AboutTranslationsParent.sys.mjs +++ b/toolkit/components/translations/actors/AboutTranslationsParent.sys.mjs @@ -57,12 +57,10 @@ export class AboutTranslationsParent extends JSWindowActorParent { return undefined; } - const { fromLanguage, toLanguage } = data; + const { languagePair } = data; try { - const port = await lazy.TranslationsParent.requestTranslationsPort( - fromLanguage, - toLanguage - ); + const port = + await lazy.TranslationsParent.requestTranslationsPort(languagePair); // 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 @@ -71,8 +69,7 @@ export class AboutTranslationsParent extends JSWindowActorParent { this.sendAsyncMessage( "AboutTranslations:SendTranslationsPort", { - fromLanguage, - toLanguage, + languagePair, port, }, [port] // Mark the port as transferable. diff --git a/toolkit/components/translations/actors/TranslationsChild.sys.mjs b/toolkit/components/translations/actors/TranslationsChild.sys.mjs index ad33dc0119cd..880489ddc0d3 100644 --- a/toolkit/components/translations/actors/TranslationsChild.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsChild.sys.mjs @@ -60,24 +60,20 @@ export class TranslationsChild extends JSWindowActorChild { return undefined; } - const { fromLanguage, toLanguage, port, translationsStart } = data; + const { languagePair, port, translationsStart } = data; if ( !TranslationsChild.#translationsCache || - !TranslationsChild.#translationsCache.matches( - fromLanguage, - toLanguage - ) + !TranslationsChild.#translationsCache.matches(languagePair) ) { TranslationsChild.#translationsCache = new lazy.LRUCache( - fromLanguage, - toLanguage + languagePair ); } this.#translatedDoc = new lazy.TranslationsDocument( this.document, - fromLanguage, - toLanguage, + languagePair.sourceLanguage, + languagePair.targetLanguage, this.contentWindow.windowGlobalChild.innerWindowId, port, () => this.sendAsyncMessage("Translations:RequestPort"), diff --git a/toolkit/components/translations/actors/TranslationsEngineChild.sys.mjs b/toolkit/components/translations/actors/TranslationsEngineChild.sys.mjs index d638858e5262..6bcf0d3b26b2 100644 --- a/toolkit/components/translations/actors/TranslationsEngineChild.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsEngineChild.sys.mjs @@ -40,12 +40,11 @@ export class TranslationsEngineChild extends JSWindowActorChild { async receiveMessage({ name, data }) { switch (name) { case "TranslationsEngine:StartTranslation": { - const { fromLanguage, toLanguage, innerWindowId, port } = data; + const { languagePair, innerWindowId, port } = data; const transferables = [port]; const message = { type: "StartTranslation", - fromLanguage, - toLanguage, + languagePair, innerWindowId, port, }; @@ -177,14 +176,12 @@ export class TranslationsEngineChild extends JSWindowActorChild { } /** - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair */ - TE_requestEnginePayload(fromLanguage, toLanguage) { + TE_requestEnginePayload(languagePair) { return this.#convertToContentPromise( this.sendQuery("TranslationsEngine:RequestEnginePayload", { - fromLanguage, - toLanguage, + languagePair, }) ); } diff --git a/toolkit/components/translations/actors/TranslationsEngineParent.sys.mjs b/toolkit/components/translations/actors/TranslationsEngineParent.sys.mjs index 434c1de71d91..0d3f1438e219 100644 --- a/toolkit/components/translations/actors/TranslationsEngineParent.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsEngineParent.sys.mjs @@ -8,6 +8,10 @@ ChromeUtils.defineESModuleGetters(lazy, { 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 * 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); return undefined; case "TranslationsEngine:RequestEnginePayload": { - const { fromLanguage, toLanguage } = data; + const { languagePair } = data; const payloadPromise = - lazy.TranslationsParent.getTranslationsEnginePayload( - fromLanguage, - toLanguage - ); + lazy.TranslationsParent.getTranslationsEnginePayload(languagePair); payloadPromise.catch(error => { lazy.TranslationsParent.telemetry().onError(String(error)); }); @@ -73,12 +74,11 @@ export class TranslationsEngineParent extends JSWindowActorParent { } /** - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair * @param {MessagePort} port * @param {TranslationsParent} [translationsParent] */ - startTranslation(fromLanguage, toLanguage, port, translationsParent) { + startTranslation(languagePair, port, translationsParent) { const innerWindowId = translationsParent?.innerWindowId; if (translationsParent) { this.#translationsParents.set(innerWindowId, translationsParent); @@ -90,8 +90,7 @@ export class TranslationsEngineParent extends JSWindowActorParent { this.sendAsyncMessage( "TranslationsEngine:StartTranslation", { - fromLanguage, - toLanguage, + languagePair, innerWindowId, port, }, diff --git a/toolkit/components/translations/actors/TranslationsParent.sys.mjs b/toolkit/components/translations/actors/TranslationsParent.sys.mjs index f8d9421576d4..05cb26985fc8 100644 --- a/toolkit/components/translations/actors/TranslationsParent.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsParent.sys.mjs @@ -210,18 +210,11 @@ const VERIFY_SIGNATURES_FROM_FS = false; * @typedef {import("../translations").WasmRecord} WasmRecord * @typedef {import("../translations").LangTags} LangTags * @typedef {import("../translations").LanguagePair} LanguagePair + * @typedef {import("../translations").ModelLanguages} ModelLanguages * @typedef {import("../translations").SupportedLanguages} SupportedLanguages * @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 * 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. * - * @type {null | TranslationPair} + * @type {null | LanguagePair} */ translateOnPageReload = null; @@ -476,16 +469,15 @@ export class TranslationsParent extends JSWindowActorParent { if (windowState.translateOnPageReload) { // The actor was recreated after a page reload, start the translation. - const { fromLanguage, toLanguage } = windowState.translateOnPageReload; + const languagePair = windowState.translateOnPageReload; windowState.translateOnPageReload = null; lazy.console.log( - `Translating on a page reload from "${fromLanguage}" to "${toLanguage}".` + `Translating on a page reload from "${lazy.TranslationsUtils.serializeLanguagePair(languagePair)}".` ); this.translate( - fromLanguage, - toLanguage, + languagePair, false // reportAsAutoTranslate ); } @@ -1214,8 +1206,7 @@ export class TranslationsParent extends JSWindowActorParent { /** * Requests a new translations port. * - * @param {string} fromLanguage - The BCP-47 from-language tag. - * @param {string} toLanguage - The BCP-47 to-language tag. + * @param {LanguagePair} languagePair * @param {TranslationsParent} [translationsParent] - A TranslationsParent actor instance. * 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 @@ -1223,11 +1214,7 @@ export class TranslationsParent extends JSWindowActorParent { * * @returns {Promise} The port for communication with the translation engine, or undefined on failure. */ - static async requestTranslationsPort( - fromLanguage, - toLanguage, - translationsParent - ) { + static async requestTranslationsPort(languagePair, translationsParent) { let translationsEngineParent; try { translationsEngineParent = @@ -1241,8 +1228,7 @@ export class TranslationsParent extends JSWindowActorParent { // process and the engine's process. const { port1, port2 } = new MessageChannel(); translationsEngineParent.startTranslation( - fromLanguage, - toLanguage, + languagePair, port1, translationsParent ); @@ -1275,8 +1261,10 @@ export class TranslationsParent extends JSWindowActorParent { if (this.shouldAutoTranslate(detectedLanguages)) { this.translate( - detectedLanguages.docLangTag, - detectedLanguages.userLangTag, + { + sourceLanguage: detectedLanguages.docLangTag, + targetLanguage: detectedLanguages.userLangTag, + }, true // reportAsAutoTranslate ); } else { @@ -1304,16 +1292,14 @@ export class TranslationsParent extends JSWindowActorParent { ); } - const { fromLanguage, toLanguage } = requestedLanguagePair; const port = await TranslationsParent.requestTranslationsPort( - fromLanguage, - toLanguage, + requestedLanguagePair, this ); if (!port) { 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; } @@ -1334,10 +1320,9 @@ export class TranslationsParent extends JSWindowActorParent { } /** - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair */ - static async getTranslationsEnginePayload(fromLanguage, toLanguage) { + static async getTranslationsEnginePayload(languagePair) { const wasmStartTime = Cu.now(); const bergamotWasmArrayBufferPromise = TranslationsParent.#getBergamotWasmArrayBuffer(); @@ -1355,33 +1340,42 @@ export class TranslationsParent extends JSWindowActorParent { const modelStartTime = Cu.now(); - let translationModelPayloads; - const nonPivotPayload = await TranslationsParent.getTranslationModelPayload( - fromLanguage, - toLanguage - ); - - if (nonPivotPayload) { - // A direct translation model was found for the language pair. - translationModelPayloads = [nonPivotPayload]; + /** @type {TranslationModelPayload[]} */ + const translationModelPayloads = []; + const { sourceLanguage, targetLanguage, sourceVariant, targetVariant } = + 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 + ) + ); } else { // No matching model was found, try to pivot between English. - const [payload1, payload2] = await Promise.all([ - TranslationsParent.getTranslationModelPayload( - fromLanguage, - PIVOT_LANGUAGE - ), - TranslationsParent.getTranslationModelPayload( - PIVOT_LANGUAGE, - toLanguage - ), - ]); - if (!payload1 || !payload2) { - throw new Error( - `No language models were found for ${fromLanguage} to ${toLanguage}` - ); - } - translationModelPayloads = [payload1, payload2]; + translationModelPayloads.push( + ...(await Promise.all([ + TranslationsParent.getTranslationModelPayload( + sourceLanguage, + PIVOT_LANGUAGE, + sourceVariant + ), + TranslationsParent.getTranslationModelPayload( + PIVOT_LANGUAGE, + targetLanguage, + targetVariant + ), + ])) + ); } ChromeUtils.addProfilerMarker( @@ -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} toLanguage + * @param {string} sourceLanguage + * @param {string} targetLanguage + * @param {string} [variant] * @returns {string} */ - static languagePairKey(fromLanguage, toLanguage) { - return `${fromLanguage},${toLanguage}`; + static nonPivotKey(sourceLanguage, targetLanguage, variant) { + 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. * - * @returns {Promise>} + * @returns {Promise>} */ - static getLanguagePairs() { + static getNonPivotLanguagePairs() { if (!TranslationsParent.#languagePairs) { TranslationsParent.#languagePairs = TranslationsParent.#getTranslationModelRecords().then(records => { const languagePairMap = new Map(); - for (const { fromLang, toLang } of records.values()) { - const key = TranslationsParent.languagePairKey(fromLang, toLang); + for (const { + fromLang: sourceLanguage, + toLang: targetLanguage, + variant, + } of records.values()) { + const key = TranslationsParent.nonPivotKey( + sourceLanguage, + targetLanguage, + variant + ); if (!languagePairMap.has(key)) { - languagePairMap.set(key, { fromLang, toLang }); + languagePairMap.set(key, { + sourceLanguage, + targetLanguage, + variant, + }); } } 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. - * This is more expensive of a call than getLanguagePairs since the display names - * are looked up. + * This is more expensive of a call than getNonPivotLanguagePairs since the display + * names are looked up. * * This is all of the information needed to render dropdowns for translation * language selection. @@ -1492,16 +1501,25 @@ export class TranslationsParent extends JSWindowActorParent { */ static async getSupportedLanguages() { await chaosMode(1 / 4); - const languagePairs = await TranslationsParent.getLanguagePairs(); + const languagePairs = await TranslationsParent.getNonPivotLanguagePairs(); /** @type {Set} */ - const fromLanguages = new Set(); + const sourceLanguageKeys = new Set(); /** @type {Set} */ - const toLanguages = new Set(); + const targetLanguageKeys = new Set(); - for (const { fromLang, toLang } of languagePairs) { - fromLanguages.add(fromLang); - toLanguages.add(toLang); + for (const { sourceLanguage, targetLanguage, variant } of languagePairs) { + if (sourceLanguage === PIVOT_LANGUAGE) { + // 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. @@ -1511,8 +1529,9 @@ export class TranslationsParent extends JSWindowActorParent { const languageDisplayNames = TranslationsParent.createLanguageDisplayNames(); - for (const langTagSet of [fromLanguages, toLanguages]) { - for (const langTag of langTagSet.keys()) { + for (const langTagSet of [sourceLanguageKeys, targetLanguageKeys]) { + for (const langTagKey of langTagSet) { + const [langTag] = langTagKey.split(","); if (displayNames.has(langTag)) { continue; } @@ -1521,19 +1540,31 @@ export class TranslationsParent extends JSWindowActorParent { } } - const addDisplayName = langTag => ({ - langTag, - displayName: displayNames.get(langTag), - }); + const addDisplayName = langTagKey => { + const [langTag, variant] = langTagKey.split(","); + 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); return { languagePairs, - fromLanguages: Array.from(fromLanguages.keys()) + sourceLanguages: Array.from(sourceLanguageKeys.keys()) .map(addDisplayName) .sort(sort), - toLanguages: Array.from(toLanguages.keys()) + targetLanguages: Array.from(targetLanguageKeys.keys()) .map(addDisplayName) .sort(sort), }; @@ -1548,8 +1579,8 @@ export class TranslationsParent extends JSWindowActorParent { static getLanguageList(supportedLanguages) { const displayNames = new Map(); for (const languages of [ - supportedLanguages.fromLanguages, - supportedLanguages.toLanguages, + supportedLanguages.sourceLanguages, + supportedLanguages.targetLanguages, ]) { for (const { langTag, displayName } of languages) { 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 // to guarantee uniqueness. lookupKey: record => - `${record.name}${TranslationsParent.languagePairKey( + `${record.name}${TranslationsParent.nonPivotKey( 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 * (not-downloaded) will delete en->es to clear the lingering one-sided package. * - * @returns {Set} Directional language pairs in the form of "fromLang,toLang" that indicates language pairs that were deleted. + * @returns {Set} Directional language pairs in the form of "sourceLanguage,targetLanguage" that indicates language pairs that were deleted. */ static async deleteCachedLanguageFiles() { - const languagePairs = await TranslationsParent.getLanguagePairs(); + const languagePairs = await TranslationsParent.getNonPivotLanguagePairs(); const deletionRequest = []; let deletedPairs = new Set(); - for (const { fromLang, toLang } of languagePairs) { + for (const { sourceLanguage, targetLanguage } of languagePairs) { const { downloadedPairs, nonDownloadedPairs } = await TranslationsParent.getDownloadedFileStatusToAndFromPair( - fromLang, - toLang + sourceLanguage, + targetLanguage ); if (downloadedPairs.size && nonDownloadedPairs.size) { @@ -2384,8 +2416,8 @@ export class TranslationsParent extends JSWindowActorParent { downloadedPairs.forEach(langPair => deletedPairs.add(langPair)); deletionRequest.push( TranslationsParent.deleteLanguageFilesToAndFromPair( - fromLang, - toLang, + sourceLanguage, + targetLanguage, /* deletePivots */ false ) ); @@ -2405,9 +2437,9 @@ export class TranslationsParent extends JSWindowActorParent { * * @returns {object} status The status between the pairs. * @returns {Set} 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} 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. */ @@ -2430,11 +2462,19 @@ export class TranslationsParent extends JSWindowActorParent { if (isDownloaded) { downloadedPairs.add( - TranslationsParent.languagePairKey(record.fromLang, record.toLang) + TranslationsParent.nonPivotKey( + record.fromLang, + record.toLang, + record.variant + ) ); } else { 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; } - const addLanguagePair = (fromLang, toLang) => { + const addLanguagePair = (sourceLanguage, targetLanguage) => { let matchFound = false; for (const record of records.values()) { if ( - lazy.TranslationsUtils.langTagsMatch(record.fromLang, fromLang) && - lazy.TranslationsUtils.langTagsMatch(record.toLang, toLang) + lazy.TranslationsUtils.langTagsMatch( + record.fromLang, + sourceLanguage + ) && + lazy.TranslationsUtils.langTagsMatch(record.toLang, targetLanguage) ) { matchedRecords.add(record); matchFound = true; @@ -2566,15 +2609,24 @@ export class TranslationsParent extends JSWindowActorParent { * * Results are only returned if the model is found. * - * @param {string} fromLanguage - * @param {string} toLanguage - * @returns {null | TranslationModelPayload} + * @param {string} sourceLanguage + * @param {string} targetLanguage + * @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(); lazy.console.log( - `Beginning model downloads: "${fromLanguage}" to "${toLanguage}"` + `Beginning model downloads: "${sourceLanguage}" to "${targetLanguage}"` ); const records = [ @@ -2582,7 +2634,7 @@ export class TranslationsParent extends JSWindowActorParent { ]; /** @type {LanguageTranslationModelFiles} */ - let results; + const results = {}; // Use Promise.all to download (or retrieve from cache) the model files in parallel. await Promise.all( @@ -2595,18 +2647,18 @@ export class TranslationsParent extends JSWindowActorParent { if ( !lazy.TranslationsUtils.langTagsMatch( record.fromLang, - fromLanguage + sourceLanguage ) || - !lazy.TranslationsUtils.langTagsMatch(record.toLang, toLanguage) + !lazy.TranslationsUtils.langTagsMatch( + record.toLang, + targetLanguage + ) || + record.variant !== variant ) { // Only use models that match. return; } - if (!results) { - results = {}; - } - const start = Date.now(); // Download or retrieve from the local cache: @@ -2626,53 +2678,50 @@ export class TranslationsParent extends JSWindowActorParent { `Translation model fetched in ${duration / 1000} seconds:`, record.fromLang, record.toLang, + record.variant, record.fileType, 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 // downloaded. if (!results.model) { 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) { 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.srcvocab) { 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) { 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) { throw new Error( - `No vocab files were provided for "${fromLanguage}" to "${toLanguage}."` + `No vocab files were provided for "${sourceLanguage}" to "${targetLanguage}."` ); } /** @type {TranslationModelPayload} */ return { - sourceLanguage: fromLanguage, - targetLanguage: toLanguage, + sourceLanguage, + targetLanguage, + variant, 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. * - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {string} sourceLanguage + * @param {string} targetLanguage * @returns {Promise} 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( - fromLanguage, - toLanguage + sourceLanguage, + targetLanguage ); // If a direct model is not found, then check pivots. if (directSize.downloadSize == 0 && !directSize.modelFound) { const indirectFrom = await TranslationsParent.#getModelDownloadSize( - fromLanguage, + sourceLanguage, PIVOT_LANGUAGE ); const indirectTo = await TranslationsParent.#getModelDownloadSize( PIVOT_LANGUAGE, - toLanguage + targetLanguage ); // 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. * - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {string} sourceLanguage + * @param {string} targetLanguage * @returns {Promise<{downloadSize: long, modelFound: boolean}>} Download size is the * 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 * bytes to download, because a model wasn't located. */ - static async #getModelDownloadSize(fromLanguage, toLanguage) { + static async #getModelDownloadSize(sourceLanguage, targetLanguage) { const client = TranslationsParent.#getTranslationModelsRemoteClient(); const records = [ ...(await TranslationsParent.#getTranslationModelRecords()).values(), @@ -2764,9 +2816,9 @@ export class TranslationsParent extends JSWindowActorParent { if ( !lazy.TranslationsUtils.langTagsMatch( record.fromLang, - fromLanguage + sourceLanguage ) || - !lazy.TranslationsUtils.langTagsMatch(record.toLang, toLanguage) + !lazy.TranslationsUtils.langTagsMatch(record.toLang, targetLanguage) ) { return; } @@ -2864,23 +2916,27 @@ export class TranslationsParent extends JSWindowActorParent { } /** - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair * @param {boolean} reportAsAutoTranslate - In telemetry, report this as * an auto-translate. */ - async translate(fromLanguage, toLanguage, reportAsAutoTranslate) { - if (!fromLanguage || !toLanguage) { + async translate(languagePair, reportAsAutoTranslate) { + const { sourceLanguage, targetLanguage } = languagePair; + if (!sourceLanguage || !targetLanguage) { lazy.console.error( - "A translation was requested but the fromLanguage or toLanguage was not set.", - { fromLanguage, toLanguage, reportAsAutoTranslate } + new Error( + "A translation was requested but the sourceLanguage or targetLanguage was not set." + ), + { sourceLanguage, targetLanguage, reportAsAutoTranslate } ); return; } - if (lazy.TranslationsUtils.langTagsMatch(fromLanguage, toLanguage)) { + if (lazy.TranslationsUtils.langTagsMatch(sourceLanguage, targetLanguage)) { lazy.console.error( - "A translation was requested where the from and to language match.", - { fromLanguage, toLanguage, reportAsAutoTranslate } + new Error( + "A translation was requested where the source and target languages match." + ), + { sourceLanguage, targetLanguage, reportAsAutoTranslate } ); return; } @@ -2888,8 +2944,8 @@ export class TranslationsParent extends JSWindowActorParent { // This page has already been translated, restore it and translate it // again once the actor has been recreated. const windowState = this.getWindowState(); - windowState.translateOnPageReload = { fromLanguage, toLanguage }; - this.restorePage(fromLanguage); + windowState.translateOnPageReload = languagePair; + this.restorePage(sourceLanguage); } else { 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 // process and the engine's process. const port = await TranslationsParent.requestTranslationsPort( - fromLanguage, - toLanguage, + languagePair, this ); if (!port) { 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; } - this.languageState.requestedLanguagePair = { - fromLanguage, - toLanguage, - }; + this.languageState.requestedLanguagePair = languagePair; const preferredLanguages = TranslationsParent.getPreferredLanguages(); const topPreferredLanguage = @@ -2927,20 +2979,19 @@ export class TranslationsParent extends JSWindowActorParent { TranslationsParent.telemetry().onTranslate({ docLangTag, - fromLanguage, - toLanguage, + sourceLanguage, + targetLanguage, topPreferredLanguage, autoTranslate: reportAsAutoTranslate, requestTarget: "full_page", }); - TranslationsParent.storeMostRecentTargetLanguage(toLanguage); + TranslationsParent.storeMostRecentTargetLanguage(targetLanguage); this.sendAsyncMessage( "Translations:TranslatePage", { - fromLanguage, - toLanguage, + languagePair, port, }, // 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. * * @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, - * where each object contains `fromLang` and `toLang` properties. + * @param {Array<{ sourceLanguage: string, targetLanguage: string }>} languagePairs - An array of language pair objects, + * where each object contains `sourceLanguage` and `targetLanguage` properties. * @returns {string | null} - The compatible source language tag, or `null` if no match is found. */ static findCompatibleSourceLangTagSync(langTag, languagePairs) { @@ -3039,11 +3090,11 @@ export class TranslationsParent extends JSWindowActorParent { return null; } - const langPair = languagePairs.find(({ fromLang }) => - lazy.TranslationsUtils.langTagsMatch(fromLang, langTag) + const langPair = languagePairs.find(({ sourceLanguage }) => + 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. */ static async findCompatibleSourceLangTag(langTag) { - const languagePairs = await TranslationsParent.getLanguagePairs(); + const languagePairs = await TranslationsParent.getNonPivotLanguagePairs(); return TranslationsParent.findCompatibleSourceLangTagSync( langTag, languagePairs @@ -3067,8 +3118,8 @@ export class TranslationsParent extends JSWindowActorParent { * 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 {Array<{ fromLang: string, toLang: string }>} languagePairs - An array of language pair objects, - * where each object contains `fromLang` and `toLang` properties. + * @param {Array<{ sourceLanguage: string, targetLanguage: string }>} languagePairs - An array of language pair objects, + * where each object contains `sourceLanguage` and `targetLanguage` properties. * @returns {string | null} - The compatible target language tag, or `null` if no match is found. */ static findCompatibleTargetLangTagSync(langTag, languagePairs) { @@ -3076,11 +3127,11 @@ export class TranslationsParent extends JSWindowActorParent { return null; } - const langPair = languagePairs.find(({ toLang }) => - lazy.TranslationsUtils.langTagsMatch(toLang, langTag) + const langPair = languagePairs.find(({ targetLanguage }) => + 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. */ static async findCompatibleTargetLangTag(langTag) { - const languagePairs = await TranslationsParent.getLanguagePairs(); + const languagePairs = await TranslationsParent.getNonPivotLanguagePairs(); return TranslationsParent.findCompatibleTargetLangTagSync( langTag, languagePairs @@ -3111,7 +3162,7 @@ export class TranslationsParent extends JSWindowActorParent { excludeLangTags, }); - const languagePairs = await TranslationsParent.getLanguagePairs(); + const languagePairs = await TranslationsParent.getNonPivotLanguagePairs(); for (const langTag of preferredLanguages) { const compatibleLangTag = TranslationsParent.findCompatibleTargetLangTagSync( @@ -3176,7 +3227,7 @@ export class TranslationsParent extends JSWindowActorParent { documentElementLang = this.maybeRefineMacroLanguageTag(documentElementLang); - let languagePairs = await TranslationsParent.getLanguagePairs(); + let languagePairs = await TranslationsParent.getNonPivotLanguagePairs(); if (this.#isDestroyed) { return null; } @@ -3737,7 +3788,7 @@ class TranslationsLanguageState { /** @type {TranslationsParent} */ #actor; - /** @type {TranslationPair | null} */ + /** @type {LanguagePair | null} */ #requestedLanguagePair = null; /** @type {LangTags | null} */ @@ -3776,7 +3827,7 @@ class TranslationsLanguageState { * that the TranslationsChild should be creating a TranslationsDocument and keep * the page updated with the target language. * - * @returns {TranslationPair | null} + * @returns {LanguagePair | null} */ get requestedLanguagePair() { return this.#requestedLanguagePair; diff --git a/toolkit/components/translations/content/Translator.mjs b/toolkit/components/translations/content/Translator.mjs index 7655a08d6f49..9d8db604def9 100644 --- a/toolkit/components/translations/content/Translator.mjs +++ b/toolkit/components/translations/content/Translator.mjs @@ -2,6 +2,15 @@ * 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/. */ +/** + * @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. */ @@ -29,24 +38,17 @@ export class Translator { #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; - - /** - * The BCP-47 language tag for the to-language. - * - * @type {string} - */ - #toLanguage; + #languagePair; /** * The callback function to request a new port, provided at construction time * by the caller. * - * @type {Function} + * @type {RequestTranslationsPort} */ #requestTranslationsPort; @@ -71,13 +73,11 @@ export class Translator { * * @see Translator.create * - * @param {string} fromLanguage - The BCP-47 from-language tag. - * @param {string} toLanguage - The BCP-47 to-language tag. - * @param {Function} requestTranslationsPort - A callback function to request a new MessagePort. + * @param {LanguagePair} languagePair + * @param {RequestTranslationsPort} requestTranslationsPort - A callback function to request a new MessagePort. */ - constructor(fromLanguage, toLanguage, requestTranslationsPort) { - this.#fromLanguage = fromLanguage; - this.#toLanguage = toLanguage; + constructor(languagePair, requestTranslationsPort) { + this.#languagePair = languagePair; this.#requestTranslationsPort = requestTranslationsPort; } @@ -95,52 +95,30 @@ export class Translator { 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. * - * @param {string} fromLanguage - The BCP-47 language tag of the from-language. - * @param {string} toLanguage - The BCP-47 language tag of the to-language. - * @param {object} data - Data for creating a translator. - * @param {Function} [data.requestTranslationsPort] + * @param {LanguagePair} languagePair + * @param {RequestTranslationsPort} [requestTranslationsPort] * - 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 - * is the same as the toLanguage. - * @param {boolean} [data.allowSameLanguage] + * This is required in all cases except if allowSameLanguage is true and the sourceLanguage + * is the same as the targetLanguage. + * @param {boolean} [allowSameLanguage] * - 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} */ static async create( - fromLanguage, - toLanguage, - { requestTranslationsPort, allowSameLanguage } + languagePair, + requestTranslationsPort, + allowSameLanguage ) { - if (!fromLanguage || !toLanguage) { - throw new Error( - "Attempt to create Translator with missing language tags." - ); - } - - if (fromLanguage === toLanguage) { + if (languagePair.sourceLanguage === languagePair.targetLanguage) { if (!allowSameLanguage) { throw new Error("Attempt to create disallowed PassthroughTranslator"); } - return new PassthroughTranslator(fromLanguage, toLanguage); + return new PassthroughTranslator(languagePair); } if (!requestTranslationsPort) { @@ -149,11 +127,7 @@ export class Translator { ); } - const translator = new Translator( - fromLanguage, - toLanguage, - requestTranslationsPort - ); + const translator = new Translator(languagePair, requestTranslationsPort); await translator.#createNewPortIfClosed(); return translator; @@ -169,10 +143,7 @@ export class Translator { return; } - this.#port = await this.#requestTranslationsPort( - this.#fromLanguage, - this.#toLanguage - ); + this.#port = await this.#requestTranslationsPort(this.#languagePair); this.#portClosed = false; // Create a promise that will be resolved when the engine is ready. @@ -193,7 +164,7 @@ export class Translator { resolve(); } else { this.#portClosed = true; - reject(); + reject(new Error(data.error)); } break; } @@ -256,18 +227,18 @@ export class Translator { /** * The PassthroughTranslator class mimics the same API as the Translator 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 * text. * - * The Translator class may return a PassthroughTranslator instance if the fromLanguage - * and toLanguage passed to the create() method are the same. + * The Translator class may return a PassthroughTranslator instance if the sourceLanguage + * and targetLanguage passed to the create() method are the same. * * @see Translator.create */ 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} */ @@ -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; } /** - * @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; } @@ -308,16 +279,15 @@ class PassthroughTranslator { * * @see Translator.create * - * @param {string} fromLanguage - The BCP-47 from-language tag. - * @param {string} toLanguage - The BCP-47 to-language tag. + * @param {LanguagePair} languagePair */ - constructor(fromLanguage, toLanguage) { - if (fromLanguage !== toLanguage) { + constructor(languagePair) { + if (languagePair.sourceLanguage !== languagePair.targetLanguage) { 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; } /** diff --git a/toolkit/components/translations/content/translations-document.sys.mjs b/toolkit/components/translations/content/translations-document.sys.mjs index 8cf01ad79d7f..806aa3db3bd9 100644 --- a/toolkit/components/translations/content/translations-document.sys.mjs +++ b/toolkit/components/translations/content/translations-document.sys.mjs @@ -53,10 +53,8 @@ export class LRUCache { #htmlCache = new Map(); /** @type {Map} */ #textCache = new Map(); - /** @type {string} */ - #fromLanguage; - /** @type {string} */ - #toLanguage; + /** @type {LanguagePair} */ + #languagePair; /** * 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; /** - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair */ - constructor(fromLanguage, toLanguage) { - this.#fromLanguage = fromLanguage; - this.#toLanguage = toLanguage; + constructor(languagePair) { + this.#languagePair = languagePair; } /** @@ -129,13 +125,18 @@ export class LRUCache { } /** - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair */ - matches(fromLanguage, toLanguage) { + matches(languagePair) { return ( - lazy.TranslationsUtils.langTagsMatch(this.#fromLanguage, fromLanguage) && - lazy.TranslationsUtils.langTagsMatch(this.#toLanguage, toLanguage) + lazy.TranslationsUtils.langTagsMatch( + this.#languagePair.sourceLanguage, + languagePair.sourceLanguage + ) && + lazy.TranslationsUtils.langTagsMatch( + this.#languagePair.targetLanguage, + languagePair.targetLanguage + ) ); } @@ -496,7 +497,7 @@ export class TranslationsDocument { * * @param {Document} document * @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 {MessagePort} port - The port to the translations engine. * @param {() => void} requestNewPort - Used when an engine times out and a new @@ -510,7 +511,7 @@ export class TranslationsDocument { constructor( document, documentLanguage, - toLanguage, + targetLanguage, innerWindowId, port, requestNewPort, @@ -660,7 +661,7 @@ export class TranslationsDocument { ); }); - document.documentElement.lang = toLanguage; + document.documentElement.lang = targetLanguage; lazy.console.log( "Beginning to translate.", @@ -1140,7 +1141,7 @@ export class TranslationsDocument { } if (!this.matchesDocumentLanguage(node)) { - // Exclude nodes that don't match the fromLanguage. + // Exclude nodes that don't match the sourceLanguage. return true; } diff --git a/toolkit/components/translations/content/translations-engine.sys.mjs b/toolkit/components/translations/content/translations-engine.sys.mjs index ace0990612d6..9818b26e9d8c 100644 --- a/toolkit/components/translations/content/translations-engine.sys.mjs +++ b/toolkit/components/translations/content/translations-engine.sys.mjs @@ -71,8 +71,15 @@ const CACHE_TIMEOUT_MS = 15_000; /** * @typedef {import("./translations-document.sys.mjs").TranslationsDocument} TranslationsDocument * @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 * 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 * been used, it is destroyed. * - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair * @param {number} innerWindowId * @returns {Promise} */ - static getOrCreate(fromLanguage, toLanguage, innerWindowId) { - const languagePairKey = getLanguagePairKey(fromLanguage, toLanguage); + static getOrCreate(languagePair, innerWindowId) { + const languagePairKey = + lazy.TranslationsUtils.serializeLanguagePair(languagePair); let enginePromise = TranslationsEngine.#cachedEngines.get(languagePairKey); if (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. - enginePromise = TranslationsEngine.create( - fromLanguage, - toLanguage, - innerWindowId - ); + enginePromise = TranslationsEngine.create(languagePair, innerWindowId); TranslationsEngine.#cachedEngines.set(languagePairKey, enginePromise); enginePromise.catch(error => { 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 ); // Remove the engine if it fails to initialize. @@ -166,25 +169,28 @@ export class TranslationsEngine { /** * Create a TranslationsEngine and bypass the cache. * - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair * @param {number} innerWindowId * @returns {Promise} */ - static async create(fromLanguage, toLanguage, innerWindowId) { + static async create(languagePair, innerWindowId) { const startTime = performance.now(); + if (!languagePair.sourceLanguage || !languagePair.targetLanguage) { + throw new Error( + "Attempt to create Translator with missing language tags." + ); + } const engine = new TranslationsEngine( - fromLanguage, - toLanguage, - await TE_requestEnginePayload(fromLanguage, toLanguage) + languagePair, + await TE_requestEnginePayload(languagePair) ); await engine.isReady; TE_addProfilerMarker({ startTime, - message: `Translations engine loaded for "${fromLanguage}" to "${toLanguage}"`, + message: `Translations engine loaded for "${lazy.TranslationsUtils.serializeLanguagePair(languagePair)}"`, innerWindowId, }); @@ -221,10 +227,10 @@ export class TranslationsEngine { clearTimeout(this.#keepAliveTimeout); } for (const [innerWindowId, data] of ports) { - const { fromLanguage, toLanguage, port } = data; + const { sourceLanguage, targetLanguage, port } = data; if ( - fromLanguage === this.fromLanguage && - toLanguage === this.toLanguage + sourceLanguage === this.sourceLanguage && + targetLanguage === this.targetLanguage ) { // This port is still active but being closed. ports.delete(innerWindowId); @@ -252,17 +258,15 @@ export class TranslationsEngine { /** * Construct and initialize the worker. * - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair * @param {TranslationsEnginePayload} enginePayload - If there is no engine payload * then the engine will be mocked. This allows this class to be used in tests. */ - constructor(fromLanguage, toLanguage, enginePayload) { - /** @type {string} */ - this.fromLanguage = fromLanguage; - /** @type {string} */ - this.toLanguage = toLanguage; - this.languagePairKey = getLanguagePairKey(fromLanguage, toLanguage); + constructor(languagePair, enginePayload) { + /** @type {LanguagePair} */ + this.languagePair = languagePair; + this.languagePairKey = + lazy.TranslationsUtils.serializeLanguagePair(languagePair); this.#worker = new Worker( "chrome://global/content/translations/translations-engine.worker.js" ); @@ -297,11 +301,13 @@ export class TranslationsEngine { } } + const { sourceLanguage, targetLanguage } = languagePair; + this.#worker.postMessage( { type: "initialize", - fromLanguage, - toLanguage, + sourceLanguage, + targetLanguage, enginePayload, messageId: this.#messageId++, logLevel: TE_getLogLevel(), @@ -370,13 +376,12 @@ export class TranslationsEngine { /** * Applies a function only if a cached engine exists. * - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair * @param {(engine: TranslationsEngine) => void} fn */ - static withCachedEngine(fromLanguage, toLanguage, fn) { + static withCachedEngine(languagePair, fn) { const engine = TranslationsEngine.#cachedEngines.get( - getLanguagePairKey(fromLanguage, toLanguage) + lazy.TranslationsUtils.serializeLanguagePair(languagePair) ); if (engine) { @@ -409,37 +414,15 @@ export class TranslationsEngine { 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. * - * @type {Map} + * @type {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 * in the content process by the TranslationsDocument. * - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {LanguagePair} languagePair * @param {number} innerWindowId * @param {MessagePort} port */ -function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) { +function listenForPortMessages(languagePair, innerWindowId, port) { async function handleMessage({ data }) { switch (data.type) { case "TranslationsPort:GetEngineStatusRequest": { // This message gets sent first before the translation queue is processed. // The engine is most likely to fail on the initial invocation. Any failure // past the first one is not reported to the UI. - TranslationsEngine.getOrCreate( - fromLanguage, - toLanguage, - innerWindowId - ).then( + TranslationsEngine.getOrCreate(languagePair, innerWindowId).then( () => { TE_log("The engine is ready for translations.", { innerWindowId, @@ -475,11 +453,13 @@ function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) { status: "ready", }); }, - () => { + error => { + console.error(error); TE_reportEngineStatus(innerWindowId, "error"); port.postMessage({ type: "TranslationsPort:GetEngineStatusResponse", status: "error", + error: String(error), }); // After an error no more translation requests will be sent. Go ahead // and close the port. @@ -492,8 +472,7 @@ function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) { case "TranslationsPort:TranslationRequest": { const { sourceText, isHTML, translationId } = data; const engine = await TranslationsEngine.getOrCreate( - fromLanguage, - toLanguage, + languagePair, innerWindowId ); const targetText = await engine.translate( @@ -511,13 +490,9 @@ function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) { } case "TranslationsPort:CancelSingleTranslation": { const { translationId } = data; - TranslationsEngine.withCachedEngine( - fromLanguage, - toLanguage, - engine => { - engine.cancelSingleTranslation(innerWindowId, translationId); - } - ); + TranslationsEngine.withCachedEngine(languagePair, engine => { + engine.cancelSingleTranslation(innerWindowId, translationId); + }); break; } case "TranslationsPort:DiscardTranslations": { @@ -551,11 +526,11 @@ function discardTranslations(innerWindowId) { const portData = ports.get(innerWindowId); if (portData) { - const { port, fromLanguage, toLanguage } = portData; + const { port, languagePair } = portData; port.close(); ports.delete(innerWindowId); - TranslationsEngine.withCachedEngine(fromLanguage, toLanguage, engine => { + TranslationsEngine.withCachedEngine(languagePair, engine => { engine.discardTranslationQueue(innerWindowId); }); } @@ -567,10 +542,14 @@ function discardTranslations(innerWindowId) { window.addEventListener("message", ({ data }) => { switch (data.type) { case "StartTranslation": { - const { fromLanguage, toLanguage, innerWindowId, port } = data; - TE_log("Starting translation", innerWindowId); - listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port); - ports.set(innerWindowId, { port, fromLanguage, toLanguage }); + const { languagePair, innerWindowId, port } = data; + TE_log( + "Starting translation", + lazy.TranslationsUtils.serializeLanguagePair(languagePair), + innerWindowId + ); + listenForPortMessages(languagePair, innerWindowId, port); + ports.set(innerWindowId, { port, languagePair }); break; } case "DiscardTranslations": { diff --git a/toolkit/components/translations/content/translations-engine.worker.js b/toolkit/components/translations/content/translations-engine.worker.js index 9faed172c803..14b8a785ac1d 100644 --- a/toolkit/components/translations/content/translations-engine.worker.js +++ b/toolkit/components/translations/content/translations-engine.worker.js @@ -136,14 +136,19 @@ async function handleInitializationMessage({ data }) { } try { - const { fromLanguage, toLanguage, enginePayload, logLevel, innerWindowId } = - data; + const { + sourceLanguage, + targetLanguage, + enginePayload, + logLevel, + innerWindowId, + } = data; - if (!fromLanguage) { - throw new Error('Worker initialization missing "fromLanguage"'); + if (!sourceLanguage) { + throw new Error('Worker initialization missing "sourceLanguage"'); } - if (!toLanguage) { - throw new Error('Worker initialization missing "toLanguage"'); + if (!targetLanguage) { + throw new Error('Worker initialization missing "targetLanguage"'); } if (logLevel) { @@ -154,7 +159,7 @@ async function handleInitializationMessage({ data }) { let engine; if (enginePayload.isMocked) { // The engine is testing mode, and no Bergamot wasm is available. - engine = new MockedEngine(fromLanguage, toLanguage); + engine = new MockedEngine(sourceLanguage, targetLanguage); } else { const { bergamotWasmArrayBuffer, translationModelPayloads } = enginePayload; @@ -162,8 +167,8 @@ async function handleInitializationMessage({ data }) { bergamotWasmArrayBuffer ); engine = new Engine( - fromLanguage, - toLanguage, + sourceLanguage, + targetLanguage, bergamot, translationModelPayloads ); @@ -220,7 +225,7 @@ function handleMessages(engine) { } try { 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 // back. The translation may never return if the translations are discarded @@ -318,16 +323,21 @@ function handleMessages(engine) { */ class Engine { /** - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {string} sourceLanguage + * @param {string} targetLanguage * @param {Bergamot} bergamot * @param {Array} translationModelPayloads */ - constructor(fromLanguage, toLanguage, bergamot, translationModelPayloads) { + constructor( + sourceLanguage, + targetLanguage, + bergamot, + translationModelPayloads + ) { /** @type {string} */ - this.fromLanguage = fromLanguage; + this.sourceLanguage = sourceLanguage; /** @type {string} */ - this.toLanguage = toLanguage; + this.targetLanguage = targetLanguage; /** @type {Bergamot} */ this.bergamot = bergamot; /** @type {Bergamot["TranslationModel"][]} */ @@ -696,14 +706,14 @@ class BergamotUtils { */ class MockedEngine { /** - * @param {string} fromLanguage - * @param {string} toLanguage + * @param {string} sourceLanguage + * @param {string} targetLanguage */ - constructor(fromLanguage, toLanguage) { + constructor(sourceLanguage, targetLanguage) { /** @type {string} */ - this.fromLanguage = fromLanguage; + this.sourceLanguage = sourceLanguage; /** @type {string} */ - this.toLanguage = toLanguage; + this.targetLanguage = targetLanguage; } /** @@ -717,7 +727,7 @@ class MockedEngine { // Note when an HTML translations is requested. let html = isHTML ? ", html" : ""; const targetText = sourceText.toUpperCase(); - return `${targetText} [${this.fromLanguage} to ${this.toLanguage}${html}]`; + return `${targetText} [${this.sourceLanguage} to ${this.targetLanguage}${html}]`; } discardTranslations() {} diff --git a/toolkit/components/translations/content/translations.mjs b/toolkit/components/translations/content/translations.mjs index 8cd2db930254..781fd5579914 100644 --- a/toolkit/components/translations/content/translations.mjs +++ b/toolkit/components/translations/content/translations.mjs @@ -11,6 +11,7 @@ AT_isTranslationEngineSupported, AT_identifyLanguage, AT_telemetry */ 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. // This is the delay in milliseconds. @@ -44,7 +45,7 @@ class TranslationsState { * * @type {string} */ - fromLanguage = ""; + sourceLanguage = ""; /** * The language to translate to, in the form of a BCP 47 language tag, @@ -52,7 +53,26 @@ class TranslationsState { * * @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 @@ -138,16 +158,16 @@ class TranslationsState { * in a new translation request. */ 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) { // Never translate when the engine isn't supported. 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. this.ui.updateTranslation(""); return; @@ -162,8 +182,7 @@ class TranslationsState { // then skip this request, as there is already a newer request with more up to // date information. this.translator !== translator || - this.fromLanguage !== fromLanguage || - this.toLanguage !== toLanguage || + this.languagePair !== languagePair || this.messageToTranslate !== messageToTranslate ) { return; @@ -180,7 +199,7 @@ class TranslationsState { // The measure events will show up in the Firefox Profiler. performance.measure( - `Translations: Translate "${this.fromLanguage}" to "${this.toLanguage}" with ${messageToTranslate.length} characters.`, + `Translations: Translate "${this.languagePairKey}" with ${messageToTranslate.length} characters.`, { start, 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. if ( - // If fromLanguage or toLanguage are unpopulated we cannot load anything. - !this.fromLanguage || - !this.toLanguage || - // If fromLanguage's value is "detect", rather than a BCP 47 language tag, then no language + // If sourceLanguage or targetLanguage are unpopulated we cannot load anything. + !this.sourceLanguage || + !this.targetLanguage || + // If sourceLanguage's value is "detect", rather than a BCP 47 language tag, then no language // has been detected yet. - this.fromLanguage === "detect" || - // If fromLanguage and toLanguage 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. - this.fromLanguage === this.toLanguage + this.sourceLanguage === "detect" || + // If sourceLanguage and targetLanguage are the same, this means that the detected language + // is the same as the targetLanguage, and we do not want to translate from one language to itself. + this.sourceLanguage === this.targetLanguage ) { if (this.translator) { // The engine is no longer needed. this.translator.destroy(); this.translator = null; + this.languagePair = null; + this.languagePairKey = null; } return; } const start = performance.now(); 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 getResponse = ({ data }) => { if ( data.type == "GetTranslationsPort" && - data.fromLanguage === fromLanguage && - data.toLanguage === toLanguage + data.languagePair.sourceLanguage === languagePair.sourceLanguage && + data.languagePair.targetLanguage === languagePair.targetLanguage && + data.languagePair.sourceVariant == languagePair.sourceVariant && + data.languagePair.targetVariant == languagePair.targetVariant ) { window.removeEventListener("message", getResponse); resolve(data.port); @@ -248,29 +271,38 @@ class TranslationsState { }; window.addEventListener("message", getResponse); - AT_createTranslationsPort(fromLanguage, toLanguage); + AT_createTranslationsPort(languagePair); return promise; }; + this.languagePair = { + sourceLanguage: this.sourceLanguage, + targetLanguage: this.targetLanguage, + sourceVariant: this.sourceVariant, + targetVariant: this.targetVariant, + }; + this.languagePairKey = TranslationsUtils.serializeLanguagePair( + this.languagePair + ); + try { const translatorPromise = Translator.create( - this.fromLanguage, - this.toLanguage, - { - allowSameLanguage: false, - requestTranslationsPort: translationPortPromise, - } + this.languagePair, + requestTranslationsPort ); const duration = performance.now() - start; // Signal to tests that the translator was created so they can exit. window.postMessage("translator-ready"); - AT_log(`Created a new Translator in ${duration / 1000} seconds`); this.translator = await translatorPromise; + AT_log(`Created a new Translator in ${duration / 1000} seconds`); + this.maybeRequestTranslation(); } catch (error) { + this.languagePair = null; + this.languagePairKey = null; this.ui.showInfo("about-translations-engine-error"); this.ui.setResultPlaceholderTextContent(l10nIds.resultsPlaceholder); 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. * - * 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 * worker if there is a valid selected target language. */ @@ -301,32 +333,36 @@ class TranslationsState { // Only update the language if the detected language matches // one of our supported languages. - const entry = supportedLanguages.fromLanguages.find( + const entry = supportedLanguages.sourceLanguages.find( ({ langTag: existingTag }) => existingTag === langTag ); if (entry) { const { displayName } = entry; - await this.setFromLanguage(langTag); + await this.setSourceLanguage(langTag); this.ui.setDetectOptionTextContent(displayName); } } /** - * @param {string} lang + * @param {string} langTagKey */ - async setFromLanguage(lang) { - if (lang !== this.fromLanguage) { - this.fromLanguage = lang; + async setSourceLanguage(langTagKey) { + const [langTag, variant] = langTagKey.split(","); + if (langTag !== this.sourceLanguage || variant !== this.sourceVariant) { + this.sourceLanguage = langTag; + this.sourceVariant = variant; await this.maybeCreateNewTranslator(); } } /** - * @param {string} lang + * @param {string} langTagKey */ - setToLanguage(lang) { - if (lang !== this.toLanguage) { - this.toLanguage = lang; + setTargetLanguage(langTagKey) { + const [langTag, variant] = langTagKey.split(","); + if (langTag !== this.targetLanguage || this.targetVariant !== variant) { + this.targetLanguage = langTag; + this.targetVariant = variant; this.maybeCreateNewTranslator(); } } @@ -348,9 +384,9 @@ class TranslationsState { */ class TranslationsUI { /** @type {HTMLSelectElement} */ - languageFrom = document.getElementById("language-from"); + sourceLanguage = document.getElementById("language-from"); /** @type {HTMLSelectElement} */ - languageTo = document.getElementById("language-to"); + targetLanguage = document.getElementById("language-to"); /** @type {HTMLButtonElement} */ languageSwap = document.getElementById("language-swap"); /** @type {HTMLTextAreaElement} */ @@ -419,45 +455,51 @@ class TranslationsUI { const supportedLanguages = await this.state.supportedLanguages; // Update the DOM elements with the display names. - for (const { langTag, displayName } of supportedLanguages.toLanguages) { + for (const { + langTagKey, + displayName, + } of supportedLanguages.targetLanguages) { const option = document.createElement("option"); - option.value = langTag; + option.value = langTagKey; 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"); - option.value = langTag; + option.value = langTagKey; option.text = displayName; - this.languageFrom.add(option); + this.sourceLanguage.add(option); } // Enable the controls. - this.languageFrom.disabled = false; - this.languageTo.disabled = false; + this.sourceLanguage.disabled = false; + this.targetLanguage.disabled = false; // Focus the language dropdowns if they are empty. - if (this.languageFrom.value == "") { - this.languageFrom.focus(); - } else if (this.languageTo.value == "") { - this.languageTo.focus(); + if (this.sourceLanguage.value == "") { + this.sourceLanguage.focus(); + } else if (this.targetLanguage.value == "") { + this.targetLanguage.focus(); } - this.state.setFromLanguage(this.languageFrom.value); - this.state.setToLanguage(this.languageTo.value); + this.state.setSourceLanguage(this.sourceLanguage.value); + this.state.setTargetLanguage(this.targetLanguage.value); await this.updateOnLanguageChange(); - this.languageFrom.addEventListener("input", async () => { - this.state.setFromLanguage(this.languageFrom.value); + this.sourceLanguage.addEventListener("input", async () => { + this.state.setSourceLanguage(this.sourceLanguage.value); await this.updateOnLanguageChange(); }); - this.languageTo.addEventListener("input", async () => { - this.state.setToLanguage(this.languageTo.value); + this.targetLanguage.addEventListener("input", async () => { + this.state.setTargetLanguage(this.targetLanguage.value); 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 () => { const translationToValue = this.translationTo.innerText; - const newFromLanguage = this.sanitizeTargetLangTagAsSourceLangTag( - this.state.toLanguage + const newSourceLanguage = this.sanitizeTargetLangTagAsSourceLangTag( + this.targetLanguage.value ); - const newToLanguage = this.sanitizeSourceLangTagAsTargetLangTag( - this.state.fromLanguage - ); - this.state.setFromLanguage(newFromLanguage); - this.state.setToLanguage(newToLanguage); + const newTargetLanguage = + this.sanitizeSourceLangTagAsTargetLangTag(this.sourceLanguage.value) || + this.state.sourceLanguage; - this.languageFrom.value = newFromLanguage; - this.languageTo.value = newToLanguage; + this.state.setSourceLanguage(newSourceLanguage); + this.state.setTargetLanguage(newTargetLanguage); + + this.sourceLanguage.value = newSourceLanguage; + this.targetLanguage.value = newTargetLanguage; await this.updateOnLanguageChange(); - this.translationTo.setAttribute("lang", this.languageTo.value); + this.translationTo.setAttribute("lang", this.targetLanguage.value); this.translationFrom.value = translationToValue; this.state.setMessageToTranslate(translationToValue); @@ -539,7 +582,7 @@ class TranslationsUI { * @returns {boolean} */ detectOptionIsSelected() { - return this.languageFrom.value === "detect"; + return this.sourceLanguage.value === "detect"; } /** @@ -588,23 +631,23 @@ class TranslationsUI { * if this is the case. */ #updateDropdownLanguages() { - for (const option of this.languageFrom.options) { + for (const option of this.sourceLanguage.options) { option.hidden = false; } - for (const option of this.languageTo.options) { + for (const option of this.targetLanguage.options) { option.hidden = false; } - if (this.state.toLanguage) { - const option = this.languageFrom.querySelector( - `[value=${this.state.toLanguage}]` + if (this.state.targetLanguage) { + const option = this.sourceLanguage.querySelector( + `[value=${this.state.targetLanguage}]` ); if (option) { option.hidden = true; } } - if (this.state.fromLanguage) { - const option = this.languageTo.querySelector( - `[value=${this.state.fromLanguage}]` + if (this.state.sourceLanguage) { + const option = this.targetLanguage.querySelector( + `[value=${this.state.sourceLanguage}]` ); if (option) { option.hidden = true; @@ -636,18 +679,18 @@ class TranslationsUI { * The effects are similar, but reversed for RTL text in an LTR UI. */ #updateMessageDirections() { - if (this.state.toLanguage) { + if (this.state.targetLanguage) { this.translationTo.setAttribute( "dir", - AT_getScriptDirection(this.state.toLanguage) + AT_getScriptDirection(this.state.targetLanguage) ); } else { this.translationTo.removeAttribute("dir"); } - if (this.state.fromLanguage) { + if (this.state.sourceLanguage) { this.translationFrom.setAttribute( "dir", - AT_getScriptDirection(this.state.fromLanguage) + AT_getScriptDirection(this.state.sourceLanguage) ); } else { 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() { - const sourceLanguage = this.state.fromLanguage; - const targetLanguage = this.state.toLanguage; + const sourceLanguage = this.state.sourceLanguage; + const targetLanguage = this.state.targetLanguage; if ( sourceLanguage === @@ -679,12 +722,12 @@ class TranslationsUI { const isSourceLanguageValidAsTargetLanguage = sourceLanguage === "detect" || supportedLanguages.languagePairs.some( - ({ toLang }) => toLang === sourceLanguage + ({ targetLanguage }) => targetLanguage === sourceLanguage ); const isTargetLanguageValidAsSourceLanguage = targetLanguage === "" || supportedLanguages.languagePairs.some( - ({ fromLang }) => fromLang === targetLanguage + ({ sourceLanguage }) => sourceLanguage === targetLanguage ); this.languageSwap.disabled = @@ -702,8 +745,8 @@ class TranslationsUI { disableUI() { this.translationFrom.disabled = true; - this.languageFrom.disabled = true; - this.languageTo.disabled = true; + this.sourceLanguage.disabled = true; + this.targetLanguage.disabled = true; this.languageSwap.disabled = true; } diff --git a/toolkit/components/translations/tests/browser/browser_translations_actor.js b/toolkit/components/translations/tests/browser/browser_translations_actor.js index 4b1dcbab6b3c..de3aa00a0eb5 100644 --- a/toolkit/components/translations/tests/browser/browser_translations_actor.js +++ b/toolkit/components/translations/tests/browser/browser_translations_actor.js @@ -32,7 +32,9 @@ add_task(async function test_pivot_language_behavior() { // Sort the language pairs, as the order is not guaranteed. function sort(list) { 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. languagePairs.sort((a, b) => - TranslationsParent.languagePairKey(a.fromLang, a.toLang).localeCompare( - TranslationsParent.languagePairKey(b.fromLang, b.toLang) + TranslationsParent.nonPivotKey( + 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( sort(languagePairs), sort([ - { fromLang: "en", toLang: "es" }, - { fromLang: "en", toLang: "yue" }, - { fromLang: "es", toLang: "en" }, - { fromLang: "is", toLang: "en" }, - { fromLang: "yue", toLang: "en" }, + { sourceLanguage: "en", targetLanguage: "es", variant: undefined }, + { sourceLanguage: "en", targetLanguage: "yue", variant: undefined }, + { sourceLanguage: "es", targetLanguage: "en", variant: undefined }, + { sourceLanguage: "is", targetLanguage: "en", variant: undefined }, + { sourceLanguage: "yue", targetLanguage: "en", variant: undefined }, ]), "Non-pivot languages were removed on debug builds." ); } else { Assert.deepEqual( sort(languagePairs), - sort(fromLanguagePairs), + sort( + fromLanguagePairs.map(({ fromLang, toLang }) => ({ + sourceLanguage: fromLang, + targetLanguage: toLang, + varient: undefined, + })) + ), "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(); - for (const { fromLang, toLang } of languagePairs) { + for (const { sourceLanguage, targetLanguage } of languagePairs) { ok( - await TranslationsParent.findCompatibleSourceLangTag(fromLang), + await TranslationsParent.findCompatibleSourceLangTag(sourceLanguage), "Each from-language should be supported as a translation source language." ); ok( - await TranslationsParent.findCompatibleTargetLangTag(toLang), + await TranslationsParent.findCompatibleTargetLangTag(targetLanguage), "Each to-language should be supported as a translation target language." ); is( - Boolean(await TranslationsParent.findCompatibleTargetLangTag(fromLang)), - languagePairs.some(({ toLang }) => - TranslationsUtils.langTagsMatch(toLang, fromLang) + Boolean( + await TranslationsParent.findCompatibleTargetLangTag(sourceLanguage) + ), + 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." ); is( - Boolean(await TranslationsParent.findCompatibleSourceLangTag(toLang)), - languagePairs.some(({ fromLang }) => - TranslationsUtils.langTagsMatch(fromLang, toLang) + Boolean( + await TranslationsParent.findCompatibleSourceLangTag(targetLanguage) + ), + 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." ); @@ -212,8 +228,8 @@ add_task(async function test_translating_to_and_from_app_language() { */ function getUniqueLanguagePairs(records) { const langPairs = new Set(); - for (const { fromLang, toLang } of records) { - langPairs.add(TranslationsParent.languagePairKey(fromLang, toLang)); + for (const { fromLang, toLang, variant } of records) { + langPairs.add(TranslationsParent.nonPivotKey(fromLang, toLang, variant)); } return Array.from(langPairs) .sort() diff --git a/toolkit/components/translations/tests/browser/browser_translations_actor_sync_wasm.js b/toolkit/components/translations/tests/browser/browser_translations_actor_sync_wasm.js index fc07b7bb89d4..c18629220ac4 100644 --- a/toolkit/components/translations/tests/browser/browser_translations_actor_sync_wasm.js +++ b/toolkit/components/translations/tests/browser/browser_translations_actor_sync_wasm.js @@ -17,7 +17,10 @@ add_task(async function test_translations_actor_sync_update_wasm() { const decoder = new TextDecoder(); const { bergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(bergamotWasmArrayBuffer), @@ -45,7 +48,10 @@ add_task(async function test_translations_actor_sync_update_wasm() { }); const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(updatedBergamotWasmArrayBuffer), @@ -67,7 +73,10 @@ add_task(async function test_translations_actor_sync_delete_wasm() { const decoder = new TextDecoder(); const { bergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(bergamotWasmArrayBuffer), @@ -91,11 +100,12 @@ add_task(async function test_translations_actor_sync_delete_wasm() { }); let errorMessage; - await TranslationsParent.getTranslationsEnginePayload("en", "es").catch( - error => { - errorMessage = error?.message; - } - ); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }).catch(error => { + errorMessage = error?.message; + }); is( errorMessage, @@ -118,7 +128,10 @@ add_task( const decoder = new TextDecoder(); const { bergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(bergamotWasmArrayBuffer), @@ -135,7 +148,10 @@ add_task( }); const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(updatedBergamotWasmArrayBuffer), @@ -159,7 +175,10 @@ add_task( const decoder = new TextDecoder(); const { bergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(bergamotWasmArrayBuffer), @@ -178,7 +197,10 @@ add_task( }); const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(updatedBergamotWasmArrayBuffer), @@ -202,7 +224,10 @@ add_task(async function test_translations_actor_sync_rollback_wasm() { const decoder = new TextDecoder(); const { bergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(bergamotWasmArrayBuffer), @@ -219,7 +244,10 @@ add_task(async function test_translations_actor_sync_rollback_wasm() { }); const { bergamotWasmArrayBuffer: updatedBergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(updatedBergamotWasmArrayBuffer), @@ -233,7 +261,10 @@ add_task(async function test_translations_actor_sync_rollback_wasm() { }); const { bergamotWasmArrayBuffer: rolledBackBergamotWasmArrayBuffer } = - await TranslationsParent.getTranslationsEnginePayload("en", "es"); + await TranslationsParent.getTranslationsEnginePayload({ + sourceLanguage: "en", + targetLanguage: "es", + }); is( decoder.decode(rolledBackBergamotWasmArrayBuffer), diff --git a/toolkit/components/translations/tests/browser/browser_translations_remote_settings.js b/toolkit/components/translations/tests/browser/browser_translations_remote_settings.js index 93571f70363f..6215f1a8c1cb 100644 --- a/toolkit/components/translations/tests/browser/browser_translations_remote_settings.js +++ b/toolkit/components/translations/tests/browser/browser_translations_remote_settings.js @@ -143,9 +143,10 @@ add_task(async function test_get_records_with_multiple_versions() { ); const lookupKey = record => - `${record.name}${TranslationsParent.languagePairKey( + `${record.name}${TranslationsParent.nonPivotKey( record.fromLang, - record.toLang + record.toLang, + record.variant )}`; // A mapping of each record name to its max version. diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js index 19209cb44c95..bd3f0aa90bb6 100644 --- a/toolkit/components/translations/tests/browser/shared-head.js +++ b/toolkit/components/translations/tests/browser/shared-head.js @@ -1004,6 +1004,9 @@ async function loadTestPage({ prefs, autoOffer, permissionsUrls, + systemLocales = ["en"], + appLocales, + webLanguages, win = window, }) { info(`Loading test page starting at url: ${page}`); @@ -1069,6 +1072,15 @@ async function loadTestPage({ TranslationsParent.testAutomaticPopup = true; } + let cleanupLocales; + if (systemLocales || appLocales || webLanguages) { + cleanupLocales = await mockLocales({ + systemLocales, + appLocales, + webLanguages, + }); + } + // Start the tab at a blank page. const tab = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, @@ -1175,6 +1187,9 @@ async function loadTestPage({ await loadBlankPage(); await EngineProcess.destroyTranslationsEngine(); await removeMocks(); + if (cleanupLocales) { + await cleanupLocales(); + } restoreA11yUtils(); Services.fog.testResetFOG(); TranslationsParent.testAutomaticPopup = false; diff --git a/toolkit/components/translations/translations.d.ts b/toolkit/components/translations/translations.d.ts index 6da52c4fc111..2c8e8576c823 100644 --- a/toolkit/components/translations/translations.d.ts +++ b/toolkit/components/translations/translations.d.ts @@ -36,6 +36,9 @@ export interface TranslationModelRecord { fromLang: string; // The BCP 47 language tag, e.g. "en" 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 version: string; // e.g. "lex" @@ -209,6 +212,7 @@ interface LanguageTranslationModelFile { interface TranslationModelPayload { sourceLanguage: string, targetLanguage: string, + variant?: string, languageModelFiles: LanguageTranslationModelFiles, }; @@ -264,16 +268,43 @@ export interface LangTags { 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 * for translation language selection. */ export interface SupportedLanguages { - languagePairs: LanguagePair[], - fromLanguages: Array<{ langTag: string, displayName: string, }>, - toLanguages: Array<{ langTag: string, displayName: string }>, + languagePairs: NonPivotLanguagePair[], + sourceLanguages: Array, + targetLanguages: Array, } export type TranslationErrors = "engine-load-error"; @@ -283,23 +314,25 @@ export type SelectTranslationsPanelState = | { phase: "closed"; } // 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. // 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. - | { 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. - | { phase: "translatable"; fromLanguage: string; toLanguage: string, sourceText: string, } + | { phase: "translatable"; sourceLanguage: string; targetLanguage: string, sourceText: string, } // 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. - | { 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. - | { phase: "unsupported"; detectedLanguage: string; toLanguage: string, sourceText: string } + | { phase: "unsupported"; detectedLanguage: string; targetLanguage: string, sourceText: string } + +export type RequestTranslationsPort = (languagePair: LanguagePair) => Promise