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 495131 31c94d7845b709c7a6d8ff8cf8e35b9b918091fd
parent 495130 263bd17c558edae18391d2cda36d476a7cbd97f5
child 495132 10b8ab80417dafbf54c53b7916b3493e015d2bc2
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflod, aswan, zbraniecki, jaws
bugs1469696
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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");
+}