tor-browser

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

translations.js (15106B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 "use strict";
      7 
      8 /**
      9 * The permission type to give to Services.perms for Translations.
     10 */
     11 const TRANSLATIONS_PERMISSION = "translations";
     12 /**
     13 * The list of BCP-47 language tags that will trigger auto-translate.
     14 */
     15 const ALWAYS_TRANSLATE_LANGS_PREF =
     16  "browser.translations.alwaysTranslateLanguages";
     17 /**
     18 * The list of BCP-47 language tags that will prevent showing Translations UI.
     19 */
     20 const NEVER_TRANSLATE_LANGS_PREF =
     21  "browser.translations.neverTranslateLanguages";
     22 
     23 ChromeUtils.defineESModuleGetters(this, {
     24  TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
     25 });
     26 
     27 function Tree(aId, aData) {
     28  this._data = aData;
     29  this._tree = document.getElementById(aId);
     30  this._tree.view = this;
     31 }
     32 
     33 Tree.prototype = {
     34  get tree() {
     35    return this._tree;
     36  },
     37  get isEmpty() {
     38    return !this._data.length;
     39  },
     40  get hasSelection() {
     41    return this.selection.count > 0;
     42  },
     43  getSelectedItems() {
     44    let result = [];
     45 
     46    let rc = this.selection.getRangeCount();
     47    for (let i = 0; i < rc; ++i) {
     48      let min = {},
     49        max = {};
     50      this.selection.getRangeAt(i, min, max);
     51      for (let j = min.value; j <= max.value; ++j) {
     52        result.push(this._data[j]);
     53      }
     54    }
     55 
     56    return result;
     57  },
     58 
     59  // nsITreeView implementation
     60  get rowCount() {
     61    return this._data.length;
     62  },
     63  getCellText(aRow) {
     64    return this._data[aRow];
     65  },
     66  isSeparator() {
     67    return false;
     68  },
     69  isSorted() {
     70    return false;
     71  },
     72  isContainer() {
     73    return false;
     74  },
     75  setTree() {},
     76  getImageSrc() {},
     77  getCellValue() {},
     78  cycleHeader() {},
     79  getRowProperties() {
     80    return "";
     81  },
     82  getColumnProperties() {
     83    return "";
     84  },
     85  getCellProperties() {
     86    return "";
     87  },
     88  QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
     89 };
     90 
     91 function Lang(aCode, label) {
     92  this.langCode = aCode;
     93  this._label = label;
     94 }
     95 
     96 Lang.prototype = {
     97  toString() {
     98    return this._label;
     99  },
    100 };
    101 
    102 var gTranslationsSettings = {
    103  onLoad() {
    104    if (this._neverTranslateSiteTree) {
    105      // Re-using an open dialog, clear the old observers.
    106      this.removeObservers();
    107    }
    108 
    109    // Load site permissions into an array.
    110    this._neverTranslateSites = TranslationsParent.listNeverTranslateSites();
    111 
    112    // Load language tags into arrays.
    113    this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages();
    114    this._neverTranslateLangs = this.getNeverTranslateLanguages();
    115 
    116    // Add observers for relevant prefs and permissions.
    117    Services.obs.addObserver(this, "perm-changed");
    118    Services.prefs.addObserver(ALWAYS_TRANSLATE_LANGS_PREF, this);
    119    Services.prefs.addObserver(NEVER_TRANSLATE_LANGS_PREF, this);
    120 
    121    window.addEventListener("unload", this);
    122    document.addEventListener("command", this);
    123 
    124    // Build trees from the arrays.
    125    this._alwaysTranslateLangsTree = new Tree(
    126      "alwaysTranslateLanguagesTree",
    127      this._alwaysTranslateLangs
    128    );
    129    this._neverTranslateLangsTree = new Tree(
    130      "neverTranslateLanguagesTree",
    131      this._neverTranslateLangs
    132    );
    133    this._neverTranslateSiteTree = new Tree(
    134      "neverTranslateSitesTree",
    135      this._neverTranslateSites
    136    );
    137 
    138    for (let { tree } of [
    139      this._alwaysTranslateLangsTree,
    140      this._neverTranslateLangsTree,
    141      this._neverTranslateSiteTree,
    142    ]) {
    143      tree.addEventListener("keypress", this);
    144      tree.addEventListener("select", this);
    145    }
    146 
    147    // Ensure the UI for each group is in the correct state.
    148    this.onSelectAlwaysTranslateLanguage();
    149    this.onSelectNeverTranslateLanguage();
    150    this.onSelectNeverTranslateSite();
    151  },
    152 
    153  /**
    154   * Retrieves the value of a char-pref splits its value into an
    155   * array delimited by commas.
    156   *
    157   * This is used for the translations preferences which are comma-
    158   * separated lists of BCP-47 language tags.
    159   *
    160   * @param {string} pref
    161   * @returns {Array<string>}
    162   */
    163  getLangsFromPref(pref) {
    164    let rawLangs = Services.prefs.getCharPref(pref);
    165    if (!rawLangs) {
    166      return [];
    167    }
    168 
    169    let langArr = rawLangs.split(",");
    170    let displayNames = Services.intl.getLanguageDisplayNames(
    171      undefined,
    172      langArr
    173    );
    174    let langs = langArr.map((lang, i) => new Lang(lang, displayNames[i]));
    175    langs.sort();
    176 
    177    return langs;
    178  },
    179 
    180  /**
    181   * Retrieves the always-translate language tags as an array.
    182   *
    183   * @returns {Array<string>}
    184   */
    185  getAlwaysTranslateLanguages() {
    186    return this.getLangsFromPref(ALWAYS_TRANSLATE_LANGS_PREF);
    187  },
    188 
    189  /**
    190   * Retrieves the never-translate language tags as an array.
    191   *
    192   * @returns {Array<string>}
    193   */
    194  getNeverTranslateLanguages() {
    195    return this.getLangsFromPref(NEVER_TRANSLATE_LANGS_PREF);
    196  },
    197 
    198  /**
    199   * Handles updating the UI components on pref or permission changes.
    200   */
    201  observe(aSubject, aTopic, aData) {
    202    if (aTopic === "perm-changed") {
    203      if (aData === "cleared") {
    204        // Permissions have been cleared
    205        if (!this._neverTranslateSites.length) {
    206          // There were no sites with permissions set, nothing to do.
    207          return;
    208        }
    209        // Update the tree based on the amount of permissions removed.
    210        let removed = this._neverTranslateSites.splice(
    211          0,
    212          this._neverTranslateSites.length
    213        );
    214        this._neverTranslateSiteTree.tree.rowCountChanged(0, -removed.length);
    215      } else {
    216        let perm = aSubject.QueryInterface(Ci.nsIPermission);
    217        if (perm.type != TRANSLATIONS_PERMISSION) {
    218          // The updated permission was not for Translations, nothing to do.
    219          return;
    220        }
    221        if (aData === "added") {
    222          if (perm.capability != Services.perms.DENY_ACTION) {
    223            // We are only showing data for sites we should never translate.
    224            // If the permission is not DENY_ACTION, we don't care about it here.
    225            return;
    226          }
    227          this._neverTranslateSites.push(perm.principal.origin);
    228          this._neverTranslateSites.sort();
    229          let tree = this._neverTranslateSiteTree.tree;
    230          tree.rowCountChanged(0, 1);
    231          tree.invalidate();
    232        } else if (aData == "deleted") {
    233          let index = this._neverTranslateSites.indexOf(perm.principal.origin);
    234          if (index == -1) {
    235            // The deleted permission was not in the tree, nothing to do.
    236            return;
    237          }
    238          this._neverTranslateSites.splice(index, 1);
    239          this._neverTranslateSiteTree.tree.rowCountChanged(index, -1);
    240        }
    241      }
    242      // Ensure the UI updates to the changes.
    243      this.onSelectNeverTranslateSite();
    244    } else if (aTopic === "nsPref:changed") {
    245      switch (aData) {
    246        case ALWAYS_TRANSLATE_LANGS_PREF: {
    247          this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages();
    248 
    249          let alwaysTranslateLangsChange =
    250            this._alwaysTranslateLangs.length -
    251            this._alwaysTranslateLangsTree.rowCount;
    252 
    253          this._alwaysTranslateLangsTree._data = this._alwaysTranslateLangs;
    254          let alwaysTranslateLangsTree = this._alwaysTranslateLangsTree.tree;
    255 
    256          if (alwaysTranslateLangsChange) {
    257            alwaysTranslateLangsTree.rowCountChanged(
    258              0,
    259              alwaysTranslateLangsChange
    260            );
    261          }
    262 
    263          alwaysTranslateLangsTree.invalidate();
    264 
    265          // Ensure the UI updates to the changes.
    266          this.onSelectAlwaysTranslateLanguage();
    267          break;
    268        }
    269        case NEVER_TRANSLATE_LANGS_PREF: {
    270          this._neverTranslateLangs = this.getNeverTranslateLanguages();
    271 
    272          let neverTranslateLangsChange =
    273            this._neverTranslateLangs.length -
    274            this._neverTranslateLangsTree.rowCount;
    275 
    276          this._neverTranslateLangsTree._data = this._neverTranslateLangs;
    277          let neverTranslateLangsTree = this._neverTranslateLangsTree.tree;
    278 
    279          if (neverTranslateLangsChange) {
    280            neverTranslateLangsTree.rowCountChanged(
    281              0,
    282              neverTranslateLangsChange
    283            );
    284          }
    285 
    286          neverTranslateLangsTree.invalidate();
    287 
    288          // Ensure the UI updates to the changes.
    289          this.onSelectNeverTranslateLanguage();
    290          break;
    291        }
    292      }
    293    }
    294  },
    295 
    296  handleEvent(event) {
    297    switch (event.type) {
    298      case "unload":
    299        this.removeObservers();
    300        break;
    301      case "command":
    302        switch (event.target.id) {
    303          case "key_close":
    304            window.close();
    305            break;
    306 
    307          case "removeAlwaysTranslateLanguage":
    308            this.onRemoveAlwaysTranslateLanguage();
    309            break;
    310          case "removeAllAlwaysTranslateLanguages":
    311            this.onRemoveAllAlwaysTranslateLanguages();
    312            break;
    313          case "removeNeverTranslateLanguage":
    314            this.onRemoveNeverTranslateLanguage();
    315            break;
    316          case "removeAllNeverTranslateLanguages":
    317            this.onRemoveAllNeverTranslateLanguages();
    318            break;
    319          case "removeNeverTranslateSite":
    320            this.onRemoveNeverTranslateSite();
    321            break;
    322          case "removeAllNeverTranslateSites":
    323            this.onRemoveAllNeverTranslateSites();
    324            break;
    325        }
    326        break;
    327      case "keypress":
    328        switch (event.currentTarget.id) {
    329          case "alwaysTranslateLanguagesTree":
    330            this.onAlwaysTranslateLanguageKeyPress(event);
    331            break;
    332          case "neverTranslateLanguagesTree":
    333            this.onNeverTranslateLanguageKeyPress(event);
    334            break;
    335          case "neverTranslateSitesTree":
    336            this.onNeverTranslateSiteKeyPress(event);
    337            break;
    338        }
    339        break;
    340      case "select":
    341        switch (event.currentTarget.id) {
    342          case "alwaysTranslateLanguagesTree":
    343            this.onSelectAlwaysTranslateLanguage();
    344            break;
    345          case "neverTranslateLanguagesTree":
    346            this.onSelectNeverTranslateLanguage();
    347            break;
    348          case "neverTranslateSitesTree":
    349            this.onSelectNeverTranslateSite();
    350            break;
    351        }
    352        break;
    353    }
    354  },
    355 
    356  /**
    357   * Ensures that buttons states are enabled/disabled accordingly based on the
    358   * content of the trees.
    359   *
    360   * The remove button should be enabled only if an item is selected.
    361   * The removeAll button should be enabled any time the tree has content.
    362   *
    363   * @param {Tree} aTree
    364   * @param {string} aIdPart
    365   */
    366  _handleButtonDisabling(aTree, aIdPart) {
    367    let empty = aTree.isEmpty;
    368    document.getElementById("removeAll" + aIdPart + "s").disabled = empty;
    369    document.getElementById("remove" + aIdPart).disabled =
    370      empty || !aTree.hasSelection;
    371  },
    372 
    373  /**
    374   * Updates the UI state for the always-translate languages section.
    375   */
    376  onSelectAlwaysTranslateLanguage() {
    377    this._handleButtonDisabling(
    378      this._alwaysTranslateLangsTree,
    379      "AlwaysTranslateLanguage"
    380    );
    381  },
    382 
    383  /**
    384   * Updates the UI state for the never-translate languages section.
    385   */
    386  onSelectNeverTranslateLanguage() {
    387    this._handleButtonDisabling(
    388      this._neverTranslateLangsTree,
    389      "NeverTranslateLanguage"
    390    );
    391  },
    392 
    393  /**
    394   * Updates the UI state for the never-translate sites section.
    395   */
    396  onSelectNeverTranslateSite() {
    397    this._handleButtonDisabling(
    398      this._neverTranslateSiteTree,
    399      "NeverTranslateSite"
    400    );
    401  },
    402 
    403  /**
    404   * Updates the value of a language pref to match when a language is removed
    405   * through the UI.
    406   *
    407   * @param {string} pref
    408   * @param {Tree} tree
    409   */
    410  _onRemoveLanguage(pref, tree) {
    411    let langs = Services.prefs.getCharPref(pref);
    412    if (!langs) {
    413      return;
    414    }
    415 
    416    let removed = tree.getSelectedItems().map(l => l.langCode);
    417 
    418    langs = langs.split(",").filter(l => !removed.includes(l));
    419    Services.prefs.setCharPref(pref, langs.join(","));
    420  },
    421 
    422  /**
    423   * Updates the never-translate language pref when a never-translate language
    424   * is removed via the UI.
    425   */
    426  onRemoveAlwaysTranslateLanguage() {
    427    this._onRemoveLanguage(
    428      ALWAYS_TRANSLATE_LANGS_PREF,
    429      this._alwaysTranslateLangsTree
    430    );
    431  },
    432 
    433  /**
    434   * Updates the always-translate language pref when a always-translate language
    435   * is removed via the UI.
    436   */
    437  onRemoveNeverTranslateLanguage() {
    438    this._onRemoveLanguage(
    439      NEVER_TRANSLATE_LANGS_PREF,
    440      this._neverTranslateLangsTree
    441    );
    442  },
    443 
    444  /**
    445   * Updates the permissions for a never-translate site when it is removed via the UI.
    446   */
    447  onRemoveNeverTranslateSite() {
    448    let removedNeverTranslateSites =
    449      this._neverTranslateSiteTree.getSelectedItems();
    450    for (let origin of removedNeverTranslateSites) {
    451      TranslationsParent.setNeverTranslateSiteByOrigin(false, origin);
    452    }
    453  },
    454 
    455  /**
    456   * Clears the always-translate languages pref when the list is cleared in the UI.
    457   */
    458  onRemoveAllAlwaysTranslateLanguages() {
    459    Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "");
    460  },
    461 
    462  /**
    463   * Clears the never-translate languages pref when the list is cleared in the UI.
    464   */
    465  onRemoveAllNeverTranslateLanguages() {
    466    Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "");
    467  },
    468 
    469  /**
    470   * Clears the never-translate sites pref when the list is cleared in the UI.
    471   */
    472  onRemoveAllNeverTranslateSites() {
    473    if (this._neverTranslateSiteTree.isEmpty) {
    474      return;
    475    }
    476 
    477    let removedNeverTranslateSites = this._neverTranslateSites.splice(
    478      0,
    479      this._neverTranslateSites.length
    480    );
    481    this._neverTranslateSiteTree.tree.rowCountChanged(
    482      0,
    483      -removedNeverTranslateSites.length
    484    );
    485 
    486    for (let origin of removedNeverTranslateSites) {
    487      TranslationsParent.setNeverTranslateSiteByOrigin(false, origin);
    488    }
    489 
    490    this.onSelectNeverTranslateSite();
    491  },
    492 
    493  /**
    494   * Handles removing a selected always-translate language via the keyboard.
    495   */
    496  onAlwaysTranslateLanguageKeyPress(aEvent) {
    497    if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
    498      this.onRemoveAlwaysTranslateLanguage();
    499    }
    500  },
    501 
    502  /**
    503   * Handles removing a selected never-translate language via the keyboard.
    504   */
    505  onNeverTranslateLanguageKeyPress(aEvent) {
    506    if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
    507      this.onRemoveNeverTranslateLanguage();
    508    }
    509  },
    510 
    511  /**
    512   * Handles removing a selected never-translate site via the keyboard.
    513   */
    514  onNeverTranslateSiteKeyPress(aEvent) {
    515    if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
    516      this.onRemoveNeverTranslateSite();
    517    }
    518  },
    519 
    520  /**
    521   * Removes any active preference and permissions observers.
    522   */
    523  removeObservers() {
    524    Services.obs.removeObserver(this, "perm-changed");
    525    Services.prefs.removeObserver(ALWAYS_TRANSLATE_LANGS_PREF, this);
    526    Services.prefs.removeObserver(NEVER_TRANSLATE_LANGS_PREF, this);
    527  },
    528 };
    529 
    530 window.addEventListener("load", () => gTranslationsSettings.onLoad());