mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			647 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			647 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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 */
 | 
						|
 | 
						|
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
 | 
						|
// This is exported by preferences.js but we can't import that in a subdialog.
 | 
						|
let { getAvailableLocales } = window.top;
 | 
						|
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "AddonManager",
 | 
						|
  "resource://gre/modules/AddonManager.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "AddonRepository",
 | 
						|
  "resource://gre/modules/addons/AddonRepository.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "RemoteSettings",
 | 
						|
  "resource://services-settings/remote-settings.js"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "SelectionChangedMenulist",
 | 
						|
  "resource:///modules/SelectionChangedMenulist.jsm"
 | 
						|
);
 | 
						|
 | 
						|
document
 | 
						|
  .getElementById("BrowserLanguagesDialog")
 | 
						|
  .addEventListener("dialoghelp", window.top.openPrefsHelp);
 | 
						|
 | 
						|
/* This dialog provides an interface for managing what language the browser is
 | 
						|
 * displayed in.
 | 
						|
 *
 | 
						|
 * There is a list of "requested" locales and a list of "available" locales. The
 | 
						|
 * requested locales must be installed and enabled. Available locales could be
 | 
						|
 * installed and enabled, or fetched from the AMO language tools API.
 | 
						|
 *
 | 
						|
 * If a langpack is disabled, there is no way to determine what locale it is for and
 | 
						|
 * it will only be listed as available if that locale is also available on AMO and
 | 
						|
 * the user has opted to search for more languages.
 | 
						|
 */
 | 
						|
 | 
						|
async function installFromUrl(url, hash, callback) {
 | 
						|
  let telemetryInfo = {
 | 
						|
    source: "about:preferences",
 | 
						|
  };
 | 
						|
  let install = await AddonManager.getInstallForURL(url, {
 | 
						|
    hash,
 | 
						|
    telemetryInfo,
 | 
						|
  });
 | 
						|
  if (callback) {
 | 
						|
    callback(install.installId.toString());
 | 
						|
  }
 | 
						|
  await install.install();
 | 
						|
  return install.addon;
 | 
						|
}
 | 
						|
 | 
						|
async function dictionaryIdsForLocale(locale) {
 | 
						|
  let entries = await RemoteSettings("language-dictionaries").get({
 | 
						|
    filters: { id: locale },
 | 
						|
  });
 | 
						|
  if (entries.length) {
 | 
						|
    return entries[0].dictionaries;
 | 
						|
  }
 | 
						|
  return [];
 | 
						|
}
 | 
						|
 | 
						|
class OrderedListBox {
 | 
						|
  constructor({
 | 
						|
    richlistbox,
 | 
						|
    upButton,
 | 
						|
    downButton,
 | 
						|
    removeButton,
 | 
						|
    onRemove,
 | 
						|
    onReorder,
 | 
						|
  }) {
 | 
						|
    this.richlistbox = richlistbox;
 | 
						|
    this.upButton = upButton;
 | 
						|
    this.downButton = downButton;
 | 
						|
    this.removeButton = removeButton;
 | 
						|
    this.onRemove = onRemove;
 | 
						|
    this.onReorder = onReorder;
 | 
						|
 | 
						|
    this.items = [];
 | 
						|
 | 
						|
    this.richlistbox.addEventListener("select", () => this.setButtonState());
 | 
						|
    this.upButton.addEventListener("command", () => this.moveUp());
 | 
						|
    this.downButton.addEventListener("command", () => this.moveDown());
 | 
						|
    this.removeButton.addEventListener("command", () => this.removeItem());
 | 
						|
  }
 | 
						|
 | 
						|
  get selectedItem() {
 | 
						|
    return this.items[this.richlistbox.selectedIndex];
 | 
						|
  }
 | 
						|
 | 
						|
  setButtonState() {
 | 
						|
    let { upButton, downButton, removeButton } = this;
 | 
						|
    let { selectedIndex, itemCount } = this.richlistbox;
 | 
						|
    upButton.disabled = selectedIndex <= 0;
 | 
						|
    downButton.disabled = selectedIndex == itemCount - 1;
 | 
						|
    removeButton.disabled = itemCount <= 1 || !this.selectedItem.canRemove;
 | 
						|
  }
 | 
						|
 | 
						|
  moveUp() {
 | 
						|
    let { selectedIndex } = this.richlistbox;
 | 
						|
    if (selectedIndex == 0) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let { items } = this;
 | 
						|
    let selectedItem = items[selectedIndex];
 | 
						|
    let prevItem = items[selectedIndex - 1];
 | 
						|
    items[selectedIndex - 1] = items[selectedIndex];
 | 
						|
    items[selectedIndex] = prevItem;
 | 
						|
    let prevEl = document.getElementById(prevItem.id);
 | 
						|
    let selectedEl = document.getElementById(selectedItem.id);
 | 
						|
    this.richlistbox.insertBefore(selectedEl, prevEl);
 | 
						|
    this.richlistbox.ensureElementIsVisible(selectedEl);
 | 
						|
    this.setButtonState();
 | 
						|
 | 
						|
    this.onReorder();
 | 
						|
  }
 | 
						|
 | 
						|
  moveDown() {
 | 
						|
    let { selectedIndex } = this.richlistbox;
 | 
						|
    if (selectedIndex == this.items.length - 1) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let { items } = this;
 | 
						|
    let selectedItem = items[selectedIndex];
 | 
						|
    let nextItem = items[selectedIndex + 1];
 | 
						|
    items[selectedIndex + 1] = items[selectedIndex];
 | 
						|
    items[selectedIndex] = nextItem;
 | 
						|
    let nextEl = document.getElementById(nextItem.id);
 | 
						|
    let selectedEl = document.getElementById(selectedItem.id);
 | 
						|
    this.richlistbox.insertBefore(nextEl, selectedEl);
 | 
						|
    this.richlistbox.ensureElementIsVisible(selectedEl);
 | 
						|
    this.setButtonState();
 | 
						|
 | 
						|
    this.onReorder();
 | 
						|
  }
 | 
						|
 | 
						|
  removeItem() {
 | 
						|
    let { selectedIndex } = this.richlistbox;
 | 
						|
 | 
						|
    if (selectedIndex == -1) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let [item] = this.items.splice(selectedIndex, 1);
 | 
						|
    this.richlistbox.selectedItem.remove();
 | 
						|
    this.richlistbox.selectedIndex = Math.min(
 | 
						|
      selectedIndex,
 | 
						|
      this.richlistbox.itemCount - 1
 | 
						|
    );
 | 
						|
    this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
 | 
						|
    this.onRemove(item);
 | 
						|
  }
 | 
						|
 | 
						|
  setItems(items) {
 | 
						|
    this.items = items;
 | 
						|
    this.populate();
 | 
						|
    this.setButtonState();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Add an item to the top of the ordered list.
 | 
						|
   *
 | 
						|
   * @param {object} item The item to insert.
 | 
						|
   */
 | 
						|
  addItem(item) {
 | 
						|
    this.items.unshift(item);
 | 
						|
    this.richlistbox.insertBefore(
 | 
						|
      this.createItem(item),
 | 
						|
      this.richlistbox.firstElementChild
 | 
						|
    );
 | 
						|
    this.richlistbox.selectedIndex = 0;
 | 
						|
    this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
 | 
						|
  }
 | 
						|
 | 
						|
  populate() {
 | 
						|
    this.richlistbox.textContent = "";
 | 
						|
 | 
						|
    let frag = document.createDocumentFragment();
 | 
						|
    for (let item of this.items) {
 | 
						|
      frag.appendChild(this.createItem(item));
 | 
						|
    }
 | 
						|
    this.richlistbox.appendChild(frag);
 | 
						|
 | 
						|
    this.richlistbox.selectedIndex = 0;
 | 
						|
    this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
 | 
						|
  }
 | 
						|
 | 
						|
  createItem({ id, label, value }) {
 | 
						|
    let listitem = document.createXULElement("richlistitem");
 | 
						|
    listitem.id = id;
 | 
						|
    listitem.setAttribute("value", value);
 | 
						|
 | 
						|
    let labelEl = document.createXULElement("label");
 | 
						|
    labelEl.textContent = label;
 | 
						|
    listitem.appendChild(labelEl);
 | 
						|
 | 
						|
    return listitem;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class SortedItemSelectList {
 | 
						|
  constructor({ menulist, button, onSelect, onChange, compareFn }) {
 | 
						|
    this.menulist = menulist;
 | 
						|
    this.popup = menulist.menupopup;
 | 
						|
    this.button = button;
 | 
						|
    this.compareFn = compareFn;
 | 
						|
    this.items = [];
 | 
						|
 | 
						|
    // This will register the "command" listener.
 | 
						|
    new SelectionChangedMenulist(this.menulist, () => {
 | 
						|
      button.disabled = !menulist.selectedItem;
 | 
						|
      if (menulist.selectedItem) {
 | 
						|
        onChange(this.items[menulist.selectedIndex]);
 | 
						|
      }
 | 
						|
    });
 | 
						|
    button.addEventListener("command", () => {
 | 
						|
      if (!menulist.selectedItem) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      let [item] = this.items.splice(menulist.selectedIndex, 1);
 | 
						|
      menulist.selectedItem.remove();
 | 
						|
      menulist.setAttribute("label", menulist.getAttribute("placeholder"));
 | 
						|
      button.disabled = true;
 | 
						|
      menulist.disabled = menulist.itemCount == 0;
 | 
						|
      menulist.selectedIndex = -1;
 | 
						|
 | 
						|
      onSelect(item);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  setItems(items) {
 | 
						|
    this.items = items.sort(this.compareFn);
 | 
						|
    this.populate();
 | 
						|
  }
 | 
						|
 | 
						|
  populate() {
 | 
						|
    let { button, items, menulist, popup } = this;
 | 
						|
    popup.textContent = "";
 | 
						|
 | 
						|
    let frag = document.createDocumentFragment();
 | 
						|
    for (let item of items) {
 | 
						|
      frag.appendChild(this.createItem(item));
 | 
						|
    }
 | 
						|
    popup.appendChild(frag);
 | 
						|
 | 
						|
    menulist.setAttribute("label", menulist.getAttribute("placeholder"));
 | 
						|
    menulist.disabled = menulist.itemCount == 0;
 | 
						|
    menulist.selectedIndex = -1;
 | 
						|
    button.disabled = true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Add an item to the list sorted by the label.
 | 
						|
   *
 | 
						|
   * @param {object} item The item to insert.
 | 
						|
   */
 | 
						|
  addItem(item) {
 | 
						|
    let { compareFn, items, menulist, popup } = this;
 | 
						|
 | 
						|
    // Find the index of the item to insert before.
 | 
						|
    let i = items.findIndex(el => compareFn(el, item) >= 0);
 | 
						|
    items.splice(i, 0, item);
 | 
						|
    popup.insertBefore(this.createItem(item), menulist.getItemAtIndex(i));
 | 
						|
 | 
						|
    menulist.disabled = menulist.itemCount == 0;
 | 
						|
  }
 | 
						|
 | 
						|
  createItem({ label, value, className, disabled }) {
 | 
						|
    let item = document.createXULElement("menuitem");
 | 
						|
    item.setAttribute("label", label);
 | 
						|
    if (value) {
 | 
						|
      item.value = value;
 | 
						|
    }
 | 
						|
    if (className) {
 | 
						|
      item.classList.add(className);
 | 
						|
    }
 | 
						|
    if (disabled) {
 | 
						|
      item.setAttribute("disabled", "true");
 | 
						|
    }
 | 
						|
    return item;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Disable the inputs and set a data-l10n-id on the menulist. This can be
 | 
						|
   * reverted with `enableWithMessageId()`.
 | 
						|
   */
 | 
						|
  disableWithMessageId(messageId) {
 | 
						|
    this.menulist.setAttribute("data-l10n-id", messageId);
 | 
						|
    this.menulist.setAttribute(
 | 
						|
      "image",
 | 
						|
      "chrome://browser/skin/tabbrowser/tab-connecting.png"
 | 
						|
    );
 | 
						|
    this.menulist.disabled = true;
 | 
						|
    this.button.disabled = true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Enable the inputs and set a data-l10n-id on the menulist. This can be
 | 
						|
   * reverted with `disableWithMessageId()`.
 | 
						|
   */
 | 
						|
  enableWithMessageId(messageId) {
 | 
						|
    this.menulist.setAttribute("data-l10n-id", messageId);
 | 
						|
    this.menulist.removeAttribute("image");
 | 
						|
    this.menulist.disabled = this.menulist.itemCount == 0;
 | 
						|
    this.button.disabled = !this.menulist.selectedItem;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
async function getLocaleDisplayInfo(localeCodes) {
 | 
						|
  let availableLocales = new Set(await getAvailableLocales());
 | 
						|
  let packagedLocales = new Set(Services.locale.packagedLocales);
 | 
						|
  let localeNames = Services.intl.getLocaleDisplayNames(undefined, localeCodes);
 | 
						|
  return localeCodes.map((code, i) => {
 | 
						|
    return {
 | 
						|
      id: "locale-" + code,
 | 
						|
      label: localeNames[i],
 | 
						|
      value: code,
 | 
						|
      canRemove: !packagedLocales.has(code),
 | 
						|
      installed: availableLocales.has(code),
 | 
						|
    };
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function compareItems(a, b) {
 | 
						|
  // Sort by installed.
 | 
						|
  if (a.installed != b.installed) {
 | 
						|
    return a.installed ? -1 : 1;
 | 
						|
 | 
						|
    // The search label is always last.
 | 
						|
  } else if (a.value == "search") {
 | 
						|
    return 1;
 | 
						|
  } else if (b.value == "search") {
 | 
						|
    return -1;
 | 
						|
 | 
						|
    // If both items are locales, sort by label.
 | 
						|
  } else if (a.value && b.value) {
 | 
						|
    return a.label.localeCompare(b.label);
 | 
						|
 | 
						|
    // One of them is a label, put it first.
 | 
						|
  } else if (a.value) {
 | 
						|
    return 1;
 | 
						|
  }
 | 
						|
  return -1;
 | 
						|
}
 | 
						|
 | 
						|
var gBrowserLanguagesDialog = {
 | 
						|
  telemetryId: null,
 | 
						|
  accepted: false,
 | 
						|
  _availableLocales: null,
 | 
						|
  _selectedLocales: null,
 | 
						|
  selectedLocales: null,
 | 
						|
 | 
						|
  get downloadEnabled() {
 | 
						|
    // Downloading langpacks isn't always supported, check the pref.
 | 
						|
    return Services.prefs.getBoolPref("intl.multilingual.downloadEnabled");
 | 
						|
  },
 | 
						|
 | 
						|
  recordTelemetry(method, extra = null) {
 | 
						|
    Services.telemetry.recordEvent(
 | 
						|
      "intl.ui.browserLanguage",
 | 
						|
      method,
 | 
						|
      "dialog",
 | 
						|
      this.telemetryId,
 | 
						|
      extra
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  beforeAccept() {
 | 
						|
    this.selected = this.getSelectedLocales();
 | 
						|
    this.accepted = true;
 | 
						|
  },
 | 
						|
 | 
						|
  async onLoad() {
 | 
						|
    document
 | 
						|
      .getElementById("BrowserLanguagesDialog")
 | 
						|
      .addEventListener("beforeaccept", () => this.beforeAccept());
 | 
						|
    // Maintain the previously selected locales even if we cancel out.
 | 
						|
    let { telemetryId, selected, search } = window.arguments[0];
 | 
						|
    this.telemetryId = telemetryId;
 | 
						|
    this.selectedLocales = selected;
 | 
						|
 | 
						|
    // This is a list of available locales that the user selected. It's more
 | 
						|
    // restricted than the Intl notion of `requested` as it only contains
 | 
						|
    // locale codes for which we have matching locales available.
 | 
						|
    // The first time this dialog is opened, populate with appLocalesAsBCP47.
 | 
						|
    let selectedLocales =
 | 
						|
      this.selectedLocales || Services.locale.appLocalesAsBCP47;
 | 
						|
    let selectedLocaleSet = new Set(selectedLocales);
 | 
						|
    let available = await getAvailableLocales();
 | 
						|
    let availableSet = new Set(available);
 | 
						|
 | 
						|
    // Filter selectedLocales since the user may select a locale when it is
 | 
						|
    // available and then disable it.
 | 
						|
    selectedLocales = selectedLocales.filter(locale =>
 | 
						|
      availableSet.has(locale)
 | 
						|
    );
 | 
						|
    // Nothing in available should be in selectedSet.
 | 
						|
    available = available.filter(locale => !selectedLocaleSet.has(locale));
 | 
						|
 | 
						|
    await this.initSelectedLocales(selectedLocales);
 | 
						|
    await this.initAvailableLocales(available, search);
 | 
						|
 | 
						|
    this.initialized = true;
 | 
						|
  },
 | 
						|
 | 
						|
  async initSelectedLocales(selectedLocales) {
 | 
						|
    this._selectedLocales = new OrderedListBox({
 | 
						|
      richlistbox: document.getElementById("selectedLocales"),
 | 
						|
      upButton: document.getElementById("up"),
 | 
						|
      downButton: document.getElementById("down"),
 | 
						|
      removeButton: document.getElementById("remove"),
 | 
						|
      onRemove: item => this.selectedLocaleRemoved(item),
 | 
						|
      onReorder: () => this.recordTelemetry("reorder"),
 | 
						|
    });
 | 
						|
    this._selectedLocales.setItems(await getLocaleDisplayInfo(selectedLocales));
 | 
						|
  },
 | 
						|
 | 
						|
  async initAvailableLocales(available, search) {
 | 
						|
    this._availableLocales = new SortedItemSelectList({
 | 
						|
      menulist: document.getElementById("availableLocales"),
 | 
						|
      button: document.getElementById("add"),
 | 
						|
      compareFn: compareItems,
 | 
						|
      onSelect: item => this.availableLanguageSelected(item),
 | 
						|
      onChange: item => {
 | 
						|
        this.hideError();
 | 
						|
        if (item.value == "search") {
 | 
						|
          // Record the search event here so we don't track the search from
 | 
						|
          // the main preferences pane twice.
 | 
						|
          this.recordTelemetry("search");
 | 
						|
          this.loadLocalesFromAMO();
 | 
						|
        }
 | 
						|
      },
 | 
						|
    });
 | 
						|
 | 
						|
    // Populate the list with the installed locales even if the user is
 | 
						|
    // searching in case the download fails.
 | 
						|
    await this.loadLocalesFromInstalled(available);
 | 
						|
 | 
						|
    // If the user opened this from the "Search for more languages" option,
 | 
						|
    // search AMO for available locales.
 | 
						|
    if (search) {
 | 
						|
      return this.loadLocalesFromAMO();
 | 
						|
    }
 | 
						|
 | 
						|
    return undefined;
 | 
						|
  },
 | 
						|
 | 
						|
  async loadLocalesFromAMO() {
 | 
						|
    if (!this.downloadEnabled) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Disable the dropdown while we hit the network.
 | 
						|
    this._availableLocales.disableWithMessageId("browser-languages-searching");
 | 
						|
 | 
						|
    // Fetch the available langpacks from AMO.
 | 
						|
    let availableLangpacks;
 | 
						|
    try {
 | 
						|
      availableLangpacks = await AddonRepository.getAvailableLangpacks();
 | 
						|
    } catch (e) {
 | 
						|
      this.showError();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Store the available langpack info for later use.
 | 
						|
    this.availableLangpacks = new Map();
 | 
						|
    for (let { target_locale, url, hash } of availableLangpacks) {
 | 
						|
      this.availableLangpacks.set(target_locale, { url, hash });
 | 
						|
    }
 | 
						|
 | 
						|
    // Remove the installed locales from the available ones.
 | 
						|
    let installedLocales = new Set(await getAvailableLocales());
 | 
						|
    let notInstalledLocales = availableLangpacks
 | 
						|
      .filter(({ target_locale }) => !installedLocales.has(target_locale))
 | 
						|
      .map(lang => lang.target_locale);
 | 
						|
 | 
						|
    // Create the rows for the remote locales.
 | 
						|
    let availableItems = await getLocaleDisplayInfo(notInstalledLocales);
 | 
						|
    availableItems.push({
 | 
						|
      label: await document.l10n.formatValue(
 | 
						|
        "browser-languages-available-label"
 | 
						|
      ),
 | 
						|
      className: "label-item",
 | 
						|
      disabled: true,
 | 
						|
      installed: false,
 | 
						|
    });
 | 
						|
 | 
						|
    // Remove the search option and add the remote locales.
 | 
						|
    let items = this._availableLocales.items;
 | 
						|
    items.pop();
 | 
						|
    items = items.concat(availableItems);
 | 
						|
 | 
						|
    // Update the dropdown and enable it again.
 | 
						|
    this._availableLocales.setItems(items);
 | 
						|
    this._availableLocales.enableWithMessageId(
 | 
						|
      "browser-languages-select-language"
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async loadLocalesFromInstalled(available) {
 | 
						|
    let items;
 | 
						|
    if (available.length) {
 | 
						|
      items = await getLocaleDisplayInfo(available);
 | 
						|
      items.push(await this.createInstalledLabel());
 | 
						|
    } else {
 | 
						|
      items = [];
 | 
						|
    }
 | 
						|
    if (this.downloadEnabled) {
 | 
						|
      items.push({
 | 
						|
        label: await document.l10n.formatValue("browser-languages-search"),
 | 
						|
        value: "search",
 | 
						|
      });
 | 
						|
    }
 | 
						|
    this._availableLocales.setItems(items);
 | 
						|
  },
 | 
						|
 | 
						|
  async availableLanguageSelected(item) {
 | 
						|
    if ((await getAvailableLocales()).includes(item.value)) {
 | 
						|
      this.recordTelemetry("add");
 | 
						|
      await this.requestLocalLanguage(item);
 | 
						|
    } else if (this.availableLangpacks.has(item.value)) {
 | 
						|
      // Telemetry is tracked in requestRemoteLanguage.
 | 
						|
      await this.requestRemoteLanguage(item);
 | 
						|
    } else {
 | 
						|
      this.showError();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  async requestLocalLanguage(item, available) {
 | 
						|
    this._selectedLocales.addItem(item);
 | 
						|
    let selectedCount = this._selectedLocales.items.length;
 | 
						|
    let availableCount = (await getAvailableLocales()).length;
 | 
						|
    if (selectedCount == availableCount) {
 | 
						|
      // Remove the installed label, they're all installed.
 | 
						|
      this._availableLocales.items.shift();
 | 
						|
      this._availableLocales.setItems(this._availableLocales.items);
 | 
						|
    }
 | 
						|
    // The label isn't always reset when the selected item is removed, so set it again.
 | 
						|
    this._availableLocales.enableWithMessageId(
 | 
						|
      "browser-languages-select-language"
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async requestRemoteLanguage(item) {
 | 
						|
    this._availableLocales.disableWithMessageId(
 | 
						|
      "browser-languages-downloading"
 | 
						|
    );
 | 
						|
 | 
						|
    let { url, hash } = this.availableLangpacks.get(item.value);
 | 
						|
    let addon;
 | 
						|
 | 
						|
    try {
 | 
						|
      addon = await installFromUrl(url, hash, installId =>
 | 
						|
        this.recordTelemetry("add", { installId })
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      this.showError();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If the add-on was previously installed, it might be disabled still.
 | 
						|
    if (addon.userDisabled) {
 | 
						|
      await addon.enable();
 | 
						|
    }
 | 
						|
 | 
						|
    item.installed = true;
 | 
						|
    this._selectedLocales.addItem(item);
 | 
						|
    this._availableLocales.enableWithMessageId(
 | 
						|
      "browser-languages-select-language"
 | 
						|
    );
 | 
						|
 | 
						|
    // This is an async task that will install the recommended dictionaries for
 | 
						|
    // this locale. This will fail silently at least until a management UI is
 | 
						|
    // added in bug 1493705.
 | 
						|
    this.installDictionariesForLanguage(item.value);
 | 
						|
  },
 | 
						|
 | 
						|
  async installDictionariesForLanguage(locale) {
 | 
						|
    try {
 | 
						|
      let ids = await dictionaryIdsForLocale(locale);
 | 
						|
      let addonInfos = await AddonRepository.getAddonsByIDs(ids);
 | 
						|
      await Promise.all(
 | 
						|
        addonInfos.map(info => installFromUrl(info.sourceURI.spec))
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      Cu.reportError(e);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  showError() {
 | 
						|
    document.getElementById("warning-message").hidden = false;
 | 
						|
    this._availableLocales.enableWithMessageId(
 | 
						|
      "browser-languages-select-language"
 | 
						|
    );
 | 
						|
 | 
						|
    // The height has likely changed, find our SubDialog and tell it to resize.
 | 
						|
    requestAnimationFrame(() => {
 | 
						|
      let dialogs = window.opener.gSubDialog._dialogs;
 | 
						|
      let index = dialogs.findIndex(d => d._frame.contentDocument == document);
 | 
						|
      if (index != -1) {
 | 
						|
        dialogs[index].resizeDialog();
 | 
						|
      }
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  hideError() {
 | 
						|
    document.getElementById("warning-message").hidden = true;
 | 
						|
  },
 | 
						|
 | 
						|
  getSelectedLocales() {
 | 
						|
    return this._selectedLocales.items.map(item => item.value);
 | 
						|
  },
 | 
						|
 | 
						|
  async selectedLocaleRemoved(item) {
 | 
						|
    this.recordTelemetry("remove");
 | 
						|
 | 
						|
    this._availableLocales.addItem(item);
 | 
						|
 | 
						|
    // If the item we added is at the top of the list, it needs the label.
 | 
						|
    if (this._availableLocales.items[0] == item) {
 | 
						|
      this._availableLocales.addItem(await this.createInstalledLabel());
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  async createInstalledLabel() {
 | 
						|
    return {
 | 
						|
      label: await document.l10n.formatValue(
 | 
						|
        "browser-languages-installed-label"
 | 
						|
      ),
 | 
						|
      className: "label-item",
 | 
						|
      disabled: true,
 | 
						|
      installed: true,
 | 
						|
    };
 | 
						|
  },
 | 
						|
};
 |