Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
});
import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
export class GeckoViewTranslations extends GeckoViewModule {
onInit() {
debug`onInit`;
this.registerListener([
"GeckoView:Translations:Translate",
"GeckoView:Translations:RestorePage",
"GeckoView:Translations:GetNeverTranslateSite",
"GeckoView:Translations:SetNeverTranslateSite",
]);
}
onEnable() {
debug`onEnable`;
this.window.addEventListener("TranslationsParent:OfferTranslation", this);
this.window.addEventListener("TranslationsParent:LanguageState", this);
}
onDisable() {
debug`onDisable`;
this.window.removeEventListener(
"TranslationsParent:OfferTranslation",
this
);
this.window.removeEventListener("TranslationsParent:LanguageState", this);
}
onEvent(aEvent, aData, aCallback) {
debug`onEvent: event=${aEvent}, data=${aData}`;
switch (aEvent) {
case "GeckoView:Translations:Translate":
try {
const fromLanguage =
GeckoViewTranslationsSettings._checkValidLanguageTagAndMinimize(
aData.fromLanguage
);
const toLanguage =
GeckoViewTranslationsSettings._checkValidLanguageTagAndMinimize(
aData.toLanguage
);
try {
this.getActor("Translations")
.translate(
fromLanguage,
toLanguage,
/* reportAsAutoTranslate */ false
)
.then(() => {
aCallback.onSuccess();
});
} catch (error) {
aCallback.onError(`Could not translate: ${error}`);
}
} catch (error) {
aCallback.onError(
`The language tag ${aData.fromLanguage} or ${aData.toLanguage} is not valid: ${error}`
);
}
break;
case "GeckoView:Translations:RestorePage":
try {
this.getActor("Translations").restorePage();
aCallback.onSuccess();
} catch (error) {
aCallback.onError(`Could not restore page: ${error}`);
}
break;
case "GeckoView:Translations:GetNeverTranslateSite":
try {
const value =
this.getActor("Translations").shouldNeverTranslateSite();
aCallback.onSuccess(value);
} catch (error) {
aCallback.onError(`Could not set site setting: ${error}`);
}
break;
case "GeckoView:Translations:SetNeverTranslateSite":
try {
this.getActor("Translations").setNeverTranslateSitePermissions(
aData.neverTranslate
);
aCallback.onSuccess();
} catch (error) {
aCallback.onError(`Could not set site setting: ${error}`);
}
break;
}
}
handleEvent(aEvent) {
debug`handleEvent: ${aEvent.type}`;
switch (aEvent.type) {
case "TranslationsParent:OfferTranslation":
this.eventDispatcher.sendRequest({
type: "GeckoView:Translations:Offer",
});
break;
case "TranslationsParent:LanguageState": {
const {
detectedLanguages,
requestedTranslationPair,
hasVisibleChange,
error,
isEngineReady,
} = aEvent.detail.actor.languageState;
const data = {
detectedLanguages,
requestedTranslationPair,
hasVisibleChange,
error,
isEngineReady,
};
this.eventDispatcher.sendRequest({
type: "GeckoView:Translations:StateChange",
data,
});
break;
}
}
}
}
// Runtime functionality
export const GeckoViewTranslationsSettings = {
// Helper method for retrieving language setting state and corresponding string name.
_getLanguageSettingName(langTag) {
const isAlways = lazy.TranslationsParent.shouldAlwaysTranslateLanguage({
docLangTag: langTag,
userLangTag: new Intl.Locale(Services.locale.appLocaleAsBCP47).language,
});
const isNever =
lazy.TranslationsParent.shouldNeverTranslateLanguage(langTag);
// Default setting is offer.
let setting = "offer";
if (isAlways & !isNever) {
setting = "always";
}
if (isNever & !isAlways) {
setting = "never";
}
return setting;
},
// Helper method to validate BCP 47 tags and reduced to only the language portion. For example, en-US will be reduced to en.
_checkValidLanguageTagAndMinimize(langTag) {
// Formats the langTag into a locale, may throw an error
const canonicalTag = new Intl.Locale(Intl.getCanonicalLocales(langTag)[0]);
return canonicalTag.minimize().toString();
},
/* eslint-disable complexity */
async onEvent(aEvent, aData, aCallback) {
debug`onEvent ${aEvent} ${aData}`;
switch (aEvent) {
case "GeckoView:Translations:IsTranslationEngineSupported": {
try {
aCallback.onSuccess(
lazy.TranslationsParent.getIsTranslationsEngineSupported()
);
} catch (error) {
aCallback.onError(
`An issue occurred while checking the translations engine: ${error}`
);
}
return;
}
case "GeckoView:Translations:PreferredLanguages": {
aCallback.onSuccess({
preferredLanguages: lazy.TranslationsParent.getPreferredLanguages(),
});
return;
}
case "GeckoView:Translations:ManageModel": {
const { language, operation, operationLevel } = aData;
if (operation === "delete") {
if (operationLevel === "all") {
lazy.TranslationsParent.deleteAllLanguageFiles().then(
function () {
aCallback.onSuccess();
},
function (error) {
aCallback.onError(
`COULD_NOT_DELETE - An issue occurred while deleting all language files: ${error}`
);
}
);
return;
}
if (operationLevel === "language") {
if (language === undefined) {
aCallback.onError(
`LANGUAGE_REQUIRED - A specified language is required language level operations.`
);
return;
}
lazy.TranslationsParent.deleteLanguageFiles(language).then(
function () {
aCallback.onSuccess();
},
function (error) {
aCallback.onError(
`COULD_NOT_DELETE - An issue occurred while deleting a language file: ${error}`
);
}
);
}
if (operationLevel === "cache") {
await lazy.TranslationsParent.deleteCachedLanguageFiles().then(
function () {
aCallback.onSuccess();
},
function (error) {
aCallback.onError(
`COULD_NOT_DELETE - An issue occurred while deleting the cache: ${error}`
);
}
);
}
} else if (operation === "download") {
if (operationLevel === "all") {
lazy.TranslationsParent.downloadAllFiles().then(
function () {
aCallback.onSuccess();
},
function (error) {
aCallback.onError(
`COULD_NOT_DOWNLOAD - An issue occurred while downloading all language files: ${error}`
);
}
);
return;
}
if (operationLevel === "language") {
if (language === undefined) {
aCallback.onError(
`LANGUAGE_REQUIRED - A specified language is required language level operations.`
);
return;
}
lazy.TranslationsParent.downloadLanguageFiles(language).then(
function () {
aCallback.onSuccess();
},
function (error) {
aCallback.onError(
`COULD_NOT_DOWNLOAD - An issue occurred while downloading a language files: ${error}`
);
}
);
}
if (operationLevel === "cache") {
aCallback.onError(
`COULD_NOT_DOWNLOAD - Downloading the cache is not a valid option. Please check the parameters and try again.
Language: ${language}, Operation: ${operation}, Operation Level: ${operationLevel}`
);
}
} else {
aCallback.onError(
`ERROR_UNKNOWN - The request to manage models appears to be malformed. Please check the parameters and try again.
Language: ${language}, Operation: ${operation}, Operation Level: ${operationLevel}`
);
}
break;
}
case "GeckoView:Translations:TranslationInformation": {
if (
Cu.isInAutomation &&
Services.prefs.getBoolPref(
"browser.translations.geckoview.enableAllTestMocks",
false
)
) {
const mockResult = {
languagePairs: [
{ fromLang: "en", toLang: "es" },
{ fromLang: "es", toLang: "en" },
],
fromLanguages: [
{ langTag: "en", displayName: "English" },
{ langTag: "es", displayName: "Spanish" },
],
toLanguages: [
{ langTag: "en", displayName: "English" },
{ langTag: "es", displayName: "Spanish" },
],
};
aCallback.onSuccess(mockResult);
return;
}
lazy.TranslationsParent.getSupportedLanguages().then(
function (value) {
aCallback.onSuccess(value);
},
function (error) {
aCallback.onError(
`Could not retrieve requested information: ${error}`
);
}
);
break;
}
case "GeckoView:Translations:ModelInformation": {
if (
Cu.isInAutomation &&
Services.prefs.getBoolPref(
"browser.translations.geckoview.enableAllTestMocks",
false
)
) {
const mockResult = {
models: [
{
langTag: "es",
displayName: "Spanish",
isDownloaded: false,
size: 12345,
},
{
langTag: "de",
displayName: "German",
isDownloaded: false,
size: 12345,
},
],
};
aCallback.onSuccess(mockResult);
return;
}
// Helper function to process remote server records size and download state for GV use
async function _processLanguageModelData(language, remoteRecords) {
// Aggregate size of downloads, e.g., one language has many model binary files
let size = 0;
remoteRecords.forEach(item => {
size += parseInt(item.attachment.size);
});
// Check if required files are downloaded
const isDownloaded =
await lazy.TranslationsParent.hasAllFilesForLanguage(
language.langTag
);
const model = {
langTag: language.langTag,
displayName: language.displayName,
isDownloaded,
size,
};
return model;
}
// Main call to toolkit
lazy.TranslationsParent.getSupportedLanguages().then(
// Retrieve supported languages
async function (supportedLanguages) {
// Get language display information,
const languageList =
lazy.TranslationsParent.getLanguageList(supportedLanguages);
const results = [];
const pivotIsDownloaded =
await lazy.TranslationsParent.hasAllFilesForLanguage(
lazy.TranslationsParent.PIVOT_LANGUAGE
);
// For each language, process the related remote server model records
languageList.forEach(language => {
// No need to include the pivot in the download size, once it has been downloaded.
const recordsResult =
lazy.TranslationsParent.getRecordsForTranslatingToAndFromAppLanguage(
language.langTag,
/* includePivotRecords */ !pivotIsDownloaded
).then(
async function (records) {
return _processLanguageModelData(language, records);
},
function (recordError) {
aCallback.onError(
`An issue occurred while aggregating information: ${recordError}`
);
},
language
);
results.push(recordsResult);
});
// Aggregate records
Promise.all(results).then(models => {
const response = [];
models.forEach(item => {
// Ensures unactionable models do not appear in the list
if (parseInt(item.size) !== 0) {
response.push(item);
}
});
aCallback.onSuccess({ models: response });
});
},
function (languageError) {
aCallback.onError(
`An issue occurred while retrieving the supported languages: ${languageError}`
);
}
);
break;
}
case "GeckoView:Translations:GetLanguageSetting": {
if (
Cu.isInAutomation &&
Services.prefs.getBoolPref(
"browser.translations.geckoview.enableAllTestMocks",
false
)
) {
aCallback.onSuccess("always");
return;
}
try {
const setting = this._getLanguageSettingName(aData.language);
aCallback.onSuccess(setting);
} catch (error) {
aCallback.onError(`Could not get language setting: ${error}`);
}
break;
}
case "GeckoView:Translations:GetLanguageSettings": {
if (
Cu.isInAutomation &&
Services.prefs.getBoolPref(
"browser.translations.geckoview.enableAllTestMocks",
false
)
) {
const mockResult = {
settings: [
{ langTag: "fr", displayName: "French", setting: "always" },
{ langTag: "de", displayName: "German", setting: "offer" },
{ langTag: "es", displayName: "Spanish", setting: "never" },
],
};
aCallback.onSuccess(mockResult);
return;
}
lazy.TranslationsParent.getSupportedLanguages().then(
function (supportedLanguages) {
const languageList =
lazy.TranslationsParent.getLanguageList(supportedLanguages);
languageList.forEach(language => {
language.setting = this._getLanguageSettingName(language.langTag);
});
aCallback.onSuccess({ settings: languageList });
}.bind(this),
function (error) {
aCallback.onError(
`Could not retrieve language setting information: ${error}`
);
}
);
break;
}
case "GeckoView:Translations:SetLanguageSettings": {
let { language, languageSetting } = aData;
languageSetting = languageSetting.toLowerCase();
try {
language = this._checkValidLanguageTagAndMinimize(language);
} catch (error) {
aCallback.onError(
`The language tag ${language} is not valid: ${error}`
);
return;
}
const ALWAYS = lazy.TranslationsParent.ALWAYS_TRANSLATE_LANGS_PREF;
const NEVER = lazy.TranslationsParent.NEVER_TRANSLATE_LANGS_PREF;
switch (languageSetting) {
case "always": {
try {
lazy.TranslationsParent.removeLangTagFromPref(language, NEVER);
lazy.TranslationsParent.addLangTagToPref(language, ALWAYS);
aCallback.onSuccess();
} catch (error) {
aCallback.onError(
`Could not set language preference to always: ${error}`
);
}
break;
}
case "never": {
try {
lazy.TranslationsParent.removeLangTagFromPref(language, ALWAYS);
lazy.TranslationsParent.addLangTagToPref(language, NEVER);
aCallback.onSuccess();
} catch (error) {
aCallback.onError(
`Could not set language preference to never: ${error}`
);
}
break;
}
case "offer": {
try {
// Reverting to default settings, so ensure nothing is set.
lazy.TranslationsParent.removeLangTagFromPref(language, NEVER);
lazy.TranslationsParent.removeLangTagFromPref(language, ALWAYS);
aCallback.onSuccess();
} catch (error) {
aCallback.onError(
`Could not set language preference to offer: ${error}`
);
}
break;
}
}
break;
}
case "GeckoView:Translations:GetNeverTranslateSpecifiedSites":
try {
const neverTranslateList =
lazy.TranslationsParent.listNeverTranslateSites();
aCallback.onSuccess({ sites: neverTranslateList });
} catch (error) {
aCallback.onError(
`Could not get list of never translate sites: ${error}`
);
}
break;
case "GeckoView:Translations:SetNeverTranslateSpecifiedSite":
try {
lazy.TranslationsParent.setNeverTranslateSiteByOrigin(
aData.neverTranslate,
aData.origin
);
aCallback.onSuccess();
} catch (error) {
aCallback.onError(
`Could not set never translate site setting: ${error}`
);
}
break;
case "GeckoView:Translations:GetTranslateDownloadSize": {
if (
Cu.isInAutomation &&
Services.prefs.getBoolPref(
"browser.translations.geckoview.enableAllTestMocks",
false
)
) {
aCallback.onSuccess({ bytes: 1234567 });
return;
}
try {
const fromLanguage = this._checkValidLanguageTagAndMinimize(
aData.fromLanguage
);
const toLanguage = this._checkValidLanguageTagAndMinimize(
aData.toLanguage
);
lazy.TranslationsParent.getExpectedTranslationDownloadSize(
fromLanguage,
toLanguage
).then(
function (bytes) {
aCallback.onSuccess({ bytes });
},
function (error) {
aCallback.onError(`Could not get the download size: ${error}`);
}
);
} catch (error) {
aCallback.onError(
`The language tag ${aData.fromLanguage} or ${aData.toLanguage} is not valid: ${error}`
);
}
break;
}
}
},
};
const { debug, warn } = GeckoViewTranslations.initLogging(
"GeckoViewTranslations"
);