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());