Bug 1509977 - Port bugs 1469696, 1505594, 1493711, 1505594, 1509080 and 1488442. r=jorgk
authorRichard Marti <richard.marti@gmail.com>
Mon, 26 Nov 2018 17:50:51 +0100
changeset 33824 23e639499671242b915d89b4cc4144410365e699
parent 33823 9a6732b236dae53ed8b7cf4da8851755c7498ba8
child 33825 fa17a20f549165e5218def02eea6978e03a5208c
push id388
push userclokep@gmail.com
push dateMon, 28 Jan 2019 20:54:56 +0000
reviewersjorgk
bugs1509977, 1469696, 1505594, 1493711, 1509080, 1488442
Bug 1509977 - Port bugs 1469696, 1505594, 1493711, 1505594, 1509080 and 1488442. r=jorgk Bug 1469696: Support installing official language packs from AMO Bug 1505594: Removing a requested locale puts it ahead of its label in the available dropdown Bug 1493711: Pref off downloading langpacks outside of release Bug 1505594: Put removed browser locale in the right order Bug 1509080: Clear language change confirmation content on hide Bug 1488442: Support disabled language packs in multilingual UI
mail/app/profile/all-thunderbird.js
mail/components/preferences/aboutPreferences.xul
mail/components/preferences/advanced.inc.xul
mail/components/preferences/advanced.js
mail/components/preferences/messengerLanguages.js
mail/components/preferences/messengerLanguages.xul
mail/locales/en-US/messenger/preferences/languages.ftl
mail/themes/shared/mail/incontentprefs/aboutPreferences.css
--- a/mail/app/profile/all-thunderbird.js
+++ b/mail/app/profile/all-thunderbird.js
@@ -799,8 +799,11 @@ pref("security.sandbox.content.mac.early
 // Enable FIDO U2F
 pref("security.webauth.u2f", true);
 
 // Use OS date and time settings by default.
 pref("intl.regional_prefs.use_os_locales", true);
 
 // Multi-lingual preferences
 pref("intl.multilingual.enabled", false);
+
+// We don't support yet language pack download from ATN
+pref("intl.multilingual.downloadEnabled", false);
--- a/mail/components/preferences/aboutPreferences.xul
+++ b/mail/components/preferences/aboutPreferences.xul
@@ -34,16 +34,17 @@
 
   <stringbundle id="bundleBrand" src="chrome://branding/locale/brand.properties"/>
   <stringbundle id="bundlePreferences"
                 src="chrome://messenger/locale/preferences/preferences.properties"/>
   <linkset>
     <link rel="localization" href="branding/brand.ftl"/>
     <link rel="localization" href="messenger/preferences/preferences.ftl"/>
     <link rel="localization" href="messenger/preferences/fonts.ftl"/>
+    <link rel="localization" href="messenger/preferences/languages.ftl"/>
   </linkset>
 
   <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript" src="chrome://communicator/content/contentAreaClick.js"/>
   <script type="application/javascript" src="chrome://messenger/content/preferences/preferences.js"/>
   <script type="application/javascript" src="chrome://messenger/content/preferences/subdialogs.js"/>
 
 #include general.inc.xul
--- a/mail/components/preferences/advanced.inc.xul
+++ b/mail/components/preferences/advanced.inc.xul
@@ -189,17 +189,17 @@
               <hbox>
                 <menulist id="defaultMessengerLanguage"
                           oncommand="gAdvancedPane.onMessengerLanguageChange(event)">
                   <menupopup/>
                 </menulist>
                 <button id="manageMessengerLanguagesButton"
                         class="accessory-button"
                         data-l10n-id="manage-messenger-languages-button"
-                        oncommand="gAdvancedPane.showMessengerLanguages()"/>
+                        oncommand="gAdvancedPane.showMessengerLanguages({search: false})"/>
               </hbox>
             </vbox>
             <hbox id="confirmMessengerLanguage"
                   class="message-bar"
                   align="center"
                   hidden="true">
               <image class="message-bar-icon"/>
               <vbox class="message-bar-content-container" align="stretch" flex="1"/>
--- a/mail/components/preferences/advanced.js
+++ b/mail/components/preferences/advanced.js
@@ -543,53 +543,81 @@ var gAdvancedPane = {
     }
     return new Localization([
       "messenger/preferences/preferences.ftl",
       "branding/brand.ftl",
     ], generateBundles);
   },
 
   initMessengerLocale() {
-    let localeCodes = Services.locale.availableLocales;
-    let localeNames = Services.intl.getLocaleDisplayNames(undefined, localeCodes);
-    let locales = localeCodes.map((code, i) => ({code, name: localeNames[i]}));
+    gAdvancedPane.setMessengerLocales(Services.locale.requestedLocale);
+  },
+
+  /**
+   * Update the available list of locales and select the locale that the user
+   * is "selecting". This could be the currently requested locale or a locale
+   * that the user would like to switch to after confirmation.
+   */
+  async setMessengerLocales(selected) {
+    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.createElement("menuitem");
       menuitem.setAttribute("value", code);
       menuitem.setAttribute("label", name);
       fragment.appendChild(menuitem);
     }
+
+    // Add an option to search for more languages if downloading is supported.
+    if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) {
+      let menuitem = document.createXULElement("menuitem");
+      menuitem.id = "defaultBrowserLanguageSearch";
+      menuitem.setAttribute(
+        "label", await document.l10n.formatValue("messenger-languages-search"));
+      menuitem.setAttribute("value", "search");
+      menuitem.addEventListener("command", () => {
+        gMainPane.showBrowserLanguages({search: true});
+      });
+      fragment.appendChild(menuitem);
+    }
+
     let menulist = document.getElementById("defaultMessengerLanguage");
     let menupopup = menulist.querySelector("menupopup");
+    menupopup.textContent = "";
     menupopup.appendChild(fragment);
-    menulist.value = Services.locale.requestedLocale;
+    menulist.value = selected;
 
     document.getElementById("messengerLanguagesBox").hidden = false;
   },
 
-  showMessengerLanguages() {
+  showMessengerLanguages({search}) {
+    let opts = {selected: gAdvancedPane.selectedLocales, search};
     gSubDialog.open(
       "chrome://messenger/content/preferences/messengerLanguages.xul",
-      null, this.requestingLocales, this.messengerLanguagesClosed);
+      null, opts, this.messengerLanguagesClosed);
   },
 
   /* Show or hide the confirm change message bar based on the updated ordering. */
   messengerLanguagesClosed() {
-    let requesting = this.gMessengerLanguagesDialog.requestedLocales;
-    let requested = Services.locale.requestedLocales;
-    let defaultMessengerLanguage = document.getElementById("defaultMessengerLanguage");
-    if (requesting && requesting.join(",") != requested.join(",")) {
-      gAdvancedPane.showConfirmLanguageChangeMessageBar(requesting);
-      defaultMessengerLanguage.value = requesting[0];
+    let selected = this.gMessengerLanguagesDialog.selected;
+    let active = Services.locale.appLocalesAsBCP47;
+
+    // Prepare for changing the locales if they are different than the current locales.
+    if (selected && selected.join(",") != active.join(",")) {
+      gAdvancedPane.showConfirmLanguageChangeMessageBar(selected);
+      gAdvancedPane.setMessengerLocales(selected[0]);
       return;
     }
-    defaultMessengerLanguage.value = Services.locale.requestedLocale;
+
+    // They matched, so we can reset the UI.
+    gAdvancedPane.setMessengerLocales(Services.locale.appLocaleAsBCP47);
     gAdvancedPane.hideConfirmLanguageChangeMessageBar();
   },
 
   /* Show the confirmation message bar to allow a restart into the new locales. */
   async showConfirmLanguageChangeMessageBar(locales) {
     let messageBar = document.getElementById("confirmMessengerLanguage");
 
     // Get the bundle for the new locale.
@@ -628,23 +656,24 @@ var gAdvancedPane = {
       button.setAttribute("locales", locales.join(","));
       button.setAttribute("label", buttonLabels[i]);
       messageContainer.appendChild(button);
 
       contentContainer.appendChild(messageContainer);
     }
 
     messageBar.hidden = false;
-    this.requestingLocales = locales;
+    this.selectedLocales = locales;
   },
 
   hideConfirmLanguageChangeMessageBar() {
     let messageBar = document.getElementById("confirmMessengerLanguage");
     messageBar.hidden = true;
-    messageBar.querySelector(".message-bar-button").removeAttribute("locales");
+    let contentContainer = messageBar.querySelector(".message-bar-content-container");
+    contentContainer.textContent = "";
     this.requestingLocales = null;
   },
 
   /* Confirm the locale change and restart the Thunderbird in the new locale. */
   confirmLanguageChange(event) {
     let localesString = (event.target.getAttribute("locales") || "").trim();
     if (!localesString || localesString.length == 0) {
       return;
@@ -658,20 +687,24 @@ var gAdvancedPane = {
     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. */
   onMessengerLanguageChange(event) {
     let locale = event.target.value;
-    if (locale == Services.locale.requestedLocale) {
+
+    if (locale == "search") {
+      return;
+    } else if (locale == Services.locale.appLocaleAsBCP47) {
       this.hideConfirmLanguageChangeMessageBar();
       return;
     }
+
     let locales = Array.from(new Set([
       locale,
       ...Services.locale.requestedLocales,
     ]).values());
     this.showConfirmLanguageChangeMessageBar(locales);
   },
 
   destroy() {
--- a/mail/components/preferences/messengerLanguages.js
+++ b/mail/components/preferences/messengerLanguages.js
@@ -1,14 +1,31 @@
 /* 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/. */
 
 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");
+
+/* 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.
+ */
+
 class OrderedListBox {
   constructor({richlistbox, upButton, downButton, removeButton, onRemove}) {
     this.richlistbox = richlistbox;
     this.upButton = upButton;
     this.downButton = downButton;
     this.removeButton = removeButton;
     this.onRemove = onRemove;
 
@@ -123,131 +140,353 @@ class OrderedListBox {
     labelEl.textContent = label;
     listitem.appendChild(labelEl);
 
     return listitem;
   }
 }
 
 class SortedItemSelectList {
-  constructor({menulist, button, onSelect}) {
+  constructor({menulist, button, onSelect, onChange, compareFn}) {
     this.menulist = menulist;
     this.popup = menulist.firstElementChild;
+    this.button = button;
+    this.compareFn = compareFn;
     this.items = [];
 
     menulist.addEventListener("command", () => {
       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((a, b) => a.label > b.label);
+    this.items = items.sort(this.compareFn);
     this.populate();
   }
 
   populate() {
-    let {items, menulist, popup} = this;
+    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 {items, menulist, popup} = this;
-    let i;
+    let {compareFn, items, menulist, popup} = this;
 
     // Find the index of the item to insert before.
-    for (i = 0; i < items.length && items[i].label < item.label; i++)
-      ;
-
+    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}) {
+  createItem({label, value, className, disabled}) {
     let item = document.createElement("menuitem");
     item.value = value;
     item.setAttribute("label", label);
+    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://global/skin/icons/loading.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 availableLocales = new Set(Services.locale.availableLocales);
   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 gMessengerLanguagesDialog = {
   _availableLocales: null,
-  _requestedLocales: null,
-  requestedLocales: null,
+  _selectedLocales: null,
+  selectedLocales: null,
+
+  get downloadEnabled() {
+    // Downloading langpacks isn't always supported, check the pref.
+    return Services.prefs.getBoolPref("intl.multilingual.downloadEnabled");
+  },
 
   beforeAccept() {
-    this.requestedLocales = this._requestedLocales.items.map(item => item.value);
+    this.selected = this.getSelectedLocales();
     return true;
   },
 
-  onLoad() {
-    // Maintain the previously requested locales even if we cancel out.
-    this.requestedLocales = window.arguments[0];
+  async onLoad() {
+    // Maintain the previously selected locales even if we cancel out.
+    let {selected, search} = window.arguments[0] || {};
+    this.selectedLocales = selected;
 
-    let requested = this.requestedLocales || Services.locale.requestedLocales;
-    let requestedSet = new Set(requested);
-    let available = Services.locale.availableLocales
-      .filter(locale => !requestedSet.has(locale));
+    // 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 = Services.locale.availableLocales;
+    let availableSet = new Set(available);
 
-    this.initRequestedLocales(requested);
-    this.initAvailableLocales(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));
+
+    this.initSelectedLocales(selectedLocales);
+    await this.initAvailableLocales(available, search);
+
     this.initialized = true;
   },
 
-  initRequestedLocales(requested) {
-    this._requestedLocales = new OrderedListBox({
-      richlistbox: document.getElementById("requestedLocales"),
+  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._availableLocales.addItem(item),
+      onRemove: (item) => this.selectedLocaleRemoved(item),
     });
-    this._requestedLocales.setItems(getLocaleDisplayInfo(requested));
+    this._selectedLocales.setItems(getLocaleDisplayInfo(selectedLocales));
   },
 
-  initAvailableLocales(available) {
+  async initAvailableLocales(available, search) {
     this._availableLocales = new SortedItemSelectList({
       menulist: document.getElementById("availableLocales"),
       button: document.getElementById("add"),
-      onSelect: (item) => this._requestedLocales.addItem(item),
+      compareFn: compareItems,
+      onSelect: (item) => this.availableLanguageSelected(item),
+      onChange: (item) => {
+        this.hideError();
+        if (item.value == "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.loadLocalesFromATN();
+    }
+
+    return undefined;
+  },
+
+  async loadLocalesFromATN() {
+    if (!this.downloadEnabled) {
+      return;
+    }
+
+    // Disable the dropdown while we hit the network.
+    this._availableLocales.disableWithMessageId("messenger-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(Services.locale.availableLocales);
+    let notInstalledLocales = availableLangpacks
+      .filter(({target_locale}) => !installedLocales.has(target_locale))
+      .map(lang => lang.target_locale);
+
+    // Create the rows for the remote locales.
+    let availableItems = getLocaleDisplayInfo(notInstalledLocales);
+    availableItems.push({
+      label: await document.l10n.formatValue("messenger-languages-available-label"),
+      className: "label-item",
+      disabled: true,
+      installed: false,
     });
-    this._availableLocales.setItems(getLocaleDisplayInfo(available));
+
+    // 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("messenger-languages-select-language");
+  },
+
+  async loadLocalesFromInstalled(available) {
+    let items;
+    if (available.length > 0) {
+      items = getLocaleDisplayInfo(available);
+      items.push(await this.createInstalledLabel());
+    } else {
+      items = [];
+    }
+    if (this.downloadEnabled) {
+      items.push({
+        label: await document.l10n.formatValue("messenger-languages-search"),
+        value: "search",
+      });
+    }
+    this._availableLocales.setItems(items);
+  },
+
+  async availableLanguageSelected(item) {
+    let available = new Set(Services.locale.availableLocales);
+
+    if (available.has(item.value)) {
+      this._selectedLocales.addItem(item);
+      if (available.size == this._selectedLocales.items.length) {
+        // Remove the installed label, they're all installed.
+        this._availableLocales.items.shift();
+        this._availableLocales.setItems(this._availableLocales.items);
+      }
+    } else if (this.availableLangpacks.has(item.value)) {
+      this._availableLocales.disableWithMessageId("messenger-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._selectedLocales.addItem(item);
+      this._availableLocales.enableWithMessageId("messenger-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("messenger-languages-select-language");
+  },
+
+  hideError() {
+    document.querySelectorAll(".warning-message-separator")
+      .forEach(separator => separator.classList.remove("thin"));
+    document.getElementById("warning-message").hidden = true;
+  },
+
+  getSelectedLocales() {
+    return this._selectedLocales.items.map(item => item.value);
+  },
+
+  async selectedLocaleRemoved(item) {
+    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("messenger-languages-installed-label"),
+      className: "label-item",
+      disabled: true,
+      installed: true,
+    };
   },
 };
--- a/mail/components/preferences/messengerLanguages.xul
+++ b/mail/components/preferences/messengerLanguages.xul
@@ -30,32 +30,37 @@
 
     <grid flex="1">
       <columns>
         <column flex="1"/>
         <column/>
       </columns>
       <rows>
         <row flex="1">
-            <richlistbox id="requestedLocales" flex="1"/>
+            <richlistbox id="selectedLocales" flex="1"/>
           <vbox>
-            <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"/>
+            <button id="up" class="action-button" disabled="true" data-l10n-id="languages-customize-moveup"/>
+            <button id="down" class="action-button" disabled="true" data-l10n-id="languages-customize-movedown"/>
+            <button id="remove" class="action-button" 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="messenger-languages-select-language"
+                    data-l10n-attrs="placeholder,label">
             <menupopup/>
           </menulist>
           <button id="add"
-                  class="add-browser-language"
+                  class="add-messenger-language action-button"
                   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"/>
+      <description class="message-bar-description" data-l10n-id="messenger-languages-error"/>
+    </hbox>
+    <separator class="warning-message-separator"/>
   </vbox>
 </dialog>
--- a/mail/locales/en-US/messenger/preferences/languages.ftl
+++ b/mail/locales/en-US/messenger/preferences/languages.ftl
@@ -21,8 +21,25 @@ languages-customize-add =
     .label = Add
     .accesskey = A
 
 messenger-languages-window =
     .title = { -brand-short-name } Language Settings
     .style = width: 40em
 
 messenger-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.
+
+messenger-languages-search = Search for more languages…
+
+messenger-languages-searching =
+    .label = Searching for languages…
+
+messenger-languages-downloading =
+    .label = Downloading…
+
+messenger-languages-select-language =
+    .label = Select a language to add…
+    .placeholder = Select a language to add…
+
+messenger-languages-installed-label = Installed languages
+messenger-languages-available-label = Available languages
+
+messenger-languages-error = { -brand-short-name } can't update your languages right now. Check that you are connected to the internet or try again.
--- a/mail/themes/shared/mail/incontentprefs/aboutPreferences.css
+++ b/mail/themes/shared/mail/incontentprefs/aboutPreferences.css
@@ -415,23 +415,41 @@ richlistbox:focus > richlistitem[selecte
   min-height: 360px;
 }
 
 #defaultMessengerLanguage {
   margin-inline-start: 0;
   min-width: 20em;
 }
 
+#MessengerLanguagesDialog > .dialog-button-box > .dialog-button[dlgtype="accept"] {
+  margin-inline-end: 0;
+}
+
 #availableLocales {
   margin: 0;
   margin-inline-end: 4px;
 }
 
-.add-browser-language {
-  margin: 0 4px;
+#warning-message > .message-bar-description {
+  width: 32em;
+}
+
+.add-messenger-language {
+  margin: 0;
+  margin-inline-start: 4px;
+}
+
+.action-button {
+  margin-inline-end: 0;
+}
+
+/* Menulist styles */
+.label-item {
+  font-size: .8em;
 }
 
 /**
  * Dialog
  */
 
 .dialogOverlay {
   visibility: hidden;