tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

GeckoViewTranslations.sys.mjs (19905B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
      9  TranslationsUtils:
     10    "chrome://global/content/translations/TranslationsUtils.mjs",
     11 });
     12 
     13 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
     14 
     15 export class GeckoViewTranslations extends GeckoViewModule {
     16  onInit() {
     17    debug`onInit`;
     18    this.registerListener([
     19      "GeckoView:Translations:Translate",
     20      "GeckoView:Translations:RestorePage",
     21      "GeckoView:Translations:GetNeverTranslateSite",
     22      "GeckoView:Translations:SetNeverTranslateSite",
     23    ]);
     24  }
     25 
     26  onEnable() {
     27    debug`onEnable`;
     28    this.window.addEventListener("TranslationsParent:OfferTranslation", this);
     29    this.window.addEventListener("TranslationsParent:LanguageState", this);
     30  }
     31 
     32  onDisable() {
     33    debug`onDisable`;
     34    this.window.removeEventListener(
     35      "TranslationsParent:OfferTranslation",
     36      this
     37    );
     38    this.window.removeEventListener("TranslationsParent:LanguageState", this);
     39  }
     40 
     41  onEvent(aEvent, aData, aCallback) {
     42    debug`onEvent: event=${aEvent}, data=${aData}`;
     43    switch (aEvent) {
     44      case "GeckoView:Translations:Translate": {
     45        try {
     46          const { sourceLanguage, targetLanguage } = aData;
     47 
     48          if (
     49            lazy.TranslationsUtils.isLangTagValid(sourceLanguage) &&
     50            lazy.TranslationsUtils.isLangTagValid(targetLanguage)
     51          ) {
     52            this.getActor("Translations")
     53              .translate(
     54                {
     55                  sourceLanguage,
     56                  targetLanguage,
     57                  // Model variants are not currently supported. See Bug 1943444.
     58                },
     59                /* reportAsAutoTranslate */ false
     60              )
     61              .then(
     62                () => aCallback.onSuccess(),
     63                error => aCallback.onError(`Could not translate: ${error}`)
     64              );
     65          } else {
     66            aCallback.onError(
     67              `The language tag ${sourceLanguage} or ${targetLanguage} is not valid.`
     68            );
     69          }
     70        } catch (error) {
     71          aCallback.onError(`Could not translate: ${error}`);
     72        }
     73        break;
     74      }
     75 
     76      case "GeckoView:Translations:RestorePage":
     77        try {
     78          this.getActor("Translations").restorePage();
     79          aCallback.onSuccess();
     80        } catch (error) {
     81          aCallback.onError(`Could not restore page: ${error}`);
     82        }
     83        break;
     84 
     85      case "GeckoView:Translations:GetNeverTranslateSite":
     86        try {
     87          const value =
     88            this.getActor("Translations").shouldNeverTranslateSite();
     89          aCallback.onSuccess(value);
     90        } catch (error) {
     91          aCallback.onError(`Could not set site setting: ${error}`);
     92        }
     93        break;
     94 
     95      case "GeckoView:Translations:SetNeverTranslateSite":
     96        try {
     97          this.getActor("Translations").setNeverTranslateSitePermissions(
     98            aData.neverTranslate
     99          );
    100          aCallback.onSuccess();
    101        } catch (error) {
    102          aCallback.onError(`Could not set site setting: ${error}`);
    103        }
    104        break;
    105    }
    106  }
    107 
    108  handleEvent(aEvent) {
    109    debug`handleEvent: ${aEvent.type}`;
    110    switch (aEvent.type) {
    111      case "TranslationsParent:OfferTranslation":
    112        this.eventDispatcher.sendRequest({
    113          type: "GeckoView:Translations:Offer",
    114        });
    115        break;
    116      case "TranslationsParent:LanguageState": {
    117        const {
    118          detectedLanguages,
    119          requestedLanguagePair,
    120          hasVisibleChange,
    121          error,
    122          isEngineReady,
    123        } = aEvent.detail.actor.languageState;
    124 
    125        const data = {
    126          detectedLanguages,
    127          requestedLanguagePair,
    128          hasVisibleChange,
    129          error,
    130          isEngineReady,
    131        };
    132 
    133        this.eventDispatcher.sendRequest({
    134          type: "GeckoView:Translations:StateChange",
    135          data,
    136        });
    137 
    138        break;
    139      }
    140    }
    141  }
    142 }
    143 
    144 // Runtime functionality
    145 export const GeckoViewTranslationsSettings = {
    146  // Helper method for retrieving language setting state and corresponding string name.
    147  _getLanguageSettingName(langTag) {
    148    const isAlways = lazy.TranslationsParent.shouldAlwaysTranslateLanguage({
    149      docLangTag: langTag,
    150      userLangTag: new Intl.Locale(Services.locale.appLocaleAsBCP47).language,
    151    });
    152    const isNever =
    153      lazy.TranslationsParent.shouldNeverTranslateLanguage(langTag);
    154    // Default setting is offer.
    155    let setting = "offer";
    156 
    157    if (isAlways & !isNever) {
    158      setting = "always";
    159    }
    160 
    161    if (isNever & !isAlways) {
    162      setting = "never";
    163    }
    164    return setting;
    165  },
    166  /* eslint-disable complexity */
    167  async onEvent(aEvent, aData, aCallback) {
    168    debug`onEvent ${aEvent} ${aData}`;
    169 
    170    switch (aEvent) {
    171      case "GeckoView:Translations:IsTranslationEngineSupported": {
    172        try {
    173          aCallback.onSuccess(
    174            lazy.TranslationsParent.getIsTranslationsEngineSupported()
    175          );
    176        } catch (error) {
    177          aCallback.onError(
    178            `An issue occurred while checking the translations engine: ${error}`
    179          );
    180        }
    181        return;
    182      }
    183      case "GeckoView:Translations:PreferredLanguages": {
    184        aCallback.onSuccess({
    185          preferredLanguages: lazy.TranslationsParent.getPreferredLanguages(),
    186        });
    187        return;
    188      }
    189      case "GeckoView:Translations:ManageModel": {
    190        const { language, operation, operationLevel } = aData;
    191        if (operation === "delete") {
    192          if (operationLevel === "all") {
    193            lazy.TranslationsParent.deleteAllLanguageFiles().then(
    194              function () {
    195                aCallback.onSuccess();
    196              },
    197              function (error) {
    198                aCallback.onError(
    199                  `COULD_NOT_DELETE - An issue occurred while deleting all language files: ${error}`
    200                );
    201              }
    202            );
    203            return;
    204          }
    205          if (operationLevel === "language") {
    206            if (language === undefined) {
    207              aCallback.onError(
    208                `LANGUAGE_REQUIRED - A specified language is required language level operations.`
    209              );
    210              return;
    211            }
    212            lazy.TranslationsParent.deleteLanguageFiles(language).then(
    213              function () {
    214                aCallback.onSuccess();
    215              },
    216              function (error) {
    217                aCallback.onError(
    218                  `COULD_NOT_DELETE - An issue occurred while deleting a language file: ${error}`
    219                );
    220              }
    221            );
    222          }
    223          if (operationLevel === "cache") {
    224            await lazy.TranslationsParent.deleteCachedLanguageFiles().then(
    225              function () {
    226                aCallback.onSuccess();
    227              },
    228              function (error) {
    229                aCallback.onError(
    230                  `COULD_NOT_DELETE - An issue occurred while deleting the cache: ${error}`
    231                );
    232              }
    233            );
    234          }
    235        } else if (operation === "download") {
    236          if (operationLevel === "all") {
    237            lazy.TranslationsParent.downloadAllFiles().then(
    238              function () {
    239                aCallback.onSuccess();
    240              },
    241              function (error) {
    242                aCallback.onError(
    243                  `COULD_NOT_DOWNLOAD - An issue occurred while downloading all language files: ${error}`
    244                );
    245              }
    246            );
    247            return;
    248          }
    249          if (operationLevel === "language") {
    250            if (language === undefined) {
    251              aCallback.onError(
    252                `LANGUAGE_REQUIRED - A specified language is required language level operations.`
    253              );
    254              return;
    255            }
    256            lazy.TranslationsParent.downloadLanguageFiles(language).then(
    257              function () {
    258                aCallback.onSuccess();
    259              },
    260              function (error) {
    261                aCallback.onError(
    262                  `COULD_NOT_DOWNLOAD - An issue occurred while downloading a language files: ${error}`
    263                );
    264              }
    265            );
    266          }
    267          if (operationLevel === "cache") {
    268            aCallback.onError(
    269              `COULD_NOT_DOWNLOAD - Downloading the cache is not a valid option. Please check the parameters and try again.
    270               Language: ${language}, Operation: ${operation}, Operation Level: ${operationLevel}`
    271            );
    272          }
    273        } else {
    274          aCallback.onError(
    275            `ERROR_UNKNOWN - The request to manage models appears to be malformed. Please check the parameters and try again.
    276            Language: ${language}, Operation: ${operation}, Operation Level: ${operationLevel}`
    277          );
    278        }
    279        break;
    280      }
    281      case "GeckoView:Translations:TranslationInformation": {
    282        if (
    283          Cu.isInAutomation &&
    284          Services.prefs.getBoolPref(
    285            "browser.translations.geckoview.enableAllTestMocks",
    286            false
    287          )
    288        ) {
    289          const mockResult = {
    290            languagePairs: [
    291              { sourceLanguage: "en", targetLanguage: "es" },
    292              { sourceLanguage: "es", targetLanguage: "en" },
    293              { sourceLanguage: "en", targetLanguage: "es", variant: "base" },
    294            ],
    295            sourceLanguages: [
    296              { langTag: "en", langTagKey: "en", displayName: "English" },
    297              { langTag: "es", langTagKey: "es", displayName: "Spanish" },
    298            ],
    299            targetLanguages: [
    300              { langTag: "en", langTagKey: "en", displayName: "English" },
    301              { langTag: "es", langTagKey: "en", displayName: "Spanish" },
    302            ],
    303          };
    304          aCallback.onSuccess(mockResult);
    305          return;
    306        }
    307 
    308        lazy.TranslationsParent.getSupportedLanguages().then(
    309          function (value) {
    310            aCallback.onSuccess(value);
    311          },
    312          function (error) {
    313            aCallback.onError(
    314              `Could not retrieve requested information: ${error}`
    315            );
    316          }
    317        );
    318        break;
    319      }
    320      case "GeckoView:Translations:ModelInformation": {
    321        if (
    322          Cu.isInAutomation &&
    323          Services.prefs.getBoolPref(
    324            "browser.translations.geckoview.enableAllTestMocks",
    325            false
    326          )
    327        ) {
    328          const mockResult = {
    329            models: [
    330              {
    331                langTag: "es",
    332                displayName: "Spanish",
    333                isDownloaded: false,
    334                size: 12345,
    335              },
    336              {
    337                langTag: "de",
    338                displayName: "German",
    339                isDownloaded: false,
    340                size: 12345,
    341              },
    342            ],
    343          };
    344          aCallback.onSuccess(mockResult);
    345          return;
    346        }
    347 
    348        // Helper function to process remote server records size and download state for GV use
    349        async function _processLanguageModelData(language, remoteRecords) {
    350          // Aggregate size of downloads, e.g., one language has many model binary files
    351          let size = 0;
    352          remoteRecords.forEach(item => {
    353            size += parseInt(item.attachment.size);
    354          });
    355          // Check if required files are downloaded
    356          const isDownloaded =
    357            await lazy.TranslationsParent.hasAllFilesForLanguage(
    358              language.langTag
    359            );
    360          const model = {
    361            langTag: language.langTag,
    362            displayName: language.displayName,
    363            isDownloaded,
    364            size,
    365          };
    366          return model;
    367        }
    368 
    369        // Main call to toolkit
    370        lazy.TranslationsParent.getSupportedLanguages().then(
    371          // Retrieve supported languages
    372          async function (supportedLanguages) {
    373            // Get language display information,
    374            const languageList =
    375              lazy.TranslationsParent.getLanguageList(supportedLanguages);
    376            const results = [];
    377            const pivotIsDownloaded =
    378              await lazy.TranslationsParent.hasAllFilesForLanguage(
    379                lazy.TranslationsParent.PIVOT_LANGUAGE
    380              );
    381 
    382            // For each language, process the related remote server model records
    383            languageList.forEach(language => {
    384              // No need to include the pivot in the download size, once it has been downloaded.
    385              const recordsResult =
    386                lazy.TranslationsParent.getRecordsForTranslatingToAndFromAppLanguage(
    387                  language.langTag,
    388                  /* includePivotRecords */ !pivotIsDownloaded
    389                ).then(
    390                  async function (records) {
    391                    return _processLanguageModelData(language, records);
    392                  },
    393                  function (recordError) {
    394                    aCallback.onError(
    395                      `An issue occurred while aggregating information: ${recordError}`
    396                    );
    397                  }
    398                );
    399              results.push(recordsResult);
    400            });
    401            // Aggregate records
    402            Promise.all(results).then(models => {
    403              const response = [];
    404              models.forEach(item => {
    405                // Ensures unactionable models do not appear in the list
    406                if (parseInt(item.size) !== 0) {
    407                  response.push(item);
    408                }
    409              });
    410              aCallback.onSuccess({ models: response });
    411            });
    412          },
    413          function (languageError) {
    414            aCallback.onError(
    415              `An issue occurred while retrieving the supported languages: ${languageError}`
    416            );
    417          }
    418        );
    419        break;
    420      }
    421 
    422      case "GeckoView:Translations:GetLanguageSetting": {
    423        if (
    424          Cu.isInAutomation &&
    425          Services.prefs.getBoolPref(
    426            "browser.translations.geckoview.enableAllTestMocks",
    427            false
    428          )
    429        ) {
    430          aCallback.onSuccess("always");
    431          return;
    432        }
    433 
    434        try {
    435          const setting = this._getLanguageSettingName(aData.language);
    436          aCallback.onSuccess(setting);
    437        } catch (error) {
    438          aCallback.onError(`Could not get language setting: ${error}`);
    439        }
    440        break;
    441      }
    442 
    443      case "GeckoView:Translations:GetLanguageSettings": {
    444        if (
    445          Cu.isInAutomation &&
    446          Services.prefs.getBoolPref(
    447            "browser.translations.geckoview.enableAllTestMocks",
    448            false
    449          )
    450        ) {
    451          const mockResult = {
    452            settings: [
    453              { langTag: "fr", displayName: "French", setting: "always" },
    454              { langTag: "de", displayName: "German", setting: "offer" },
    455              { langTag: "es", displayName: "Spanish", setting: "never" },
    456            ],
    457          };
    458          aCallback.onSuccess(mockResult);
    459          return;
    460        }
    461 
    462        lazy.TranslationsParent.getSupportedLanguages().then(
    463          function (supportedLanguages) {
    464            const languageList =
    465              lazy.TranslationsParent.getLanguageList(supportedLanguages);
    466 
    467            languageList.forEach(language => {
    468              language.setting = this._getLanguageSettingName(language.langTag);
    469            });
    470 
    471            aCallback.onSuccess({ settings: languageList });
    472          }.bind(this),
    473          function (error) {
    474            aCallback.onError(
    475              `Could not retrieve language setting information: ${error}`
    476            );
    477          }
    478        );
    479        break;
    480      }
    481 
    482      case "GeckoView:Translations:SetLanguageSettings": {
    483        let { language, languageSetting } = aData;
    484        languageSetting = languageSetting.toLowerCase();
    485 
    486        if (!lazy.TranslationsUtils.isLangTagValid(aData.language)) {
    487          aCallback.onError(`The language tag ${language} is not valid.`);
    488          return;
    489        }
    490 
    491        const ALWAYS = lazy.TranslationsParent.ALWAYS_TRANSLATE_LANGS_PREF;
    492        const NEVER = lazy.TranslationsParent.NEVER_TRANSLATE_LANGS_PREF;
    493 
    494        switch (languageSetting) {
    495          case "always": {
    496            try {
    497              lazy.TranslationsParent.removeLangTagFromPref(language, NEVER);
    498              lazy.TranslationsParent.addLangTagToPref(language, ALWAYS);
    499              aCallback.onSuccess();
    500            } catch (error) {
    501              aCallback.onError(
    502                `Could not set language preference to always: ${error}`
    503              );
    504            }
    505            break;
    506          }
    507 
    508          case "never": {
    509            try {
    510              lazy.TranslationsParent.removeLangTagFromPref(language, ALWAYS);
    511              lazy.TranslationsParent.addLangTagToPref(language, NEVER);
    512              aCallback.onSuccess();
    513            } catch (error) {
    514              aCallback.onError(
    515                `Could not set language preference to never: ${error}`
    516              );
    517            }
    518            break;
    519          }
    520 
    521          case "offer": {
    522            try {
    523              // Reverting to default settings, so ensure nothing is set.
    524              lazy.TranslationsParent.removeLangTagFromPref(language, NEVER);
    525              lazy.TranslationsParent.removeLangTagFromPref(language, ALWAYS);
    526              aCallback.onSuccess();
    527            } catch (error) {
    528              aCallback.onError(
    529                `Could not set language preference to offer: ${error}`
    530              );
    531            }
    532            break;
    533          }
    534        }
    535        break;
    536      }
    537 
    538      case "GeckoView:Translations:GetNeverTranslateSpecifiedSites":
    539        try {
    540          const neverTranslateList =
    541            lazy.TranslationsParent.listNeverTranslateSites();
    542          aCallback.onSuccess({ sites: neverTranslateList });
    543        } catch (error) {
    544          aCallback.onError(
    545            `Could not get list of never translate sites: ${error}`
    546          );
    547        }
    548        break;
    549 
    550      case "GeckoView:Translations:SetNeverTranslateSpecifiedSite":
    551        try {
    552          lazy.TranslationsParent.setNeverTranslateSiteByOrigin(
    553            aData.neverTranslate,
    554            aData.origin
    555          );
    556          aCallback.onSuccess();
    557        } catch (error) {
    558          aCallback.onError(
    559            `Could not set never translate site setting: ${error}`
    560          );
    561        }
    562        break;
    563      case "GeckoView:Translations:GetTranslateDownloadSize": {
    564        if (
    565          Cu.isInAutomation &&
    566          Services.prefs.getBoolPref(
    567            "browser.translations.geckoview.enableAllTestMocks",
    568            false
    569          )
    570        ) {
    571          aCallback.onSuccess({ bytes: 1234567 });
    572          return;
    573        }
    574 
    575        const fromLangValid = lazy.TranslationsUtils.isLangTagValid(
    576          aData.fromLanguage
    577        );
    578        const toLangValid = lazy.TranslationsUtils.isLangTagValid(
    579          aData.toLanguage
    580        );
    581        if (!fromLangValid || !toLangValid) {
    582          aCallback.onError(
    583            `The language tag ${aData.fromLanguage} or ${aData.toLanguage} is not valid.`
    584          );
    585          return;
    586        }
    587 
    588        lazy.TranslationsParent.getExpectedTranslationDownloadSize(
    589          aData.fromLanguage,
    590          aData.toLanguage
    591        ).then(
    592          function (bytes) {
    593            aCallback.onSuccess({ bytes });
    594          },
    595          function (error) {
    596            aCallback.onError(`Could not get the download size: ${error}`);
    597          }
    598        );
    599        break;
    600      }
    601    }
    602  },
    603 };
    604 
    605 const { debug, warn } = GeckoViewTranslations.initLogging(
    606  "GeckoViewTranslations"
    607 );