summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs')
-rw-r--r--mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs572
1 files changed, 572 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs b/mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs
new file mode 100644
index 0000000000..3db694a64a
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs
@@ -0,0 +1,572 @@
+/* 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,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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);
+ 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 {
+ var 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,
+ error,
+ isEngineReady,
+ } = aEvent.detail.actor.languageState;
+
+ const data = {
+ detectedLanguages,
+ requestedTranslationPair,
+ 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.
+ var 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
+ var 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 (value) {
+ 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 (value) {
+ aCallback.onSuccess();
+ },
+ function (error) {
+ aCallback.onError(
+ `COULD_NOT_DELETE - An issue occurred while deleting a language file: ${error}`
+ );
+ }
+ );
+ return;
+ }
+ }
+ if (operation === "download") {
+ if (operationLevel === "all") {
+ lazy.TranslationsParent.downloadAllFiles().then(
+ function (value) {
+ 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 (value) {
+ aCallback.onSuccess();
+ },
+ function (error) {
+ aCallback.onError(
+ `COULD_NOT_DOWNLOAD - An issue occurred while downloading a language files: ${error}`
+ );
+ }
+ );
+ }
+ }
+ 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
+ var size = 0;
+ remoteRecords.forEach(item => {
+ size += parseInt(item.attachment.size);
+ });
+ // Check if required files are downloaded
+ var isDownloaded =
+ await lazy.TranslationsParent.hasAllFilesForLanguage(
+ language.langTag
+ );
+ var 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);
+ var results = [];
+ // For each language, process the related remote server model records
+ languageList.forEach(language => {
+ const recordsResult =
+ lazy.TranslationsParent.getRecordsForTranslatingToAndFromAppLanguage(
+ language.langTag,
+ false
+ ).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 => {
+ var response = [];
+ models.forEach(item => {
+ 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 {
+ var 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": {
+ var { 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"
+);