Bug 1942349 - Add support for model variants; r=translations-reviewers,settings-reviewers,mossop,nordzilla
This is a big patch, but it adds the support for model variants by defining a language pair struct instead of a fromLanguage and toLanguage. This struct can also include a source variant and target variant. The code will resolve the pivot models as needed. This is not going to be release user facing, but can be used in a Nightly setting. There is nothing enforcing this, but it's the intention of the code. This will allow us to test multiple variants of models in Nightly and gather feedback from users and other engineers. Differential Revision: https://phabricator.services.mozilla.com/D235495
This commit is contained in:
@@ -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<void>}
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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<MessagePort | undefined>} 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<Translator>} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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<MessagePort | undefined>} 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<Array<LanguagePair>>}
|
||||
* @returns {Promise<Array<NonPivotLanguagePair>>}
|
||||
*/
|
||||
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<string>} */
|
||||
const fromLanguages = new Set();
|
||||
const sourceLanguageKeys = new Set();
|
||||
/** @type {Set<string>} */
|
||||
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<string>} Directional language pairs in the form of "fromLang,toLang" that indicates language pairs that were deleted.
|
||||
* @returns {Set<string>} Directional language pairs in the form of "sourceLanguage,targetLanguage" that indicates language pairs that were deleted.
|
||||
*/
|
||||
static async deleteCachedLanguageFiles() {
|
||||
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<string>} status.downloadedPairs A set of strings that has directionality about what side
|
||||
* is downloaded, in the format "fromLang,toLang".
|
||||
* is downloaded, in the format "sourceLanguage,targetLanguage".
|
||||
* @returns {Set<string>} status.nonDownloadedPairs A set of strings that has directionality about what side
|
||||
* 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<long>} Size in bytes of the expected download. A result of 0 indicates no download is expected for the request.
|
||||
*/
|
||||
static async getExpectedTranslationDownloadSize(fromLanguage, toLanguage) {
|
||||
static async getExpectedTranslationDownloadSize(
|
||||
sourceLanguage,
|
||||
targetLanguage
|
||||
) {
|
||||
const directSize = await this.#getModelDownloadSize(
|
||||
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;
|
||||
|
||||
@@ -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<Translator | PassthroughTranslator>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,10 +53,8 @@ export class LRUCache {
|
||||
#htmlCache = new Map();
|
||||
/** @type {Map<string, string>} */
|
||||
#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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TranslationsEngine>}
|
||||
*/
|
||||
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<TranslationsEngine>}
|
||||
*/
|
||||
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<number, { fromLanguage: string, toLanguage: string, port: MessagePort }>}
|
||||
* @type {Map<number, {
|
||||
* languagePair: LanguagePair,
|
||||
* port: MessagePort
|
||||
* }>}
|
||||
*/
|
||||
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": {
|
||||
|
||||
@@ -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<TranslationModelPayload>} 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() {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<SupportedLanguage>,
|
||||
targetLanguages: Array<SupportedLanguage>,
|
||||
}
|
||||
|
||||
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<MessagePort>
|
||||
|
||||
Reference in New Issue
Block a user