tor-browser

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

TranslationsPanelShared.sys.mjs (8734B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * @typedef {typeof import("../../../../toolkit/components/translations/actors/TranslationsParent.sys.mjs").TranslationsParent} TranslationsParent
      7 */
      8 
      9 /** @type {{ TranslationsParent: TranslationsParent }} */
     10 const lazy = {};
     11 
     12 ChromeUtils.defineESModuleGetters(lazy, {
     13  TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
     14 });
     15 
     16 /**
     17 * A class containing static functionality that is shared by both
     18 * the FullPageTranslationsPanel and SelectTranslationsPanel classes.
     19 *
     20 * It is recommended to read the documentation above the TranslationsParent class
     21 * definition to understand the scope of the Translations architecture throughout
     22 * Firefox.
     23 *
     24 * @see TranslationsParent
     25 *
     26 * The static instance of this class is a singleton in the parent process, and is
     27 * available throughout all windows and tabs, just like the static instance of
     28 * the TranslationsParent class.
     29 *
     30 * Unlike the TranslationsParent, this class is never instantiated as an actor
     31 * outside of the static-context functionality defined below.
     32 */
     33 export class TranslationsPanelShared {
     34  /**
     35   * A map from Translations Panel instances to their initialized states.
     36   * There is one instance of each panel per top ChromeWindow in Firefox.
     37   *
     38   * See the documentation above the TranslationsParent class for a detailed
     39   * explanation of the translations architecture throughout Firefox.
     40   *
     41   * @see TranslationsParent
     42   *
     43   * @type {Map<FullPageTranslationsPanel | SelectTranslationsPanel, string>}
     44   */
     45  static #langListsInitState = new WeakMap();
     46 
     47  /**
     48   * True if the next language-list initialization to fail for testing.
     49   *
     50   * @see TranslationsPanelShared.ensureLangListsBuilt
     51   *
     52   * @type {boolean}
     53   */
     54  static #simulateLangListError = false;
     55 
     56  /**
     57   * Set to true once we've initialized the observers for this static global class,
     58   * to ensure that we only ever create observers once.
     59   *
     60   * @type {boolean}
     61   */
     62  static #observersInitialized = false;
     63 
     64  /**
     65   * Clears cached data regarding the initialization state of the
     66   * FullPageTranslationsPanel and the SelectTranslationsPanel dropdown menu lists.
     67   *
     68   * This will cause all panels to rebuild their menulist items upon its next open event.
     69   * There exists one SelectTranslationsPanel and one FullPageTranslationsPanel per open
     70   * Firefox window. There are several situations in which this should be called:
     71   *
     72   *  1) In between test cases, which may explicitly test a different set of available languages.
     73   *  2) Whenever the application locale changes, which requires new language display names.
     74   *  3) Whenever a Remote Settings sync changes the list of available languages.
     75   */
     76  static clearLanguageListsCache() {
     77    TranslationsPanelShared.#langListsInitState = new WeakMap();
     78  }
     79 
     80  /**
     81   * Defines lazy getters for accessing elements in the document based on provided entries.
     82   *
     83   * @param {Document} document - The document object.
     84   * @param {object} lazyElements - An object where lazy getters will be defined.
     85   * @param {object} entries - An object of key/value pairs for which to define lazy getters.
     86   */
     87  static defineLazyElements(document, lazyElements, entries) {
     88    for (const [name, discriminator] of Object.entries(entries)) {
     89      let element;
     90      Object.defineProperty(lazyElements, name, {
     91        get: () => {
     92          if (!element) {
     93            if (discriminator[0] === ".") {
     94              // Lookup by class
     95              element = document.querySelector(discriminator);
     96            } else {
     97              // Lookup by id
     98              element = document.getElementById(discriminator);
     99            }
    100          }
    101          if (!element) {
    102            throw new Error(`Could not find "${name}" at "#${discriminator}".`);
    103          }
    104          return element;
    105        },
    106      });
    107    }
    108  }
    109 
    110  /**
    111   * Ensures that the next call to ensureLangListBuilt wil fail
    112   * for the purpose of testing the error state.
    113   *
    114   * @see TranslationsPanelShared.ensureLangListsBuilt
    115   *
    116   * @type {boolean}
    117   */
    118  static simulateLangListError() {
    119    this.#simulateLangListError = true;
    120  }
    121 
    122  /**
    123   * Retrieves the initialization state of language lists for the specified panel.
    124   *
    125   * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
    126   *   - The panel for which to look up the state.
    127   */
    128  static getLangListsInitState(panel) {
    129    return TranslationsPanelShared.#langListsInitState.get(panel);
    130  }
    131 
    132  /**
    133   * Builds the <menulist> of languages for both the "from" and "to". This can be
    134   * called every time the popup is shown, as it will retry when there is an error
    135   * (such as a network error) or be a noop if it's already initialized.
    136   *
    137   * @param {Document} document - The document object.
    138   * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
    139   *   - The panel for which to ensure language lists are built.
    140   */
    141  static async ensureLangListsBuilt(document, panel) {
    142    if (!TranslationsPanelShared.#observersInitialized) {
    143      TranslationsPanelShared.#observersInitialized = true;
    144 
    145      // The language dropdowns must be rebuilt any time the application locale changes.
    146      // Since the dropdowns are dynamically populated with localized language display names,
    147      // we need to repopulate the display names for the new locale.
    148      Services.obs.addObserver(
    149        TranslationsPanelShared.clearLanguageListsCache,
    150        "intl:app-locales-changed"
    151      );
    152 
    153      // The language dropdowns must be rebuilt any time language pairs change.
    154      // This is most often due to a Remote Settings sync, which could be triggered
    155      // due to publishing a new language model, or by changing the Remote Settings channel.
    156      Services.obs.addObserver(
    157        TranslationsPanelShared.clearLanguageListsCache,
    158        "translations:language-pairs-changed"
    159      );
    160    }
    161 
    162    const { panel: panelElement } = panel.elements;
    163    switch (TranslationsPanelShared.#langListsInitState.get(panel)) {
    164      case "initialized":
    165        // This has already been initialized.
    166        return;
    167      case "error":
    168      case undefined:
    169        // Set the error state in case there is an early exit at any point.
    170        // This will be set to "initialized" if everything succeeds.
    171        TranslationsPanelShared.#langListsInitState.set(panel, "error");
    172        break;
    173      default:
    174        throw new Error(
    175          `Unknown langList phase ${
    176            TranslationsPanelShared.#langListsInitState
    177          }`
    178        );
    179    }
    180    /** @type {SupportedLanguages} */
    181    const { languagePairs, sourceLanguages, targetLanguages } =
    182      await lazy.TranslationsParent.getSupportedLanguages();
    183 
    184    // Verify that we are in a proper state.
    185    if (languagePairs.length === 0 || this.#simulateLangListError) {
    186      this.#simulateLangListError = false;
    187      throw new Error("No translation languages were retrieved.");
    188    }
    189 
    190    const fromPopups = panelElement.querySelectorAll(
    191      ".translations-panel-language-menupopup-from"
    192    );
    193    const toPopups = panelElement.querySelectorAll(
    194      ".translations-panel-language-menupopup-to"
    195    );
    196 
    197    for (const popup of fromPopups) {
    198      // For the moment, the FullPageTranslationsPanel includes its own
    199      // menu item for "Choose another language" as the first item in the list
    200      // with an empty-string for its value. The SelectTranslationsPanel has
    201      // only languages in its list with BCP-47 tags for values. As such,
    202      // this loop works for both panels, to remove all of the languages
    203      // from the list, but ensuring that any empty-string items are retained.
    204      while (popup.lastChild?.value) {
    205        popup.lastChild.remove();
    206      }
    207      for (const { langTagKey, displayName } of sourceLanguages) {
    208        const fromMenuItem = document.createXULElement("menuitem");
    209        fromMenuItem.setAttribute("value", langTagKey);
    210        fromMenuItem.setAttribute("label", displayName);
    211        popup.appendChild(fromMenuItem);
    212      }
    213    }
    214 
    215    for (const popup of toPopups) {
    216      while (popup.lastChild?.value) {
    217        popup.lastChild.remove();
    218      }
    219      for (const { langTagKey, displayName } of targetLanguages) {
    220        const toMenuItem = document.createXULElement("menuitem");
    221        toMenuItem.setAttribute("value", langTagKey);
    222        toMenuItem.setAttribute("label", displayName);
    223        popup.appendChild(toMenuItem);
    224      }
    225    }
    226 
    227    TranslationsPanelShared.#langListsInitState.set(panel, "initialized");
    228  }
    229 }