Bug 1469696 - Part 2: Allow installing langpacks from AMO in prefs r=flod,aswan,zbraniecki,jaws
authorMark Striemer <mstriemer@mozilla.com>
Wed, 03 Oct 2018 14:07:14 +0000
changeset 487766 31c94d7845b709c7a6d8ff8cf8e35b9b918091fd
parent 487765 263bd17c558edae18391d2cda36d476a7cbd97f5
child 487767 10b8ab80417dafbf54c53b7916b3493e015d2bc2
push id246
push userfmarier@mozilla.com
push dateSat, 13 Oct 2018 00:15:40 +0000
reviewersflod, aswan, zbraniecki, jaws
bugs1469696
milestone64.0a1
Bug 1469696 - Part 2: Allow installing langpacks from AMO in prefs r=flod,aswan,zbraniecki,jaws Differential Revision: https://phabricator.services.mozilla.com/D6312
browser/components/preferences/browserLanguages.js
browser/components/preferences/browserLanguages.xul
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/main.xul
browser/locales/en-US/browser/preferences/languages.ftl
browser/themes/shared/warning.svg
toolkit/themes/shared/in-content/common.inc.css
--- a/browser/components/preferences/browserLanguages.js
+++ b/browser/components/preferences/browserLanguages.js
@@ -1,16 +1,21 @@
 /* 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 */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+ChromeUtils.defineModuleGetter(this, "AddonManager",
+                               "resource://gre/modules/AddonManager.jsm");
+ChromeUtils.defineModuleGetter(this, "AddonRepository",
+                               "resource://gre/modules/addons/AddonRepository.jsm");
+
 class OrderedListBox {
   constructor({richlistbox, upButton, downButton, removeButton, onRemove}) {
     this.richlistbox = richlistbox;
     this.upButton = upButton;
     this.downButton = downButton;
     this.removeButton = removeButton;
     this.onRemove = onRemove;
 
@@ -128,16 +133,17 @@ class OrderedListBox {
     return listitem;
   }
 }
 
 class SortedItemSelectList {
   constructor({menulist, button, onSelect}) {
     this.menulist = menulist;
     this.popup = menulist.firstElementChild;
+    this.button = button;
     this.items = [];
 
     menulist.addEventListener("command", () => {
       button.disabled = !menulist.selectedItem;
     });
     button.addEventListener("command", () => {
       if (!menulist.selectedItem) return;
 
@@ -189,16 +195,38 @@ class SortedItemSelectList {
   }
 
   createItem({label, value}) {
     let item = document.createElement("menuitem");
     item.value = value;
     item.setAttribute("label", label);
     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;
+  }
 }
 
 function getLocaleDisplayInfo(localeCodes) {
   let packagedLocales = new Set(Services.locale.packagedLocales);
   let localeNames = Services.intl.getLocaleDisplayNames(undefined, localeCodes);
   return localeCodes.map((code, i) => {
     return {
       id: "locale-" + code,
@@ -214,42 +242,144 @@ var gBrowserLanguagesDialog = {
   _requestedLocales: null,
   requestedLocales: null,
 
   beforeAccept() {
     this.requestedLocales = this._requestedLocales.items.map(item => item.value);
     return true;
   },
 
-  onLoad() {
+  async onLoad() {
     // Maintain the previously requested locales even if we cancel out.
-    this.requestedLocales = window.arguments[0];
+    let {requesting, search} = window.arguments[0] || {};
+    this.requestedLocales = requesting;
 
     let requested = this.requestedLocales || Services.locale.requestedLocales;
     let requestedSet = new Set(requested);
     let available = Services.locale.availableLocales
       .filter(locale => !requestedSet.has(locale));
 
     this.initRequestedLocales(requested);
-    this.initAvailableLocales(available);
+    await this.initAvailableLocales(available, search);
     this.initialized = true;
   },
 
   initRequestedLocales(requested) {
     this._requestedLocales = new OrderedListBox({
       richlistbox: document.getElementById("requestedLocales"),
       upButton: document.getElementById("up"),
       downButton: document.getElementById("down"),
       removeButton: document.getElementById("remove"),
       onRemove: (item) => this._availableLocales.addItem(item),
     });
     this._requestedLocales.setItems(getLocaleDisplayInfo(requested));
   },
 
-  initAvailableLocales(available) {
+  async initAvailableLocales(available, search) {
     this._availableLocales = new SortedItemSelectList({
       menulist: document.getElementById("availableLocales"),
       button: document.getElementById("add"),
-      onSelect: (item) => this._requestedLocales.addItem(item),
+      onSelect: (item) => this.availableLanguageSelected(item),
     });
-    this._availableLocales.setItems(getLocaleDisplayInfo(available));
+
+    // 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() {
+    // 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});
+    }
+
+    // Create a list of installed locales to hide.
+    let installedLocales = new Set([
+      ...Services.locale.requestedLocales,
+      ...Services.locale.availableLocales,
+    ]);
+
+    let availableLocales = availableLangpacks
+      .filter(({target_locale}) => !installedLocales.has(target_locale))
+      .map(lang => lang.target_locale);
+    let availableItems = getLocaleDisplayInfo(availableLocales);
+    let items = this._availableLocales.items;
+    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 > 0) {
+      items = getLocaleDisplayInfo(available);
+    } else {
+      items = [];
+    }
+    this._availableLocales.setItems(items);
+  },
+
+  async availableLanguageSelected(item) {
+    let available = new Set(Services.locale.availableLocales);
+
+    if (available.has(item.value)) {
+      this._requestedLocales.addItem(item);
+      if (available.size == this._requestedLocales.items.length) {
+        this._availableLocales.setItems(this._availableLocales.items);
+      }
+    } else if (this.availableLangpacks.has(item.value)) {
+      this._availableLocales.disableWithMessageId("browser-languages-downloading");
+
+      let {url, hash} = this.availableLangpacks.get(item.value);
+      let install = await AddonManager.getInstallForURL(
+        url, "application/x-xpinstall", hash);
+
+      try {
+        await install.install();
+      } catch (e) {
+        this.showError();
+        return;
+      }
+
+      item.installed = true;
+      this._requestedLocales.addItem(item);
+      this._availableLocales.enableWithMessageId("browser-languages-select-language");
+    } else {
+      this.showError();
+    }
+  },
+
+  showError() {
+    document.querySelectorAll(".warning-message-separator")
+      .forEach(separator => separator.classList.add("thin"));
+    document.getElementById("warning-message").hidden = false;
+    this._availableLocales.enableWithMessageId("browser-languages-select-language");
+  },
+
+  hideError() {
+    document.querySelectorAll(".warning-message-separator")
+      .forEach(separator => separator.classList.remove("thin"));
+    document.getElementById("warning-message").hidden = true;
   },
 };
--- a/browser/components/preferences/browserLanguages.xul
+++ b/browser/components/preferences/browserLanguages.xul
@@ -43,22 +43,29 @@
             <button id="up" disabled="true" data-l10n-id="languages-customize-moveup"/>
             <button id="down" disabled="true" data-l10n-id="languages-customize-movedown"/>
             <button id="remove" disabled="true" data-l10n-id="languages-customize-remove"/>
           </vbox>
         </row>
         <row>
           <menulist id="availableLocales"
                     class="available-locales-list"
-                    data-l10n-id="languages-customize-select-language"
-                    data-l10n-attrs="placeholder">
+                    data-l10n-id="browser-languages-select-language"
+                    data-l10n-attrs="placeholder,label">
             <menupopup/>
           </menulist>
           <button id="add"
                   class="add-browser-language"
                   data-l10n-id="languages-customize-add"
                   disabled="true"/>
         </row>
       </rows>
     </grid>
-    <separator/>
+    <separator class="warning-message-separator"/>
+    <hbox id="warning-message" class="message-bar message-bar-warning" hidden="true">
+      <image class="message-bar-icon"/>
+      <hbox align="center" flex="1">
+        <description class="message-bar-description" data-l10n-id="browser-languages-error"/>
+      </hbox>
+    </hbox>
+    <separator class="warning-message-separator"/>
   </vbox>
 </dialog>
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -780,32 +780,52 @@ var gMainPane = {
       newValue = startupPref.value === this.STARTUP_PREF_RESTORE_SESSION;
     }
     if (checkbox.checked !== newValue) {
       checkbox.checked = newValue;
     }
   },
 
   initBrowserLocale() {
-    let localeCodes = Services.locale.availableLocales;
-    let localeNames = Services.intl.getLocaleDisplayNames(undefined, localeCodes);
-    let locales = localeCodes.map((code, i) => ({code, name: localeNames[i]}));
+    gMainPane.setBrowserLocales(Services.locale.requestedLocale);
+  },
+
+  /**
+   * Update the available list of locales and select the locale that the user
+   * is "requesting". This could be the currently requested locale or a locale
+   * that the user would like to switch to after confirmation.
+   */
+  async setBrowserLocales(requesting) {
+    let available = Services.locale.availableLocales;
+    let localeNames = Services.intl.getLocaleDisplayNames(undefined, available);
+    let locales = available.map((code, i) => ({code, name: localeNames[i]}));
     locales.sort((a, b) => a.name > b.name);
 
     let fragment = document.createDocumentFragment();
     for (let {code, name} of locales) {
       let menuitem = document.createXULElement("menuitem");
       menuitem.setAttribute("value", code);
       menuitem.setAttribute("label", name);
       fragment.appendChild(menuitem);
     }
+
+    // Add an option to search for more languages.
+    let menuitem = document.createXULElement("menuitem");
+    menuitem.setAttribute(
+      "label", await document.l10n.formatValue("browser-languages-search"));
+    menuitem.addEventListener("command", () => {
+      gMainPane.showBrowserLanguages({search: true});
+    });
+    fragment.appendChild(menuitem);
+
     let menulist = document.getElementById("defaultBrowserLanguage");
     let menupopup = menulist.querySelector("menupopup");
+    menupopup.textContent = "";
     menupopup.appendChild(fragment);
-    menulist.value = Services.locale.requestedLocale;
+    menulist.value = requesting;
 
     document.getElementById("browserLanguagesBox").hidden = false;
   },
 
   /* Show the confirmation message bar to allow a restart into the new locales. */
   async showConfirmLanguageChangeMessageBar(locales) {
     let messageBar = document.getElementById("confirmBrowserLanguage");
     // Set the text in the message bar for the new locale.
@@ -844,20 +864,26 @@ var gMainPane = {
     if (!cancelQuit.data) {
       Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
     }
   },
 
   /* Show or hide the confirm change message bar based on the new locale. */
   onBrowserLanguageChange(event) {
     let locale = event.target.value;
-    if (locale == Services.locale.requestedLocale) {
+
+    // If there is no value, then this is the search option, leave the
+    // message bar in its current state.
+    if (!locale) {
+      return;
+    } else if (locale == Services.locale.requestedLocale) {
       this.hideConfirmLanguageChangeMessageBar();
       return;
     }
+
     let locales = Array.from(new Set([
       locale,
       ...Services.locale.requestedLocales,
     ]).values());
     this.showConfirmLanguageChangeMessageBar(locales);
   },
 
   onBrowserRestoreSessionChange(event) {
@@ -979,33 +1005,33 @@ var gMainPane = {
 
   /**
    * Shows a dialog in which the preferred language for web content may be set.
    */
   showLanguages() {
     gSubDialog.open("chrome://browser/content/preferences/languages.xul");
   },
 
-  showBrowserLanguages() {
+  showBrowserLanguages({search}) {
+    let opts = {requesting: gMainPane.requestingLocales, search};
     gSubDialog.open(
       "chrome://browser/content/preferences/browserLanguages.xul",
-      null, gMainPane.requestingLocales, this.browserLanguagesClosed);
+      null, opts, this.browserLanguagesClosed);
   },
 
   /* Show or hide the confirm change message bar based on the updated ordering. */
   browserLanguagesClosed() {
     let requesting = this.gBrowserLanguagesDialog.requestedLocales;
     let requested = Services.locale.requestedLocales;
-    let defaultBrowserLanguage = document.getElementById("defaultBrowserLanguage");
     if (requesting && requesting.join(",") != requested.join(",")) {
       gMainPane.showConfirmLanguageChangeMessageBar(requesting);
-      defaultBrowserLanguage.value = requesting[0];
+      gMainPane.setBrowserLocales(requesting[0]);
       return;
     }
-    defaultBrowserLanguage.value = Services.locale.requestedLocale;
+    gMainPane.setBrowserLocales(Services.locale.requestedLocale);
     gMainPane.hideConfirmLanguageChangeMessageBar();
   },
 
   /**
    * Displays the translation exceptions dialog where specific site and language
    * translation preferences can be set.
    */
   showTranslationExceptions() {
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -288,17 +288,17 @@
     <description flex="1" controls="chooseBrowserLanguage" data-l10n-id="choose-browser-language-description"/>
     <hbox>
       <menulist id="defaultBrowserLanguage" oncommand="gMainPane.onBrowserLanguageChange(event)">
         <menupopup/>
       </menulist>
       <button id="manageBrowserLanguagesButton"
               class="accessory-button"
               data-l10n-id="manage-browser-languages-button"
-              oncommand="gMainPane.showBrowserLanguages()"/>
+              oncommand="gMainPane.showBrowserLanguages({search: false})"/>
     </hbox>
   </vbox>
   <hbox id="confirmBrowserLanguage" class="message-bar" align="center" hidden="true">
     <image class="message-bar-icon"/>
     <hbox class="message-bar-content" align="center" flex="1">
       <description class="message-bar-description" flex="1"/>
       <button class="message-bar-button" oncommand="gMainPane.confirmBrowserLanguageChange()"/>
     </hbox>
--- a/browser/locales/en-US/browser/preferences/languages.ftl
+++ b/browser/locales/en-US/browser/preferences/languages.ftl
@@ -49,8 +49,23 @@ languages-code-format =
 languages-active-code-format =
     .value = { languages-code-format.label }
 
 browser-languages-window =
     .title = { -brand-short-name } Language Settings
     .style = width: 40em
 
 browser-languages-description = { -brand-short-name } will display the first language as your default and will display alternate languages if necessary in the order they appear.
+
+browser-languages-search = Search for more languages…
+
+browser-languages-searching =
+    .label = Searching for languages…
+
+browser-languages-downloading =
+    .label = Downloading…
+
+browser-languages-select-language =
+    .label = Select a language to add…
+    .placeholder = Select a language to add…
+
+browser-languages-error =
+    .value = An error occurred.
--- a/browser/themes/shared/warning.svg
+++ b/browser/themes/shared/warning.svg
@@ -1,6 +1,6 @@
 <!-- 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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
   <path fill="context-fill #FFBF00" stroke="context-stroke #fff" stroke-opacity="0.3" d="M15.4,12.9 9.46,1.41 C9.12,0.756 8.59,0.381 8,0.381 7.41,0.381 6.88,0.756 6.54,1.41 L0.642,12.9 c-0.331,0.6 -0.348,1.3 -0.05,1.9 0.299,0.5 0.854,0.8 1.534,0.8 H13.9 c0.6,0 1.2,-0.3 1.5,-0.8 0.3,-0.6 0.3,-1.3 0,-1.9z M8.83,5.07 8.65,10.5 H7.34 L7.15,5.07 H8.83z M8,13.7 c-0.55,0 -0.99,-0.5 -0.99,-1 0,-0.6 0.44,-1 0.99,-1 0.56,0 0.99,0.4 0.99,1 0,0.5 -0.43,1 -0.99,1z"/>
 </svg>
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -40,16 +40,18 @@
   --in-content-table-background: #ebebeb;
   --in-content-table-border-dark-color: #d1d1d1;
   --in-content-table-header-background: #0a84ff;
   --grey-20: #ededf0;
   --grey-90: #0c0c0d;
   --grey-90-a10: rgba(12, 12, 13, 0.1);
   --grey-90-a20: rgba(12, 12, 13, 0.2);
   --grey-90-a30: rgba(12, 12, 13, 0.3);
+  --yellow-50: #ffe900;
+  --yellow-90: #3e2800;
 }
 
 html|html,
 xul|page,
 xul|window {
   font: message-box;
   -moz-appearance: none;
   background-color: var(--in-content-page-background);
@@ -822,8 +824,18 @@ xul|treechildren::-moz-tree-image(select
   list-style-image: url("chrome://browser/skin/identity-icon.svg");
   width: 24px;
   height: 24px;
   padding: 4px;
   margin-inline-end: 4px;
   -moz-context-properties: fill;
   fill: currentColor;
 }
+
+/* Warning styles */
+.message-bar-warning {
+  background-color: var(--yellow-50);
+  color: var(--yellow-90);
+}
+
+.message-bar-warning > .message-bar-icon {
+  list-style-image: url("chrome://browser/skin/warning.svg");
+}