/* -*- 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 */
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)
);
let addListener = (id, cmd) => {
document.getElementById(id).addEventListener(cmd,
this);
};
addListener(
"LanguagesDialog",
"command");
addListener(
"availableLanguages",
"command");
addListener(
"LanguagesDialog",
"dialoghelp");
addListener(
"activeLanguages",
"select");
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 ==
"") {
this._activeLanguages.selectedIndex = -1;
this.onLanguageSelect();
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();
},
handleEvent(event) {
switch (event.type) {
case "command":
if (event.currentTarget.id ==
"availableLanguages") {
this.onAvailableLanguageSelect();
break;
}
switch (event.target.id) {
case "key_close":
Preferences.close(event);
break;
case "up":
this.moveUp();
break;
case "down":
this.moveDown();
break;
case "remove":
this.removeLanguage();
break;
case "addButton":
this.addLanguage();
break;
}
break;
case "dialoghelp":
window.top.openPrefsHelp();
break;
case "load":
this.onLoad();
break;
case "select":
this.onLanguageSelect();
break;
}
},
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;
this.onAvailableLanguageSelect();
// 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;
},
};
window.addEventListener(
"load", gLanguagesDialog);