languages.js (13759B)
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 /* import-globals-from /toolkit/content/preferencesBindings.js */ 7 8 Preferences.addAll([ 9 { id: "intl.accept_languages", type: "string" }, 10 { id: "pref.browser.language.disable_button.up", type: "bool" }, 11 { id: "pref.browser.language.disable_button.down", type: "bool" }, 12 { id: "pref.browser.language.disable_button.remove", type: "bool" }, 13 { id: "privacy.spoof_english", type: "int" }, 14 ]); 15 Preferences.addSetting({ 16 id: "acceptLanguages", 17 pref: "intl.accept_languages", 18 get(prefVal, _, setting) { 19 return setting.pref.defaultValue != prefVal 20 ? prefVal 21 : Services.locale.acceptLanguages; 22 }, 23 }); 24 25 var gLanguagesDialog = { 26 _availableLanguagesList: [], 27 _acceptLanguages: {}, 28 29 _selectedItemID: null, 30 31 onLoad() { 32 let spoofEnglishElement = document.getElementById("spoofEnglish"); 33 Preferences.addSyncFromPrefListener(spoofEnglishElement, () => 34 gLanguagesDialog.readSpoofEnglish() 35 ); 36 Preferences.addSyncToPrefListener(spoofEnglishElement, () => 37 gLanguagesDialog.writeSpoofEnglish() 38 ); 39 40 Preferences.getSetting("acceptLanguages").on("change", () => 41 this._readAcceptLanguages().catch(console.error) 42 ); 43 44 let addListener = (id, cmd) => { 45 document.getElementById(id).addEventListener(cmd, this); 46 }; 47 addListener("LanguagesDialog", "command"); 48 addListener("availableLanguages", "command"); 49 addListener("activeLanguages", "select"); 50 51 if (!this._availableLanguagesList.length) { 52 document.mozSubdialogReady = this._loadAvailableLanguages(); 53 } 54 }, 55 56 get _activeLanguages() { 57 return document.getElementById("activeLanguages"); 58 }, 59 60 get _availableLanguages() { 61 return document.getElementById("availableLanguages"); 62 }, 63 64 async _loadAvailableLanguages() { 65 // This is a parser for: resource://gre/res/language.properties 66 // The file is formatted like so: 67 // ab[-cd].accept=true|false 68 // ab = language 69 // cd = region 70 var bundleAccepted = document.getElementById("bundleAccepted"); 71 72 function LocaleInfo(aLocaleName, aLocaleCode, aIsVisible) { 73 this.name = aLocaleName; 74 this.code = aLocaleCode; 75 this.isVisible = aIsVisible; 76 } 77 78 // 1) Read the available languages out of language.properties 79 80 let localeCodes = []; 81 let localeValues = []; 82 for (let currString of bundleAccepted.strings) { 83 var property = currString.key.split("."); // ab[-cd].accept 84 if (property[1] == "accept") { 85 localeCodes.push(property[0]); 86 localeValues.push(currString.value); 87 } 88 } 89 90 let localeNames = Services.intl.getLocaleDisplayNames( 91 undefined, 92 localeCodes 93 ); 94 95 for (let i in localeCodes) { 96 let isVisible = 97 localeValues[i] == "true" && 98 (!(localeCodes[i] in this._acceptLanguages) || 99 !this._acceptLanguages[localeCodes[i]]); 100 101 let li = new LocaleInfo(localeNames[i], localeCodes[i], isVisible); 102 this._availableLanguagesList.push(li); 103 } 104 105 await this._buildAvailableLanguageList(); 106 await this._readAcceptLanguages(); 107 }, 108 109 async _buildAvailableLanguageList() { 110 var availableLanguagesPopup = document.getElementById( 111 "availableLanguagesPopup" 112 ); 113 while (availableLanguagesPopup.hasChildNodes()) { 114 availableLanguagesPopup.firstChild.remove(); 115 } 116 117 let frag = document.createDocumentFragment(); 118 119 // Load the UI with the data 120 for (var i = 0; i < this._availableLanguagesList.length; ++i) { 121 let locale = this._availableLanguagesList[i]; 122 let localeCode = locale.code; 123 if ( 124 locale.isVisible && 125 (!(localeCode in this._acceptLanguages) || 126 !this._acceptLanguages[localeCode]) 127 ) { 128 var menuitem = document.createXULElement("menuitem"); 129 menuitem.id = localeCode; 130 document.l10n.setAttributes(menuitem, "languages-code-format", { 131 locale: locale.name, 132 code: localeCode, 133 }); 134 frag.appendChild(menuitem); 135 } 136 } 137 138 await document.l10n.translateFragment(frag); 139 140 // Sort the list of languages by name 141 let comp = new Services.intl.Collator(undefined, { 142 usage: "sort", 143 }); 144 145 let items = Array.from(frag.children); 146 147 items.sort((a, b) => { 148 return comp.compare(a.getAttribute("label"), b.getAttribute("label")); 149 }); 150 151 // Re-append items in the correct order: 152 items.forEach(item => frag.appendChild(item)); 153 154 availableLanguagesPopup.appendChild(frag); 155 156 this._availableLanguages.setAttribute( 157 "label", 158 this._availableLanguages.getAttribute("placeholder") 159 ); 160 }, 161 162 async _readAcceptLanguages() { 163 while (this._activeLanguages.hasChildNodes()) { 164 this._activeLanguages.firstChild.remove(); 165 } 166 167 var selectedIndex = 0; 168 var preference = Preferences.getSetting("acceptLanguages"); 169 if (preference.value == "") { 170 this._activeLanguages.selectedIndex = -1; 171 this.onLanguageSelect(); 172 return; 173 } 174 var languages = preference.value.toLowerCase().split(/\s*,\s*/); 175 for (var i = 0; i < languages.length; ++i) { 176 var listitem = document.createXULElement("richlistitem"); 177 var label = document.createXULElement("label"); 178 listitem.appendChild(label); 179 listitem.id = languages[i]; 180 if (languages[i] == this._selectedItemID) { 181 selectedIndex = i; 182 } 183 this._activeLanguages.appendChild(listitem); 184 var localeName = this._getLocaleName(languages[i]); 185 document.l10n.setAttributes(label, "languages-active-code-format", { 186 locale: localeName, 187 code: languages[i], 188 }); 189 190 // Hash this language as an "Active" language so we don't 191 // show it in the list that can be added. 192 this._acceptLanguages[languages[i]] = true; 193 } 194 195 // We're forcing an early localization here because otherwise 196 // the initial sizing of the dialog will happen before it and 197 // result in overflow. 198 await document.l10n.translateFragment(this._activeLanguages); 199 200 if (this._activeLanguages.childNodes.length) { 201 this._activeLanguages.ensureIndexIsVisible(selectedIndex); 202 this._activeLanguages.selectedIndex = selectedIndex; 203 } 204 205 // Update states of accept-language list and buttons according to 206 // privacy.resistFingerprinting and privacy.spoof_english. 207 this.readSpoofEnglish(); 208 }, 209 210 handleEvent(event) { 211 switch (event.type) { 212 case "command": 213 if (event.currentTarget.id == "availableLanguages") { 214 this.onAvailableLanguageSelect(); 215 break; 216 } 217 218 switch (event.target.id) { 219 case "key_close": 220 Preferences.close(event); 221 break; 222 223 case "up": 224 this.moveUp(); 225 break; 226 case "down": 227 this.moveDown(); 228 break; 229 case "remove": 230 this.removeLanguage(); 231 break; 232 case "addButton": 233 this.addLanguage(); 234 break; 235 } 236 break; 237 case "dialoghelp": 238 window.top.openPrefsHelp(); 239 break; 240 case "load": 241 this.onLoad(); 242 break; 243 case "select": 244 this.onLanguageSelect(); 245 break; 246 } 247 }, 248 249 onAvailableLanguageSelect() { 250 var availableLanguages = this._availableLanguages; 251 var addButton = document.getElementById("addButton"); 252 addButton.disabled = 253 availableLanguages.disabled || availableLanguages.selectedIndex < 0; 254 255 this._availableLanguages.removeAttribute("accesskey"); 256 }, 257 258 addLanguage() { 259 var selectedID = this._availableLanguages.selectedItem.id; 260 var preference = Preferences.getSetting("acceptLanguages"); 261 var arrayOfPrefs = preference.value.toLowerCase().split(/\s*,\s*/); 262 for (var i = 0; i < arrayOfPrefs.length; ++i) { 263 if (arrayOfPrefs[i] == selectedID) { 264 return; 265 } 266 } 267 268 this._selectedItemID = selectedID; 269 270 if (preference.value == "") { 271 preference.value = selectedID; 272 } else { 273 arrayOfPrefs.unshift(selectedID); 274 preference.value = arrayOfPrefs.join(","); 275 } 276 277 this._acceptLanguages[selectedID] = true; 278 this._availableLanguages.selectedItem = null; 279 this.onAvailableLanguageSelect(); 280 281 // Rebuild the available list with the added item removed... 282 this._buildAvailableLanguageList().catch(console.error); 283 }, 284 285 removeLanguage() { 286 // Build the new preference value string. 287 var languagesArray = []; 288 for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { 289 var item = this._activeLanguages.childNodes[i]; 290 if (!item.selected) { 291 languagesArray.push(item.id); 292 } else { 293 this._acceptLanguages[item.id] = false; 294 } 295 } 296 var string = languagesArray.join(","); 297 298 // Get the item to select after the remove operation completes. 299 var selection = this._activeLanguages.selectedItems; 300 var lastSelected = selection[selection.length - 1]; 301 var selectItem = lastSelected.nextSibling || lastSelected.previousSibling; 302 selectItem = selectItem ? selectItem.id : null; 303 304 this._selectedItemID = selectItem; 305 306 // Update the preference and force a UI rebuild 307 var preference = Preferences.getSetting("acceptLanguages"); 308 preference.value = string; 309 310 this._buildAvailableLanguageList().catch(console.error); 311 }, 312 313 _getLocaleName(localeCode) { 314 if (!this._availableLanguagesList.length) { 315 this._loadAvailableLanguages(); 316 } 317 let languageName = ""; 318 for (var i = 0; i < this._availableLanguagesList.length; ++i) { 319 if (localeCode == this._availableLanguagesList[i].code) { 320 return this._availableLanguagesList[i].name; 321 } 322 // Try resolving the locale code without region code. Can't return 323 // directly because there might be a perfect match later. 324 if (localeCode.split("-")[0] == this._availableLanguagesList[i].code) { 325 languageName = this._availableLanguagesList[i].name; 326 } 327 } 328 329 return languageName; 330 }, 331 332 moveUp() { 333 var selectedItem = this._activeLanguages.selectedItems[0]; 334 var previousItem = selectedItem.previousSibling; 335 336 var string = ""; 337 for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { 338 var item = this._activeLanguages.childNodes[i]; 339 string += i == 0 ? "" : ","; 340 if (item.id == previousItem.id) { 341 string += selectedItem.id; 342 } else if (item.id == selectedItem.id) { 343 string += previousItem.id; 344 } else { 345 string += item.id; 346 } 347 } 348 349 this._selectedItemID = selectedItem.id; 350 351 // Update the preference and force a UI rebuild 352 var preference = Preferences.getSetting("acceptLanguages"); 353 preference.value = string; 354 }, 355 356 moveDown() { 357 var selectedItem = this._activeLanguages.selectedItems[0]; 358 var nextItem = selectedItem.nextSibling; 359 360 var string = ""; 361 for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { 362 var item = this._activeLanguages.childNodes[i]; 363 string += i == 0 ? "" : ","; 364 if (item.id == nextItem.id) { 365 string += selectedItem.id; 366 } else if (item.id == selectedItem.id) { 367 string += nextItem.id; 368 } else { 369 string += item.id; 370 } 371 } 372 373 this._selectedItemID = selectedItem.id; 374 375 // Update the preference and force a UI rebuild 376 var preference = Preferences.getSetting("acceptLanguages"); 377 preference.value = string; 378 }, 379 380 onLanguageSelect() { 381 var upButton = document.getElementById("up"); 382 var downButton = document.getElementById("down"); 383 var removeButton = document.getElementById("remove"); 384 switch (this._activeLanguages.selectedCount) { 385 case 0: 386 upButton.disabled = downButton.disabled = removeButton.disabled = true; 387 break; 388 case 1: 389 upButton.disabled = this._activeLanguages.selectedIndex == 0; 390 downButton.disabled = 391 this._activeLanguages.selectedIndex == 392 this._activeLanguages.childNodes.length - 1; 393 removeButton.disabled = false; 394 break; 395 default: 396 upButton.disabled = true; 397 downButton.disabled = true; 398 removeButton.disabled = false; 399 } 400 }, 401 402 readSpoofEnglish() { 403 var checkbox = document.getElementById("spoofEnglish"); 404 var resistFingerprinting = Services.prefs.getBoolPref( 405 "privacy.resistFingerprinting" 406 ); 407 if (!resistFingerprinting) { 408 checkbox.hidden = true; 409 return false; 410 } 411 412 var spoofEnglish = Preferences.get("privacy.spoof_english").value; 413 var activeLanguages = this._activeLanguages; 414 var availableLanguages = this._availableLanguages; 415 checkbox.hidden = false; 416 switch (spoofEnglish) { 417 case 1: // don't spoof intl.accept_languages 418 activeLanguages.disabled = false; 419 activeLanguages.selectItem(activeLanguages.firstChild); 420 availableLanguages.disabled = false; 421 this.onAvailableLanguageSelect(); 422 return false; 423 case 2: // spoof intl.accept_languages 424 activeLanguages.clearSelection(); 425 activeLanguages.disabled = true; 426 availableLanguages.disabled = true; 427 this.onAvailableLanguageSelect(); 428 return true; 429 default: 430 // will prompt for spoofing intl.accept_languages if resisting fingerprinting 431 return false; 432 } 433 }, 434 435 writeSpoofEnglish() { 436 return document.getElementById("spoofEnglish").checked ? 2 : 1; 437 }, 438 }; 439 440 window.addEventListener("load", gLanguagesDialog);