tor-browser

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

fullPageTranslationsPanel.js (53786B)


      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 /* eslint-disable jsdoc/valid-types */
      6 /**
      7 * @typedef {import("../../../../toolkit/components/translations/translations").LangTags} LangTags
      8 */
      9 /* eslint-enable jsdoc/valid-types */
     10 
     11 ChromeUtils.defineESModuleGetters(this, {
     12  PageActions: "resource:///modules/PageActions.sys.mjs",
     13  TranslationsUtils:
     14    "chrome://global/content/translations/TranslationsUtils.mjs",
     15  TranslationsPanelShared:
     16    "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs",
     17 });
     18 
     19 /**
     20 * The set of actions that can occur from interaction with the
     21 * translations panel.
     22 */
     23 const PageAction = Object.freeze({
     24  NO_CHANGE: "NO_CHANGE",
     25  RESTORE_PAGE: "RESTORE_PAGE",
     26  TRANSLATE_PAGE: "TRANSLATE_PAGE",
     27  CLOSE_PANEL: "CLOSE_PANEL",
     28 });
     29 
     30 /**
     31 * A mechanism for determining the next relevant page action
     32 * based on the current translated state of the page and the state
     33 * of the persistent options in the translations panel settings.
     34 */
     35 class CheckboxPageAction {
     36  /**
     37   * Whether or not translations is active on the page.
     38   *
     39   * @type {boolean}
     40   */
     41  #translationsActive = false;
     42 
     43  /**
     44   * Whether the always-translate-language menuitem is checked
     45   * in the translations panel settings menu.
     46   *
     47   * @type {boolean}
     48   */
     49  #alwaysTranslateLanguage = false;
     50 
     51  /**
     52   * Whether the never-translate-language menuitem is checked
     53   * in the translations panel settings menu.
     54   *
     55   * @type {boolean}
     56   */
     57  #neverTranslateLanguage = false;
     58 
     59  /**
     60   * Whether the never-translate-site menuitem is checked
     61   * in the translations panel settings menu.
     62   *
     63   * @type {boolean}
     64   */
     65  #neverTranslateSite = false;
     66 
     67  /**
     68   * @param {boolean} translationsActive
     69   * @param {boolean} alwaysTranslateLanguage
     70   * @param {boolean} neverTranslateLanguage
     71   * @param {boolean} neverTranslateSite
     72   */
     73  constructor(
     74    translationsActive,
     75    alwaysTranslateLanguage,
     76    neverTranslateLanguage,
     77    neverTranslateSite
     78  ) {
     79    this.#translationsActive = translationsActive;
     80    this.#alwaysTranslateLanguage = alwaysTranslateLanguage;
     81    this.#neverTranslateLanguage = neverTranslateLanguage;
     82    this.#neverTranslateSite = neverTranslateSite;
     83  }
     84 
     85  /**
     86   * Accepts four integers that are either 0 or 1 and returns
     87   * a single, unique number for each possible combination of
     88   * values.
     89   *
     90   * @param {number} translationsActive
     91   * @param {number} alwaysTranslateLanguage
     92   * @param {number} neverTranslateLanguage
     93   * @param {number} neverTranslateSite
     94   *
     95   * @returns {number} - An integer representation of the state
     96   */
     97  static #computeState(
     98    translationsActive,
     99    alwaysTranslateLanguage,
    100    neverTranslateLanguage,
    101    neverTranslateSite
    102  ) {
    103    return (
    104      (translationsActive << 3) |
    105      (alwaysTranslateLanguage << 2) |
    106      (neverTranslateLanguage << 1) |
    107      neverTranslateSite
    108    );
    109  }
    110 
    111  /**
    112   * Returns the current state of the data members as a single number.
    113   *
    114   * @returns {number} - An integer representation of the state
    115   */
    116  #state() {
    117    return CheckboxPageAction.#computeState(
    118      Number(this.#translationsActive),
    119      Number(this.#alwaysTranslateLanguage),
    120      Number(this.#neverTranslateLanguage),
    121      Number(this.#neverTranslateSite)
    122    );
    123  }
    124 
    125  /**
    126   * Returns the next page action to take when the always-translate-language
    127   * menuitem is toggled in the translations panel settings menu.
    128   *
    129   * @returns {PageAction}
    130   */
    131  alwaysTranslateLanguage() {
    132    switch (this.#state()) {
    133      case CheckboxPageAction.#computeState(1, 1, 0, 1):
    134      case CheckboxPageAction.#computeState(1, 1, 0, 0):
    135        return PageAction.RESTORE_PAGE;
    136      case CheckboxPageAction.#computeState(0, 0, 1, 0):
    137      case CheckboxPageAction.#computeState(0, 0, 0, 0):
    138        return PageAction.TRANSLATE_PAGE;
    139    }
    140    return PageAction.NO_CHANGE;
    141  }
    142 
    143  /**
    144   * Returns the next page action to take when the never-translate-language
    145   * menuitem is toggled in the translations panel settings menu.
    146   *
    147   * @returns {PageAction}
    148   */
    149  neverTranslateLanguage() {
    150    switch (this.#state()) {
    151      case CheckboxPageAction.#computeState(1, 1, 0, 1):
    152      case CheckboxPageAction.#computeState(1, 1, 0, 0):
    153      case CheckboxPageAction.#computeState(1, 0, 0, 1):
    154      case CheckboxPageAction.#computeState(1, 0, 0, 0):
    155        return PageAction.RESTORE_PAGE;
    156      case CheckboxPageAction.#computeState(0, 1, 0, 0):
    157      case CheckboxPageAction.#computeState(0, 0, 0, 1):
    158      case CheckboxPageAction.#computeState(0, 1, 0, 1):
    159      case CheckboxPageAction.#computeState(0, 0, 0, 0):
    160        return PageAction.CLOSE_PANEL;
    161    }
    162    return PageAction.NO_CHANGE;
    163  }
    164 
    165  /**
    166   * Returns the next page action to take when the never-translate-site
    167   * menuitem is toggled in the translations panel settings menu.
    168   *
    169   * @returns {PageAction}
    170   */
    171  neverTranslateSite() {
    172    switch (this.#state()) {
    173      case CheckboxPageAction.#computeState(1, 1, 0, 0):
    174      case CheckboxPageAction.#computeState(1, 0, 1, 0):
    175      case CheckboxPageAction.#computeState(1, 0, 0, 0):
    176        return PageAction.RESTORE_PAGE;
    177      case CheckboxPageAction.#computeState(0, 1, 0, 1):
    178        return PageAction.TRANSLATE_PAGE;
    179      case CheckboxPageAction.#computeState(0, 0, 1, 0):
    180      case CheckboxPageAction.#computeState(0, 1, 0, 0):
    181      case CheckboxPageAction.#computeState(0, 0, 0, 0):
    182        return PageAction.CLOSE_PANEL;
    183    }
    184    return PageAction.NO_CHANGE;
    185  }
    186 }
    187 
    188 /**
    189 * This singleton class controls the FullPageTranslations panel.
    190 *
    191 * This component is a `/browser` component, and the actor is a `/toolkit` actor, so care
    192 * must be taken to keep the presentation (this component) from the state management
    193 * (the Translations actor). This class reacts to state changes coming from the
    194 * Translations actor.
    195 *
    196 * A global instance of this class is created once per top ChromeWindow and is initialized
    197 * when the new window is created.
    198 *
    199 * See the comment above TranslationsParent for more details.
    200 *
    201 * @see TranslationsParent
    202 */
    203 var FullPageTranslationsPanel = new (class {
    204  /** @type {Console?} */
    205  #console;
    206 
    207  /**
    208   * The cached detected languages for both the document and the user.
    209   *
    210   * @type {null | LangTags}
    211   */
    212  detectedLanguages = null;
    213 
    214  /**
    215   * Lazily get a console instance. Note that this script is loaded in very early to
    216   * the browser loading process, and may run before the console is avialable. In
    217   * this case the console will return as `undefined`.
    218   *
    219   * @returns {Console | void}
    220   */
    221  get console() {
    222    if (!this.#console) {
    223      try {
    224        this.#console = console.createInstance({
    225          maxLogLevelPref: "browser.translations.logLevel",
    226          prefix: "Translations",
    227        });
    228      } catch {
    229        // The console may not be initialized yet.
    230      }
    231    }
    232    return this.#console;
    233  }
    234 
    235  /**
    236   * Tracks if the popup is open, or scheduled to be open.
    237   *
    238   * @type {boolean}
    239   */
    240  #isPopupOpen = false;
    241 
    242  /**
    243   * Where the lazy elements are stored.
    244   *
    245   * @type {Record<string, Element>?}
    246   */
    247  #lazyElements;
    248 
    249  /**
    250   * Lazily creates the dom elements, and lazily selects them.
    251   *
    252   * @returns {Record<string, Element>}
    253   */
    254  get elements() {
    255    if (!this.#lazyElements) {
    256      // Lazily turn the template into a DOM element.
    257      /** @type {HTMLTemplateElement} */
    258      const wrapper = document.getElementById("template-translations-panel");
    259      const panel = wrapper.content.firstElementChild;
    260      wrapper.replaceWith(wrapper.content);
    261 
    262      panel.addEventListener("command", this);
    263      panel.addEventListener("click", this);
    264      panel.addEventListener("popupshown", this);
    265      panel.addEventListener("popuphidden", this);
    266 
    267      const settingsButton = document.getElementById(
    268        "translations-panel-settings"
    269      );
    270      // Clone the settings toolbarbutton across all the views.
    271      for (const header of panel.querySelectorAll(".panel-header")) {
    272        if (header.contains(settingsButton)) {
    273          continue;
    274        }
    275        const settingsButtonClone = settingsButton.cloneNode(true);
    276        settingsButtonClone.removeAttribute("id");
    277        header.appendChild(settingsButtonClone);
    278      }
    279 
    280      // Lazily select the elements.
    281      this.#lazyElements = {
    282        panel,
    283        settingsButton,
    284        // The rest of the elements are set by the getter below.
    285      };
    286 
    287      TranslationsPanelShared.defineLazyElements(document, this.#lazyElements, {
    288        alwaysTranslateLanguageMenuItem: ".always-translate-language-menuitem",
    289        appMenuButton: "PanelUI-menu-button",
    290        cancelButton: "full-page-translations-panel-cancel",
    291        changeSourceLanguageButton:
    292          "full-page-translations-panel-change-source-language",
    293        dismissErrorButton: "full-page-translations-panel-dismiss-error",
    294        error: "full-page-translations-panel-error",
    295        errorMessage: "full-page-translations-panel-error-message",
    296        errorMessageHint: "full-page-translations-panel-error-message-hint",
    297        errorHintAction: "full-page-translations-panel-translate-hint-action",
    298        fromLabel: "full-page-translations-panel-from-label",
    299        fromMenuList: "full-page-translations-panel-from",
    300        fromMenuPopup: "full-page-translations-panel-from-menupopup",
    301        header: "full-page-translations-panel-header",
    302        intro: "full-page-translations-panel-intro",
    303        introLearnMoreLink:
    304          "full-page-translations-panel-intro-learn-more-link",
    305        langSelection: "full-page-translations-panel-lang-selection",
    306        manageLanguagesMenuItem: ".manage-languages-menuitem",
    307        multiview: "full-page-translations-panel-multiview",
    308        neverTranslateLanguageMenuItem: ".never-translate-language-menuitem",
    309        neverTranslateSiteMenuItem: ".never-translate-site-menuitem",
    310        restoreButton: "full-page-translations-panel-restore-button",
    311        toLabel: "full-page-translations-panel-to-label",
    312        toMenuList: "full-page-translations-panel-to",
    313        toMenuPopup: "full-page-translations-panel-to-menupopup",
    314        translateButton: "full-page-translations-panel-translate",
    315        unsupportedHeader:
    316          "full-page-translations-panel-unsupported-language-header",
    317        unsupportedHint: "full-page-translations-panel-error-unsupported-hint",
    318        unsupportedLearnMoreLink:
    319          "full-page-translations-panel-unsupported-learn-more-link",
    320      });
    321    }
    322 
    323    return this.#lazyElements;
    324  }
    325 
    326  #lazyButtonElements = null;
    327 
    328  /**
    329   * When accessing `this.elements` the first time, it de-lazifies the custom components
    330   * that are needed for the popup. Avoid that by having a second element lookup
    331   * just for modifying the button.
    332   */
    333  get buttonElements() {
    334    if (!this.#lazyButtonElements) {
    335      this.#lazyButtonElements = {
    336        button: document.getElementById("translations-button"),
    337        buttonLocale: document.getElementById("translations-button-locale"),
    338        buttonCircleArrows: document.getElementById(
    339          "translations-button-circle-arrows"
    340        ),
    341      };
    342    }
    343    return this.#lazyButtonElements;
    344  }
    345 
    346  /**
    347   * Cache the last command used for error hints so that it can be later removed.
    348   */
    349  #lastHintCommand = null;
    350 
    351  /**
    352   * @param {object} options
    353   * @param {string} options.message - l10n id
    354   * @param {string} options.hint - l10n id
    355   * @param {string} options.actionText - l10n id
    356   * @param {Function} options.actionCommand - The action to perform.
    357   */
    358  #showError({
    359    message,
    360    hint,
    361    actionText: hintCommandText,
    362    actionCommand: hintCommand,
    363  }) {
    364    const { error, errorMessage, errorMessageHint, errorHintAction, intro } =
    365      this.elements;
    366    error.hidden = false;
    367    intro.hidden = true;
    368    document.l10n.setAttributes(errorMessage, message);
    369 
    370    if (hint) {
    371      errorMessageHint.hidden = false;
    372      document.l10n.setAttributes(errorMessageHint, hint);
    373    } else {
    374      errorMessageHint.hidden = true;
    375    }
    376 
    377    if (hintCommand && hintCommandText) {
    378      errorHintAction.removeEventListener("command", this.#lastHintCommand);
    379      this.#lastHintCommand = hintCommand;
    380      errorHintAction.addEventListener("command", hintCommand);
    381      errorHintAction.hidden = false;
    382      document.l10n.setAttributes(errorHintAction, hintCommandText);
    383    } else {
    384      errorHintAction.hidden = true;
    385    }
    386  }
    387 
    388  /**
    389   * Fetches the language tags for the document and the user and caches the results
    390   * Use `#getCachedDetectedLanguages` when the lang tags do not need to be re-fetched.
    391   * This requires a bit of work to do, so prefer the cached version when possible.
    392   *
    393   * @returns {Promise<LangTags>}
    394   */
    395  async #fetchDetectedLanguages() {
    396    this.detectedLanguages = await TranslationsParent.getTranslationsActor(
    397      gBrowser.selectedBrowser
    398    ).getDetectedLanguages();
    399    return this.detectedLanguages;
    400  }
    401 
    402  /**
    403   * If the detected language tags have been retrieved previously, return the cached
    404   * version. Otherwise do a fresh lookup of the document's language tag.
    405   *
    406   * @returns {Promise<LangTags>}
    407   */
    408  async #getCachedDetectedLanguages() {
    409    if (!this.detectedLanguages) {
    410      return this.#fetchDetectedLanguages();
    411    }
    412    return this.detectedLanguages;
    413  }
    414 
    415  /**
    416   * Builds the <menulist> of languages for both the "from" and "to". This can be
    417   * called every time the popup is shown, as it will retry when there is an error
    418   * (such as a network error) or be a noop if it's already initialized.
    419   */
    420  async #ensureLangListsBuilt() {
    421    try {
    422      await TranslationsPanelShared.ensureLangListsBuilt(document, this);
    423    } catch (error) {
    424      this.console?.error(error);
    425    }
    426  }
    427 
    428  /**
    429   * Reactively sets the views based on the async state changes of the engine, and
    430   * other component state changes.
    431   *
    432   * @param {TranslationsLanguageState} languageState
    433   */
    434  #updateViewFromTranslationStatus(
    435    languageState = TranslationsParent.getTranslationsActor(
    436      gBrowser.selectedBrowser
    437    ).languageState
    438  ) {
    439    const { translateButton, toMenuList, fromMenuList, header, cancelButton } =
    440      this.elements;
    441    const { requestedLanguagePair, isEngineReady } = languageState;
    442 
    443    // Remove the model variant. e.g. "ru,base" -> "ru"
    444    const selectedFrom = fromMenuList.value.split(",")[0];
    445    const selectedTo = toMenuList.value.split(",")[0];
    446 
    447    if (
    448      requestedLanguagePair &&
    449      !isEngineReady &&
    450      TranslationsUtils.langTagsMatch(
    451        selectedFrom,
    452        requestedLanguagePair.sourceLanguage
    453      ) &&
    454      TranslationsUtils.langTagsMatch(
    455        selectedTo,
    456        requestedLanguagePair.targetLanguage
    457      )
    458    ) {
    459      // A translation has been requested, but is not ready yet.
    460      document.l10n.setAttributes(
    461        translateButton,
    462        "translations-panel-translate-button-loading"
    463      );
    464      translateButton.disabled = true;
    465      cancelButton.hidden = false;
    466      this.updateUIForReTranslation(false /* isReTranslation */);
    467    } else {
    468      document.l10n.setAttributes(
    469        translateButton,
    470        "translations-panel-translate-button"
    471      );
    472      translateButton.disabled =
    473        // No "to" language was provided.
    474        !toMenuList.value ||
    475        // No "from" language was provided.
    476        !fromMenuList.value ||
    477        // The translation languages are the same, don't allow this translation.
    478        TranslationsUtils.langTagsMatch(selectedFrom, selectedTo) ||
    479        // This is the requested language pair.
    480        (requestedLanguagePair &&
    481          TranslationsUtils.langTagsMatch(
    482            requestedLanguagePair.sourceLanguage,
    483            selectedFrom
    484          ) &&
    485          TranslationsUtils.langTagsMatch(
    486            requestedLanguagePair.targetLanguage,
    487            selectedTo
    488          ));
    489    }
    490 
    491    if (requestedLanguagePair && isEngineReady) {
    492      const { sourceLanguage, targetLanguage } = requestedLanguagePair;
    493      const languageDisplayNames =
    494        TranslationsParent.createLanguageDisplayNames();
    495      cancelButton.hidden = true;
    496      this.updateUIForReTranslation(true /* isReTranslation */);
    497 
    498      document.l10n.setAttributes(header, "translations-panel-revisit-header", {
    499        fromLanguage: languageDisplayNames.of(sourceLanguage),
    500        toLanguage: languageDisplayNames.of(targetLanguage),
    501      });
    502    } else {
    503      document.l10n.setAttributes(header, "translations-panel-header");
    504    }
    505  }
    506 
    507  /**
    508   * @param {boolean} isReTranslation
    509   */
    510  updateUIForReTranslation(isReTranslation) {
    511    const { restoreButton, fromLabel, fromMenuList, toLabel } = this.elements;
    512    restoreButton.hidden = !isReTranslation;
    513    // When offering to re-translate a page, hide the "from" language so users don't
    514    // get confused.
    515    fromLabel.hidden = isReTranslation;
    516    fromMenuList.hidden = isReTranslation;
    517    if (isReTranslation) {
    518      fromLabel.style.marginBlockStart = "";
    519      toLabel.style.marginBlockStart = 0;
    520    } else {
    521      fromLabel.style.marginBlockStart = 0;
    522      toLabel.style.marginBlockStart = "";
    523    }
    524  }
    525 
    526  /**
    527   * Returns true if the panel is currently showing the default view, otherwise false.
    528   *
    529   * @returns {boolean}
    530   */
    531  #isShowingDefaultView() {
    532    if (!this.#lazyElements) {
    533      // Nothing has been initialized.
    534      return false;
    535    }
    536    const { multiview } = this.elements;
    537    return (
    538      multiview.getAttribute("mainViewId") ===
    539      "full-page-translations-panel-view-default"
    540    );
    541  }
    542 
    543  /**
    544   * Show the default view of choosing a source and target language.
    545   *
    546   * @param {TranslationsParent} actor
    547   * @param {boolean} force - Force the page to show translation options.
    548   */
    549  async #showDefaultView(actor, force = false) {
    550    const {
    551      fromMenuList,
    552      multiview,
    553      panel,
    554      error,
    555      toMenuList,
    556      translateButton,
    557      langSelection,
    558      intro,
    559      header,
    560    } = this.elements;
    561 
    562    this.#updateViewFromTranslationStatus();
    563 
    564    // Unconditionally hide the intro text in case the panel is re-shown.
    565    intro.hidden = true;
    566 
    567    if (TranslationsPanelShared.getLangListsInitState(this) === "error") {
    568      // There was an error, display it in the view rather than the language
    569      // dropdowns.
    570      const { cancelButton, errorHintAction } = this.elements;
    571 
    572      this.#showError({
    573        message: "translations-panel-error-load-languages",
    574        hint: "translations-panel-error-load-languages-hint",
    575        actionText: "translations-panel-error-load-languages-hint-button",
    576        actionCommand: () => this.#reloadLangList(actor),
    577      });
    578 
    579      translateButton.disabled = true;
    580      this.updateUIForReTranslation(false /* isReTranslation */);
    581      cancelButton.hidden = false;
    582      langSelection.hidden = true;
    583      errorHintAction.disabled = false;
    584      return;
    585    }
    586 
    587    // Remove any old selected values synchronously before asking for new ones.
    588    fromMenuList.value = "";
    589    error.hidden = true;
    590    langSelection.hidden = false;
    591    // Remove the model variant. e.g. "ru,base" -> "ru"
    592    const selectedSource = fromMenuList.value.split(",")[0];
    593 
    594    const { userLangTag, docLangTag, isDocLangTagSupported } =
    595      await this.#fetchDetectedLanguages().then(langTags => langTags ?? {});
    596 
    597    if (isDocLangTagSupported || force) {
    598      // Show the default view with the language selection
    599      const { cancelButton } = this.elements;
    600 
    601      if (isDocLangTagSupported) {
    602        fromMenuList.value = docLangTag ?? "";
    603      } else {
    604        fromMenuList.value = "";
    605      }
    606 
    607      if (
    608        this.#manuallySelectedToLanguage &&
    609        !TranslationsUtils.langTagsMatch(
    610          docLangTag,
    611          this.#manuallySelectedToLanguage
    612        )
    613      ) {
    614        // Use the manually selected language if available
    615        toMenuList.value = this.#manuallySelectedToLanguage;
    616      } else if (
    617        userLangTag &&
    618        !TranslationsUtils.langTagsMatch(userLangTag, docLangTag)
    619      ) {
    620        // The userLangTag is specified and does not match the doc lang tag, so we should use it.
    621        toMenuList.value = userLangTag;
    622      } else {
    623        // No userLangTag is specified in the cache and no #manuallySelectedToLanguage is available,
    624        // so we will attempt to find a suitable one.
    625        toMenuList.value =
    626          await TranslationsParent.getTopPreferredSupportedToLang({
    627            excludeLangTags: [
    628              // Avoid offering to translate into the original source language.
    629              docLangTag,
    630              // Avoid same-language to same-language translations if possible.
    631              selectedSource,
    632            ],
    633          });
    634      }
    635 
    636      const resolvedSource = fromMenuList.value.split(",")[0];
    637      const resolvedTarget = toMenuList.value.split(",")[0];
    638 
    639      if (TranslationsUtils.langTagsMatch(resolvedSource, resolvedTarget)) {
    640        // The best possible user-preferred language tag that we were able to find for the
    641        // toMenuList is the same as the fromMenuList, but same-language to same-language
    642        // translations are not allowed in Full Page Translations, so we will just show the
    643        // "Choose a language" option in this case.
    644        toMenuList.value = "";
    645      }
    646 
    647      this.onChangeLanguages();
    648 
    649      this.updateUIForReTranslation(false /* isReTranslation */);
    650      cancelButton.hidden = false;
    651      multiview.setAttribute(
    652        "mainViewId",
    653        "full-page-translations-panel-view-default"
    654      );
    655 
    656      if (TranslationsParent.hasUserEverTranslated()) {
    657        intro.hidden = true;
    658        document.l10n.setAttributes(header, "translations-panel-header");
    659      } else {
    660        intro.hidden = false;
    661        document.l10n.setAttributes(header, "translations-panel-intro-header");
    662      }
    663    } else {
    664      // Show the "unsupported language" view.
    665      const { unsupportedHint } = this.elements;
    666      multiview.setAttribute(
    667        "mainViewId",
    668        "full-page-translations-panel-view-unsupported-language"
    669      );
    670      let language;
    671      if (docLangTag) {
    672        const languageDisplayNames =
    673          TranslationsParent.createLanguageDisplayNames({
    674            fallback: "none",
    675          });
    676        language = languageDisplayNames.of(docLangTag);
    677      }
    678      if (language) {
    679        document.l10n.setAttributes(
    680          unsupportedHint,
    681          "translations-panel-error-unsupported-hint-known",
    682          { language }
    683        );
    684      } else {
    685        document.l10n.setAttributes(
    686          unsupportedHint,
    687          "translations-panel-error-unsupported-hint-unknown"
    688        );
    689      }
    690    }
    691 
    692    // Focus the "from" language, as it is the only field not set.
    693    panel.addEventListener(
    694      "ViewShown",
    695      () => {
    696        if (!fromMenuList.value) {
    697          fromMenuList.focus();
    698        }
    699        if (!toMenuList.value) {
    700          toMenuList.focus();
    701        }
    702      },
    703      { once: true }
    704    );
    705  }
    706 
    707  /**
    708   * Updates the checked states of the settings menu checkboxes that
    709   * pertain to languages.
    710   */
    711  async #updateSettingsMenuLanguageCheckboxStates() {
    712    const langTags = await this.#getCachedDetectedLanguages();
    713    const { docLangTag, isDocLangTagSupported } = langTags;
    714 
    715    const { panel } = this.elements;
    716    const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll(
    717      ".always-translate-language-menuitem"
    718    );
    719    const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll(
    720      ".never-translate-language-menuitem"
    721    );
    722    const alwaysOfferTranslationsMenuItems =
    723      panel.ownerDocument.querySelectorAll(
    724        ".always-offer-translations-menuitem"
    725      );
    726 
    727    const alwaysOfferTranslations =
    728      TranslationsParent.shouldAlwaysOfferTranslations();
    729    const alwaysTranslateLanguage =
    730      TranslationsParent.shouldAlwaysTranslateLanguage(langTags);
    731    const neverTranslateLanguage =
    732      TranslationsParent.shouldNeverTranslateLanguage(docLangTag);
    733    const shouldDisable =
    734      !docLangTag ||
    735      !isDocLangTagSupported ||
    736      docLangTag ===
    737        (await TranslationsParent.getTopPreferredSupportedToLang());
    738 
    739    for (const menuitem of alwaysOfferTranslationsMenuItems) {
    740      menuitem.toggleAttribute("checked", alwaysOfferTranslations);
    741    }
    742    for (const menuitem of alwaysTranslateMenuItems) {
    743      menuitem.toggleAttribute("checked", alwaysTranslateLanguage);
    744      menuitem.disabled = shouldDisable;
    745    }
    746    for (const menuitem of neverTranslateMenuItems) {
    747      menuitem.toggleAttribute("checked", neverTranslateLanguage);
    748      menuitem.disabled = shouldDisable;
    749    }
    750  }
    751 
    752  /**
    753   * Updates the checked states of the settings menu checkboxes that
    754   * pertain to site permissions.
    755   */
    756  async #updateSettingsMenuSiteCheckboxStates() {
    757    const { panel } = this.elements;
    758    const neverTranslateSiteMenuItems = panel.ownerDocument.querySelectorAll(
    759      ".never-translate-site-menuitem"
    760    );
    761    const neverTranslateSite = await TranslationsParent.getTranslationsActor(
    762      gBrowser.selectedBrowser
    763    ).shouldNeverTranslateSite();
    764 
    765    for (const menuitem of neverTranslateSiteMenuItems) {
    766      menuitem.toggleAttribute("checked", neverTranslateSite);
    767    }
    768  }
    769 
    770  /**
    771   * Populates the language-related settings menuitems by adding the
    772   * localized display name of the document's detected language tag.
    773   */
    774  async #populateSettingsMenuItems() {
    775    const { docLangTag } = await this.#getCachedDetectedLanguages();
    776 
    777    const { panel } = this.elements;
    778 
    779    const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll(
    780      ".always-translate-language-menuitem"
    781    );
    782    const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll(
    783      ".never-translate-language-menuitem"
    784    );
    785 
    786    /** @type {string | undefined} */
    787    let docLangDisplayName;
    788    if (docLangTag) {
    789      const languageDisplayNames =
    790        TranslationsParent.createLanguageDisplayNames({
    791          fallback: "none",
    792        });
    793      // The display name will still be empty if the docLangTag is not known.
    794      docLangDisplayName = languageDisplayNames.of(docLangTag);
    795    }
    796 
    797    for (const menuitem of alwaysTranslateMenuItems) {
    798      if (docLangDisplayName) {
    799        document.l10n.setAttributes(
    800          menuitem,
    801          "translations-panel-settings-always-translate-language",
    802          { language: docLangDisplayName }
    803        );
    804      } else {
    805        document.l10n.setAttributes(
    806          menuitem,
    807          "translations-panel-settings-always-translate-unknown-language"
    808        );
    809      }
    810    }
    811 
    812    for (const menuitem of neverTranslateMenuItems) {
    813      if (docLangDisplayName) {
    814        document.l10n.setAttributes(
    815          menuitem,
    816          "translations-panel-settings-never-translate-language",
    817          { language: docLangDisplayName }
    818        );
    819      } else {
    820        document.l10n.setAttributes(
    821          menuitem,
    822          "translations-panel-settings-never-translate-unknown-language"
    823        );
    824      }
    825    }
    826 
    827    await Promise.all([
    828      this.#updateSettingsMenuLanguageCheckboxStates(),
    829      this.#updateSettingsMenuSiteCheckboxStates(),
    830    ]);
    831  }
    832 
    833  /**
    834   * Configures the panel for the user to reset the page after it has been translated.
    835   *
    836   * @param {LanguagePair} languagePair
    837   */
    838  async #showRevisitView({ sourceLanguage, targetLanguage, sourceVariant }) {
    839    const { fromMenuList, toMenuList, intro } = this.elements;
    840    if (!this.#isShowingDefaultView()) {
    841      await this.#showDefaultView(
    842        TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser)
    843      );
    844    }
    845    intro.hidden = true;
    846    if (sourceVariant) {
    847      fromMenuList.value = `${sourceLanguage},${sourceVariant}`;
    848    } else {
    849      fromMenuList.value = sourceLanguage;
    850    }
    851    toMenuList.value = await TranslationsParent.getTopPreferredSupportedToLang({
    852      excludeLangTags: [
    853        // Avoid offering to translate into the original source language.
    854        sourceLanguage,
    855        // Avoid offering to translate into current active target language.
    856        targetLanguage,
    857      ],
    858    });
    859    this.onChangeLanguages();
    860  }
    861 
    862  /**
    863   * Handle the disable logic for when the menulist is changed for the "Translate to"
    864   * on the "revisit" subview.
    865   */
    866  onChangeRevisitTo() {
    867    const { revisitTranslate, revisitMenuList } = this.elements;
    868    revisitTranslate.disabled = !revisitMenuList.value;
    869  }
    870 
    871  /**
    872   * Handle logic and telemetry for changing the selected from-language option.
    873   *
    874   * @param {Event} event
    875   */
    876  onChangeFromLanguage(event) {
    877    const { target } = event;
    878    if (target?.value) {
    879      TranslationsParent.telemetry()
    880        .fullPagePanel()
    881        .onChangeFromLanguage(target.value);
    882    }
    883    this.onChangeLanguages();
    884  }
    885 
    886  /**
    887   * Handle logic and telemetry for changing the selected to-language option.
    888   *
    889   * @param {Event} event
    890   */
    891  onChangeToLanguage(event) {
    892    const { target } = event;
    893    if (target?.value) {
    894      TranslationsParent.telemetry()
    895        .fullPagePanel()
    896        .onChangeToLanguage(target.value);
    897    }
    898    this.onChangeLanguages();
    899    // Update the manually selected language when the user changes the target language.
    900    this.#manuallySelectedToLanguage = target.value;
    901  }
    902 
    903  /**
    904   * When changing the language selection, the translate button will need updating.
    905   */
    906  onChangeLanguages() {
    907    this.#updateViewFromTranslationStatus();
    908  }
    909 
    910  /**
    911   * Hide the pop up (for event handlers).
    912   */
    913  close() {
    914    PanelMultiView.hidePopup(this.elements.panel);
    915  }
    916 
    917  /*
    918   * Handler for clicking the learn more link from linked text
    919   * within the translations panel.
    920   */
    921  onLearnMoreLink() {
    922    TranslationsParent.telemetry().fullPagePanel().onLearnMoreLink();
    923    FullPageTranslationsPanel.close();
    924  }
    925 
    926  /*
    927   * Handler for clicking the learn more link from the gear menu.
    928   */
    929  onAboutTranslations() {
    930    TranslationsParent.telemetry().fullPagePanel().onAboutTranslations();
    931    PanelMultiView.hidePopup(this.elements.panel);
    932    const window =
    933      gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
    934    window.openTrustedLinkIn(
    935      "https://support.mozilla.org/kb/website-translation",
    936      "tab",
    937      {
    938        forceForeground: true,
    939        triggeringPrincipal:
    940          Services.scriptSecurityManager.getSystemPrincipal(),
    941      }
    942    );
    943  }
    944 
    945  /**
    946   * When a language is not supported and the menu is manually invoked, an error message
    947   * is shown. This method switches the panel back to the language selection view.
    948   * Note that this bypasses the showSubView method since the main view doesn't support
    949   * a subview.
    950   */
    951  async onChangeSourceLanguage(event) {
    952    const { panel } = this.elements;
    953    PanelMultiView.hidePopup(panel);
    954 
    955    await this.#showDefaultView(
    956      TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser),
    957      true /* force this view to be shown */
    958    );
    959 
    960    await this.#openPanelPopup(this.elements.appMenuButton, {
    961      event,
    962      viewName: "defaultView",
    963      maintainFlow: true,
    964    });
    965  }
    966 
    967  /**
    968   * @param {TranslationsActor} actor
    969   */
    970  async #reloadLangList(actor) {
    971    try {
    972      await this.#ensureLangListsBuilt();
    973      await this.#showDefaultView(actor);
    974    } catch (error) {
    975      this.elements.errorHintAction.disabled = false;
    976    }
    977  }
    978 
    979  /**
    980   * Handle telemetry events when buttons are invoked in the panel.
    981   *
    982   * @param {Event} event
    983   */
    984  handlePanelButtonEvent(event) {
    985    const {
    986      cancelButton,
    987      changeSourceLanguageButton,
    988      dismissErrorButton,
    989      restoreButton,
    990      translateButton,
    991    } = this.elements;
    992    switch (event.target.id) {
    993      case cancelButton.id: {
    994        TranslationsParent.telemetry().fullPagePanel().onCancelButton();
    995        break;
    996      }
    997      case changeSourceLanguageButton.id: {
    998        TranslationsParent.telemetry()
    999          .fullPagePanel()
   1000          .onChangeSourceLanguageButton();
   1001        break;
   1002      }
   1003      case dismissErrorButton.id: {
   1004        TranslationsParent.telemetry().fullPagePanel().onDismissErrorButton();
   1005        break;
   1006      }
   1007      case restoreButton.id: {
   1008        TranslationsParent.telemetry().fullPagePanel().onRestorePageButton();
   1009        break;
   1010      }
   1011      case translateButton.id: {
   1012        TranslationsParent.telemetry().fullPagePanel().onTranslateButton();
   1013        break;
   1014      }
   1015    }
   1016  }
   1017 
   1018  /**
   1019   * Handle telemetry events when popups are shown in the panel.
   1020   *
   1021   * @param {Event} event
   1022   */
   1023  handlePanelPopupShownEvent(event) {
   1024    const { panel, fromMenuList, toMenuList } = this.elements;
   1025    switch (event.target.id) {
   1026      case panel.id: {
   1027        // This telemetry event is invoked externally because it requires
   1028        // extra logic about from where the panel was opened and whether
   1029        // or not the flow should be maintained or started anew.
   1030        break;
   1031      }
   1032      case fromMenuList.firstChild.id: {
   1033        TranslationsParent.telemetry().fullPagePanel().onOpenFromLanguageMenu();
   1034        break;
   1035      }
   1036      case toMenuList.firstChild.id: {
   1037        TranslationsParent.telemetry().fullPagePanel().onOpenToLanguageMenu();
   1038        break;
   1039      }
   1040    }
   1041  }
   1042 
   1043  /**
   1044   * Handle telemetry events when popups are hidden in the panel.
   1045   *
   1046   * @param {Event} event
   1047   */
   1048  handlePanelPopupHiddenEvent(event) {
   1049    const { panel, fromMenuList, toMenuList } = this.elements;
   1050    switch (event.target.id) {
   1051      case panel.id: {
   1052        TranslationsParent.telemetry().fullPagePanel().onClose();
   1053        this.#isPopupOpen = false;
   1054        this.elements.error.hidden = true;
   1055        break;
   1056      }
   1057      case fromMenuList.firstChild.id: {
   1058        TranslationsParent.telemetry()
   1059          .fullPagePanel()
   1060          .onCloseFromLanguageMenu();
   1061        break;
   1062      }
   1063      case toMenuList.firstChild.id: {
   1064        TranslationsParent.telemetry().fullPagePanel().onCloseToLanguageMenu();
   1065        break;
   1066      }
   1067    }
   1068  }
   1069 
   1070  /**
   1071   * Handle telemetry events when the settings menu is shown.
   1072   */
   1073  handleSettingsPopupShownEvent() {
   1074    TranslationsParent.telemetry().fullPagePanel().onOpenSettingsMenu();
   1075  }
   1076 
   1077  /**
   1078   * Handle telemetry events when the settings menu is hidden.
   1079   */
   1080  handleSettingsPopupHiddenEvent() {
   1081    TranslationsParent.telemetry().fullPagePanel().onCloseSettingsMenu();
   1082  }
   1083 
   1084  /**
   1085   * Opens the Translations panel popup at the given target.
   1086   *
   1087   * @param {object} target - The target element at which to open the popup.
   1088   * @param {object} telemetryData
   1089   * @param {string} telemetryData.event
   1090   *   The trigger event for opening the popup.
   1091   * @param {string} telemetryData.viewName
   1092   *   The name of the view shown by the panel.
   1093   * @param {boolean} telemetryData.autoShow
   1094   *   True if the panel was automatically opened, otherwise false.
   1095   * @param {boolean} telemetryData.maintainFlow
   1096   *   Whether or not to maintain the flow of telemetry.
   1097   */
   1098  async #openPanelPopup(
   1099    target,
   1100    { event = null, viewName = null, autoShow = false, maintainFlow = false }
   1101  ) {
   1102    const { panel, appMenuButton } = this.elements;
   1103    const openedFromAppMenu = target.id === appMenuButton.id;
   1104    const { docLangTag } = await this.#getCachedDetectedLanguages();
   1105 
   1106    TranslationsParent.telemetry().fullPagePanel().onOpen({
   1107      viewName,
   1108      autoShow,
   1109      docLangTag,
   1110      maintainFlow,
   1111      openedFromAppMenu,
   1112    });
   1113 
   1114    this.#isPopupOpen = true;
   1115 
   1116    PanelMultiView.openPopup(panel, target, {
   1117      position: "bottomright topright",
   1118      triggerEvent: event,
   1119    }).catch(error => this.console?.error(error));
   1120  }
   1121 
   1122  /**
   1123   * Keeps track of open requests to guard against race conditions.
   1124   *
   1125   * @type {Promise<void> | null}
   1126   */
   1127  #openPromise = null;
   1128 
   1129  /**
   1130   * Opens the FullPageTranslationsPanel.
   1131   *
   1132   * @param {Event} event
   1133   * @param {boolean} reportAsAutoShow
   1134   *   True to report to telemetry that the panel was opened automatically, otherwise false.
   1135   */
   1136  async open(event, reportAsAutoShow = false) {
   1137    if (this.#openPromise) {
   1138      // There is already an open event happening, do not open.
   1139      return;
   1140    }
   1141 
   1142    this.#openPromise = this.#openImpl(event, reportAsAutoShow);
   1143    this.#openPromise.finally(() => {
   1144      this.#openPromise = null;
   1145    });
   1146  }
   1147 
   1148  /**
   1149   * The language tag that was manually selected by the user.
   1150   * This is used to remember the selection if the user happens to close and then reopen the panel prior to translating.
   1151   *
   1152   * @type {string | null}
   1153   */
   1154  #manuallySelectedToLanguage = null;
   1155 
   1156  /**
   1157   * Implementation function for opening the panel. Prefer FullPageTranslationsPanel.open.
   1158   *
   1159   * @param {Event} event
   1160   */
   1161  async #openImpl(event, reportAsAutoShow) {
   1162    event.stopPropagation();
   1163    if (
   1164      (event.type == "click" && event.button != 0) ||
   1165      (event.type == "keypress" &&
   1166        event.charCode != KeyEvent.DOM_VK_SPACE &&
   1167        event.keyCode != KeyEvent.DOM_VK_RETURN)
   1168    ) {
   1169      // Allow only left click, space, or enter.
   1170      return;
   1171    }
   1172 
   1173    const { button } = this.buttonElements;
   1174 
   1175    const { requestedLanguagePair } = TranslationsParent.getTranslationsActor(
   1176      gBrowser.selectedBrowser
   1177    ).languageState;
   1178 
   1179    await this.#ensureLangListsBuilt();
   1180 
   1181    if (requestedLanguagePair) {
   1182      await this.#showRevisitView(requestedLanguagePair).catch(error => {
   1183        this.console?.error(error);
   1184      });
   1185    } else {
   1186      await this.#showDefaultView(
   1187        TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser)
   1188      ).catch(error => {
   1189        this.console?.error(error);
   1190      });
   1191    }
   1192 
   1193    this.#populateSettingsMenuItems();
   1194 
   1195    const targetButton =
   1196      button.contains(event.target) ||
   1197      event.type === "TranslationsParent:OfferTranslation"
   1198        ? button
   1199        : this.elements.appMenuButton;
   1200 
   1201    this.console?.log(`Showing a translation panel`, gBrowser.currentURI.spec);
   1202 
   1203    await this.#openPanelPopup(targetButton, {
   1204      event,
   1205      autoShow: reportAsAutoShow,
   1206      viewName: requestedLanguagePair ? "revisitView" : "defaultView",
   1207      maintainFlow: false,
   1208    });
   1209  }
   1210 
   1211  /**
   1212   * Returns true if translations is currently active, otherwise false.
   1213   *
   1214   * @returns {boolean}
   1215   */
   1216  #isTranslationsActive() {
   1217    const { requestedLanguagePair } = TranslationsParent.getTranslationsActor(
   1218      gBrowser.selectedBrowser
   1219    ).languageState;
   1220    return requestedLanguagePair !== null;
   1221  }
   1222 
   1223  /**
   1224   * Handle the translation button being clicked when there are two language options.
   1225   */
   1226  async onTranslate() {
   1227    this.#manuallySelectedToLanguage = null;
   1228    PanelMultiView.hidePopup(this.elements.panel);
   1229 
   1230    const actor = TranslationsParent.getTranslationsActor(
   1231      gBrowser.selectedBrowser
   1232    );
   1233    const [sourceLanguage, sourceVariant] =
   1234      this.elements.fromMenuList.value.split(",");
   1235    const [targetLanguage, targetVariant] =
   1236      this.elements.toMenuList.value.split(",");
   1237 
   1238    actor.translate(
   1239      { sourceLanguage, targetLanguage, sourceVariant, targetVariant },
   1240      false // reportAsAutoTranslate
   1241    );
   1242  }
   1243 
   1244  /**
   1245   * Handle the cancel button being clicked.
   1246   */
   1247  onCancel() {
   1248    PanelMultiView.hidePopup(this.elements.panel);
   1249  }
   1250 
   1251  /**
   1252   * A handler for opening the settings context menu.
   1253   */
   1254  openSettingsPopup(button) {
   1255    this.#updateSettingsMenuLanguageCheckboxStates();
   1256    this.#updateSettingsMenuSiteCheckboxStates();
   1257    const popup = button.ownerDocument.getElementById(
   1258      "full-page-translations-panel-settings-menupopup"
   1259    );
   1260    popup.openPopup(button, "after_end");
   1261  }
   1262 
   1263  /**
   1264   * Creates a new CheckboxPageAction based on the current translated
   1265   * state of the page and the state of the persistent options in the
   1266   * translations panel settings.
   1267   *
   1268   * @returns {CheckboxPageAction}
   1269   */
   1270  getCheckboxPageActionFor() {
   1271    const {
   1272      alwaysTranslateLanguageMenuItem,
   1273      neverTranslateLanguageMenuItem,
   1274      neverTranslateSiteMenuItem,
   1275    } = this.elements;
   1276 
   1277    const alwaysTranslateLanguage =
   1278      alwaysTranslateLanguageMenuItem.hasAttribute("checked");
   1279    const neverTranslateLanguage =
   1280      neverTranslateLanguageMenuItem.hasAttribute("checked");
   1281    const neverTranslateSite =
   1282      neverTranslateSiteMenuItem.hasAttribute("checked");
   1283 
   1284    return new CheckboxPageAction(
   1285      this.#isTranslationsActive(),
   1286      alwaysTranslateLanguage,
   1287      neverTranslateLanguage,
   1288      neverTranslateSite
   1289    );
   1290  }
   1291 
   1292  /**
   1293   * Redirect the user to about:preferences
   1294   */
   1295  openManageLanguages() {
   1296    TranslationsParent.telemetry().fullPagePanel().onManageLanguages();
   1297    const window =
   1298      gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
   1299    window.openTrustedLinkIn("about:preferences#general-translations", "tab");
   1300  }
   1301 
   1302  /**
   1303   * Performs the given page action.
   1304   *
   1305   * @param {PageAction} pageAction
   1306   */
   1307  async #doPageAction(pageAction) {
   1308    switch (pageAction) {
   1309      case PageAction.NO_CHANGE: {
   1310        break;
   1311      }
   1312      case PageAction.RESTORE_PAGE: {
   1313        await this.onRestore();
   1314        break;
   1315      }
   1316      case PageAction.TRANSLATE_PAGE: {
   1317        await this.onTranslate();
   1318        break;
   1319      }
   1320      case PageAction.CLOSE_PANEL: {
   1321        PanelMultiView.hidePopup(this.elements.panel);
   1322        break;
   1323      }
   1324    }
   1325  }
   1326 
   1327  /**
   1328   * Updates the always-translate-language menuitem prefs and checked state.
   1329   * If auto-translate is currently active for the doc language, deactivates it.
   1330   * If auto-translate is currently inactive for the doc language, activates it.
   1331   */
   1332  async onAlwaysTranslateLanguage() {
   1333    const langTags = await this.#getCachedDetectedLanguages();
   1334    const { docLangTag } = langTags;
   1335    if (!docLangTag) {
   1336      throw new Error("Expected to have a document language tag.");
   1337    }
   1338    const pageAction =
   1339      this.getCheckboxPageActionFor().alwaysTranslateLanguage();
   1340    const toggledOn =
   1341      TranslationsParent.toggleAlwaysTranslateLanguagePref(langTags);
   1342    TranslationsParent.telemetry()
   1343      .fullPagePanel()
   1344      .onAlwaysTranslateLanguage(docLangTag, toggledOn);
   1345    this.#updateSettingsMenuLanguageCheckboxStates();
   1346    await this.#doPageAction(pageAction);
   1347  }
   1348 
   1349  /**
   1350   * Toggle offering translations.
   1351   */
   1352  async onAlwaysOfferTranslations() {
   1353    const toggledOn = TranslationsParent.toggleAutomaticallyPopupPref();
   1354    TranslationsParent.telemetry()
   1355      .fullPagePanel()
   1356      .onAlwaysOfferTranslations(toggledOn);
   1357  }
   1358 
   1359  /**
   1360   * Updates the never-translate-language menuitem prefs and checked state.
   1361   * If never-translate is currently active for the doc language, deactivates it.
   1362   * If never-translate is currently inactive for the doc language, activates it.
   1363   */
   1364  async onNeverTranslateLanguage() {
   1365    const { docLangTag } = await this.#getCachedDetectedLanguages();
   1366    if (!docLangTag) {
   1367      throw new Error("Expected to have a document language tag.");
   1368    }
   1369    const pageAction = this.getCheckboxPageActionFor().neverTranslateLanguage();
   1370    const toggledOn =
   1371      TranslationsParent.toggleNeverTranslateLanguagePref(docLangTag);
   1372    TranslationsParent.telemetry()
   1373      .fullPagePanel()
   1374      .onNeverTranslateLanguage(docLangTag, toggledOn);
   1375    this.#updateSettingsMenuLanguageCheckboxStates();
   1376    await this.#doPageAction(pageAction);
   1377  }
   1378 
   1379  /**
   1380   * Updates the never-translate-site menuitem permissions and checked state.
   1381   * If never-translate is currently active for the site, deactivates it.
   1382   * If never-translate is currently inactive for the site, activates it.
   1383   */
   1384  async onNeverTranslateSite() {
   1385    const pageAction = this.getCheckboxPageActionFor().neverTranslateSite();
   1386    const toggledOn = await TranslationsParent.getTranslationsActor(
   1387      gBrowser.selectedBrowser
   1388    ).toggleNeverTranslateSitePermissions();
   1389    TranslationsParent.telemetry()
   1390      .fullPagePanel()
   1391      .onNeverTranslateSite(toggledOn);
   1392    this.#updateSettingsMenuSiteCheckboxStates();
   1393    await this.#doPageAction(pageAction);
   1394  }
   1395 
   1396  /**
   1397   * Handle the restore button being clicked.
   1398   */
   1399  async onRestore() {
   1400    const { panel } = this.elements;
   1401    PanelMultiView.hidePopup(panel);
   1402    const { docLangTag } = await this.#getCachedDetectedLanguages();
   1403    if (!docLangTag) {
   1404      throw new Error("Expected to have a document language tag.");
   1405    }
   1406 
   1407    TranslationsParent.getTranslationsActor(
   1408      gBrowser.selectedBrowser
   1409    ).restorePage(docLangTag);
   1410  }
   1411 
   1412  /**
   1413   * An event handler that allows the FullPageTranslationsPanel object
   1414   * to be compatible with the addTabsProgressListener function.
   1415   *
   1416   * @param {tabbrowser} browser
   1417   */
   1418  onLocationChange(browser) {
   1419    if (browser.currentURI.spec.startsWith("about:reader")) {
   1420      // Hide the translations button when entering reader mode.
   1421      this.buttonElements.button.hidden = true;
   1422    }
   1423  }
   1424 
   1425  /**
   1426   * Update the view to show an error.
   1427   *
   1428   * @param {TranslationParent} actor
   1429   */
   1430  async #showEngineError(actor) {
   1431    const { button } = this.buttonElements;
   1432    await this.#ensureLangListsBuilt();
   1433    await this.#showDefaultView(actor).catch(e => {
   1434      this.console?.error(e);
   1435    });
   1436    this.elements.error.hidden = false;
   1437    this.#showError({
   1438      message: "translations-panel-error-translating",
   1439    });
   1440    const targetButton = button.hidden ? this.elements.appMenuButton : button;
   1441 
   1442    // Re-open the menu on an error.
   1443    await this.#openPanelPopup(targetButton, {
   1444      autoShow: true,
   1445      viewName: "errorView",
   1446      maintainFlow: true,
   1447    });
   1448  }
   1449 
   1450  /**
   1451   * Set the state of the translations button in the URL bar.
   1452   *
   1453   * @param {CustomEvent} event
   1454   */
   1455  handleEvent = event => {
   1456    const target = event.target;
   1457    let { id } = target;
   1458 
   1459    // If a menuitem within a menulist is the target, it will not have an id,
   1460    // so we want to grab the closest relevant id.
   1461    if (!id) {
   1462      id = target.closest("[id]")?.id;
   1463    }
   1464 
   1465    switch (event.type) {
   1466      case "command": {
   1467        switch (id) {
   1468          case "translations-panel-settings":
   1469            this.openSettingsPopup(target);
   1470            break;
   1471          case "full-page-translations-panel-from-menupopup":
   1472            this.onChangeFromLanguage(event);
   1473            break;
   1474          case "full-page-translations-panel-to-menupopup":
   1475            this.onChangeToLanguage(event);
   1476            break;
   1477          case "full-page-translations-panel-restore-button":
   1478            this.onRestore(event);
   1479            break;
   1480          case "full-page-translations-panel-cancel":
   1481          case "full-page-translations-panel-dismiss-error":
   1482            this.onCancel(event);
   1483            break;
   1484          case "full-page-translations-panel-translate":
   1485            this.onTranslate(event);
   1486            break;
   1487          case "full-page-translations-panel-change-source-language":
   1488            this.onChangeSourceLanguage(event);
   1489            break;
   1490        }
   1491        break;
   1492      }
   1493      case "click": {
   1494        switch (id) {
   1495          case "full-page-translations-panel-intro-learn-more-link":
   1496          case "full-page-translations-panel-unsupported-learn-more-link":
   1497            this.onLearnMoreLink();
   1498            break;
   1499          default:
   1500            this.handlePanelButtonEvent(event);
   1501        }
   1502        break;
   1503      }
   1504      case "popupshown":
   1505        this.handlePanelPopupShownEvent(event);
   1506        break;
   1507      case "popuphidden":
   1508        this.handlePanelPopupHiddenEvent(event);
   1509        break;
   1510      case "TranslationsParent:OfferTranslation": {
   1511        if (Services.wm.getMostRecentBrowserWindow()?.gBrowser === gBrowser) {
   1512          this.open(event, /* reportAsAutoShow */ true);
   1513        }
   1514        break;
   1515      }
   1516      case "TranslationsParent:LanguageState": {
   1517        const { actor, reason } = event.detail;
   1518 
   1519        const innerWindowId =
   1520          gBrowser.selectedBrowser.browsingContext.top.embedderElement
   1521            .innerWindowID;
   1522 
   1523        this.console?.debug("TranslationsParent:LanguageState", {
   1524          reason,
   1525          currentId: innerWindowId,
   1526          originatorId: actor.innerWindowId,
   1527        });
   1528 
   1529        if (innerWindowId !== actor.innerWindowId) {
   1530          // The id of the currently active tab does not match the id of the tab that was active when the event was dispatched.
   1531          // This likely means that the tab was changed after the event was dispatched, but before it was received by this class.
   1532          //
   1533          // Keep in mind that there is only one instance of this class (FullPageTranslationsPanel) for each open Firefox window,
   1534          // but there is one instance of the TranslationsParent actor for each open tab within a Firefox window. As such, it is
   1535          // possible for a tab-specific actor to fire an event that is received by the window-global panel after switching tabs.
   1536          //
   1537          // Since the dispatched event did not originate in the currently active tab, we should not react to it any further.
   1538          return;
   1539        }
   1540 
   1541        const {
   1542          detectedLanguages,
   1543          requestedLanguagePair,
   1544          error,
   1545          isEngineReady,
   1546        } = actor.languageState;
   1547 
   1548        const { button, buttonLocale, buttonCircleArrows } =
   1549          this.buttonElements;
   1550 
   1551        const hasSupportedLanguage =
   1552          detectedLanguages?.docLangTag &&
   1553          detectedLanguages?.userLangTag &&
   1554          detectedLanguages?.isDocLangTagSupported;
   1555 
   1556        if (detectedLanguages) {
   1557          // Ensure the cached detected languages are up to date, for instance whenever
   1558          // the user switches tabs.
   1559          FullPageTranslationsPanel.detectedLanguages = detectedLanguages;
   1560          // Reset the manually selected language when the user switches tabs.
   1561          this.#manuallySelectedToLanguage = null;
   1562        }
   1563 
   1564        if (this.#isPopupOpen) {
   1565          // Make sure to use the language state that is passed by the event.detail, and
   1566          // don't read it from the actor here, as it's possible the actor isn't available
   1567          // via the gBrowser.selectedBrowser.
   1568          this.#updateViewFromTranslationStatus(actor.languageState);
   1569        }
   1570 
   1571        if (
   1572          // We've already requested to translate this page, so always show the icon.
   1573          requestedLanguagePair ||
   1574          // There was an error translating, so always show the icon. This can happen
   1575          // when a user manually invokes the translation and we wouldn't normally show
   1576          // the icon.
   1577          error ||
   1578          // Finally check that we can translate this language.
   1579          (hasSupportedLanguage &&
   1580            TranslationsParent.getIsTranslationsEngineSupported())
   1581        ) {
   1582          // Keep track if the button was originally hidden, because it will be shown now.
   1583          const wasButtonHidden = button.hidden;
   1584 
   1585          button.hidden = false;
   1586          if (requestedLanguagePair) {
   1587            // The translation is active, update the urlbar button.
   1588            button.setAttribute("translationsactive", true);
   1589            if (isEngineReady) {
   1590              const languageDisplayNames =
   1591                TranslationsParent.createLanguageDisplayNames();
   1592 
   1593              document.l10n.setAttributes(
   1594                button,
   1595                "urlbar-translations-button-translated",
   1596                {
   1597                  fromLanguage: languageDisplayNames.of(
   1598                    requestedLanguagePair.sourceLanguage
   1599                  ),
   1600                  toLanguage: languageDisplayNames.of(
   1601                    requestedLanguagePair.targetLanguage
   1602                  ),
   1603                }
   1604              );
   1605              // Show the language tag of translated the page in the button.
   1606              buttonLocale.hidden = false;
   1607              buttonCircleArrows.hidden = true;
   1608              buttonLocale.innerText =
   1609                requestedLanguagePair.targetLanguage.split("-")[0];
   1610            } else {
   1611              document.l10n.setAttributes(
   1612                button,
   1613                "urlbar-translations-button-loading"
   1614              );
   1615              // Show the spinning circle arrows to indicate that the engine is
   1616              // still loading.
   1617              buttonCircleArrows.hidden = false;
   1618              buttonLocale.hidden = true;
   1619            }
   1620          } else {
   1621            // The translation is not active, update the urlbar button.
   1622            button.removeAttribute("translationsactive");
   1623            buttonLocale.hidden = true;
   1624            buttonCircleArrows.hidden = true;
   1625 
   1626            // Follow the same rules for displaying the first-run intro text for the
   1627            // button's accessible tooltip label.
   1628            if (TranslationsParent.hasUserEverTranslated()) {
   1629              document.l10n.setAttributes(
   1630                button,
   1631                "urlbar-translations-button2"
   1632              );
   1633            } else {
   1634              document.l10n.setAttributes(
   1635                button,
   1636                "urlbar-translations-button-intro"
   1637              );
   1638            }
   1639          }
   1640 
   1641          // The button was hidden, but now it is shown.
   1642          if (wasButtonHidden) {
   1643            PageActions.sendPlacedInUrlbarTrigger(button);
   1644          }
   1645        } else if (!button.hidden) {
   1646          // There are no translations visible, hide the button.
   1647          button.hidden = true;
   1648        }
   1649 
   1650        switch (error) {
   1651          case null:
   1652            break;
   1653          case "engine-load-failure":
   1654            this.#showEngineError(actor).catch(viewError =>
   1655              this.console?.error(viewError)
   1656            );
   1657            break;
   1658          default:
   1659            console.error("Unknown translation error", error);
   1660        }
   1661        break;
   1662      }
   1663    }
   1664  };
   1665 })();