/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* import-globals-from /toolkit/content/preferencesBindings.js */ document .getElementById("LanguagesDialog") .addEventListener("dialoghelp", window.top.openPrefsHelp); Preferences.addAll([ { id: "intl.accept_languages", type: "wstring" }, { id: "pref.browser.language.disable_button.up", type: "bool" }, { id: "pref.browser.language.disable_button.down", type: "bool" }, { id: "pref.browser.language.disable_button.remove", type: "bool" }, { id: "privacy.spoof_english", type: "int" }, ]); var gLanguagesDialog = { _availableLanguagesList: [], _acceptLanguages: {}, _selectedItemID: null, onLoad() { let spoofEnglishElement = document.getElementById("spoofEnglish"); Preferences.addSyncFromPrefListener(spoofEnglishElement, () => gLanguagesDialog.readSpoofEnglish() ); Preferences.addSyncToPrefListener(spoofEnglishElement, () => gLanguagesDialog.writeSpoofEnglish() ); Preferences.get("intl.accept_languages").on("change", () => this._readAcceptLanguages().catch(console.error) ); if (!this._availableLanguagesList.length) { document.mozSubdialogReady = this._loadAvailableLanguages(); } }, get _activeLanguages() { return document.getElementById("activeLanguages"); }, get _availableLanguages() { return document.getElementById("availableLanguages"); }, async _loadAvailableLanguages() { // This is a parser for: resource://gre/res/language.properties // The file is formatted like so: // ab[-cd].accept=true|false // ab = language // cd = region var bundleAccepted = document.getElementById("bundleAccepted"); function LocaleInfo(aLocaleName, aLocaleCode, aIsVisible) { this.name = aLocaleName; this.code = aLocaleCode; this.isVisible = aIsVisible; } // 1) Read the available languages out of language.properties let localeCodes = []; let localeValues = []; for (let currString of bundleAccepted.strings) { var property = currString.key.split("."); // ab[-cd].accept if (property[1] == "accept") { localeCodes.push(property[0]); localeValues.push(currString.value); } } let localeNames = Services.intl.getLocaleDisplayNames( undefined, localeCodes ); for (let i in localeCodes) { let isVisible = localeValues[i] == "true" && (!(localeCodes[i] in this._acceptLanguages) || !this._acceptLanguages[localeCodes[i]]); let li = new LocaleInfo(localeNames[i], localeCodes[i], isVisible); this._availableLanguagesList.push(li); } await this._buildAvailableLanguageList(); await this._readAcceptLanguages(); }, async _buildAvailableLanguageList() { var availableLanguagesPopup = document.getElementById( "availableLanguagesPopup" ); while (availableLanguagesPopup.hasChildNodes()) { availableLanguagesPopup.firstChild.remove(); } let frag = document.createDocumentFragment(); // Load the UI with the data for (var i = 0; i < this._availableLanguagesList.length; ++i) { let locale = this._availableLanguagesList[i]; let localeCode = locale.code; if ( locale.isVisible && (!(localeCode in this._acceptLanguages) || !this._acceptLanguages[localeCode]) ) { var menuitem = document.createXULElement("menuitem"); menuitem.id = localeCode; document.l10n.setAttributes(menuitem, "languages-code-format", { locale: locale.name, code: localeCode, }); frag.appendChild(menuitem); } } await document.l10n.translateFragment(frag); // Sort the list of languages by name let comp = new Services.intl.Collator(undefined, { usage: "sort", }); let items = Array.from(frag.children); items.sort((a, b) => { return comp.compare(a.getAttribute("label"), b.getAttribute("label")); }); // Re-append items in the correct order: items.forEach(item => frag.appendChild(item)); availableLanguagesPopup.appendChild(frag); this._availableLanguages.setAttribute( "label", this._availableLanguages.getAttribute("placeholder") ); }, async _readAcceptLanguages() { while (this._activeLanguages.hasChildNodes()) { this._activeLanguages.firstChild.remove(); } var selectedIndex = 0; var preference = Preferences.get("intl.accept_languages"); if (preference.value == "") { return; } var languages = preference.value.toLowerCase().split(/\s*,\s*/); for (var i = 0; i < languages.length; ++i) { var listitem = document.createXULElement("richlistitem"); var label = document.createXULElement("label"); listitem.appendChild(label); listitem.id = languages[i]; if (languages[i] == this._selectedItemID) { selectedIndex = i; } this._activeLanguages.appendChild(listitem); var localeName = this._getLocaleName(languages[i]); document.l10n.setAttributes(label, "languages-active-code-format", { locale: localeName, code: languages[i], }); // Hash this language as an "Active" language so we don't // show it in the list that can be added. this._acceptLanguages[languages[i]] = true; } // We're forcing an early localization here because otherwise // the initial sizing of the dialog will happen before it and // result in overflow. await document.l10n.translateFragment(this._activeLanguages); if (this._activeLanguages.childNodes.length) { this._activeLanguages.ensureIndexIsVisible(selectedIndex); this._activeLanguages.selectedIndex = selectedIndex; } // Update states of accept-language list and buttons according to // privacy.resistFingerprinting and privacy.spoof_english. this.readSpoofEnglish(); }, onAvailableLanguageSelect() { var availableLanguages = this._availableLanguages; var addButton = document.getElementById("addButton"); addButton.disabled = availableLanguages.disabled || availableLanguages.selectedIndex < 0; this._availableLanguages.removeAttribute("accesskey"); }, addLanguage() { var selectedID = this._availableLanguages.selectedItem.id; var preference = Preferences.get("intl.accept_languages"); var arrayOfPrefs = preference.value.toLowerCase().split(/\s*,\s*/); for (var i = 0; i < arrayOfPrefs.length; ++i) { if (arrayOfPrefs[i] == selectedID) { return; } } this._selectedItemID = selectedID; if (preference.value == "") { preference.value = selectedID; } else { arrayOfPrefs.unshift(selectedID); preference.value = arrayOfPrefs.join(","); } this._acceptLanguages[selectedID] = true; this._availableLanguages.selectedItem = null; // Rebuild the available list with the added item removed... this._buildAvailableLanguageList().catch(console.error); }, removeLanguage() { // Build the new preference value string. var languagesArray = []; for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { var item = this._activeLanguages.childNodes[i]; if (!item.selected) { languagesArray.push(item.id); } else { this._acceptLanguages[item.id] = false; } } var string = languagesArray.join(","); // Get the item to select after the remove operation completes. var selection = this._activeLanguages.selectedItems; var lastSelected = selection[selection.length - 1]; var selectItem = lastSelected.nextSibling || lastSelected.previousSibling; selectItem = selectItem ? selectItem.id : null; this._selectedItemID = selectItem; // Update the preference and force a UI rebuild var preference = Preferences.get("intl.accept_languages"); preference.value = string; this._buildAvailableLanguageList().catch(console.error); }, _getLocaleName(localeCode) { if (!this._availableLanguagesList.length) { this._loadAvailableLanguages(); } let languageName = ""; for (var i = 0; i < this._availableLanguagesList.length; ++i) { if (localeCode == this._availableLanguagesList[i].code) { return this._availableLanguagesList[i].name; } // Try resolving the locale code without region code. Can't return // directly because there might be a perfect match later. if (localeCode.split("-")[0] == this._availableLanguagesList[i].code) { languageName = this._availableLanguagesList[i].name; } } return languageName; }, moveUp() { var selectedItem = this._activeLanguages.selectedItems[0]; var previousItem = selectedItem.previousSibling; var string = ""; for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { var item = this._activeLanguages.childNodes[i]; string += i == 0 ? "" : ","; if (item.id == previousItem.id) { string += selectedItem.id; } else if (item.id == selectedItem.id) { string += previousItem.id; } else { string += item.id; } } this._selectedItemID = selectedItem.id; // Update the preference and force a UI rebuild var preference = Preferences.get("intl.accept_languages"); preference.value = string; }, moveDown() { var selectedItem = this._activeLanguages.selectedItems[0]; var nextItem = selectedItem.nextSibling; var string = ""; for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { var item = this._activeLanguages.childNodes[i]; string += i == 0 ? "" : ","; if (item.id == nextItem.id) { string += selectedItem.id; } else if (item.id == selectedItem.id) { string += nextItem.id; } else { string += item.id; } } this._selectedItemID = selectedItem.id; // Update the preference and force a UI rebuild var preference = Preferences.get("intl.accept_languages"); preference.value = string; }, onLanguageSelect() { var upButton = document.getElementById("up"); var downButton = document.getElementById("down"); var removeButton = document.getElementById("remove"); switch (this._activeLanguages.selectedCount) { case 0: upButton.disabled = downButton.disabled = removeButton.disabled = true; break; case 1: upButton.disabled = this._activeLanguages.selectedIndex == 0; downButton.disabled = this._activeLanguages.selectedIndex == this._activeLanguages.childNodes.length - 1; removeButton.disabled = false; break; default: upButton.disabled = true; downButton.disabled = true; removeButton.disabled = false; } }, readSpoofEnglish() { var checkbox = document.getElementById("spoofEnglish"); var resistFingerprinting = Services.prefs.getBoolPref( "privacy.resistFingerprinting" ); if (!resistFingerprinting) { checkbox.hidden = true; return false; } var spoofEnglish = Preferences.get("privacy.spoof_english").value; var activeLanguages = this._activeLanguages; var availableLanguages = this._availableLanguages; checkbox.hidden = false; switch (spoofEnglish) { case 1: // don't spoof intl.accept_languages activeLanguages.disabled = false; activeLanguages.selectItem(activeLanguages.firstChild); availableLanguages.disabled = false; this.onAvailableLanguageSelect(); return false; case 2: // spoof intl.accept_languages activeLanguages.clearSelection(); activeLanguages.disabled = true; availableLanguages.disabled = true; this.onAvailableLanguageSelect(); return true; default: // will prompt for spoofing intl.accept_languages if resisting fingerprinting return false; } }, writeSpoofEnglish() { return document.getElementById("spoofEnglish").checked ? 2 : 1; }, };