Bug 1431260 - Migrate LocaleService::AvailableLocales from ChromeRegistry to multilocale.txt. r=jfkthame
authorZibi Braniecki <zbraniecki@mozilla.com>
Tue, 13 Feb 2018 23:41:39 -0800
changeset 459730 f0e06a115f0f58c252625684b007912f49c2db0e
parent 459729 6f787e71c76f7dbc62b6e0e4a0dbee2759e5bfe0
child 459731 5dae26ba399f28facf16ea6b74caf026810201f4
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjfkthame
bugs1431260
milestone60.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 1431260 - Migrate LocaleService::AvailableLocales from ChromeRegistry to multilocale.txt. r=jfkthame MozReview-Commit-ID: 6S4VaAvDako
browser/components/nsBrowserGlue.js
chrome/nsChromeRegistryChrome.cpp
intl/l10n/L10nRegistry.jsm
intl/l10n/Localization.jsm
intl/locale/LocaleService.cpp
intl/locale/LocaleService.h
intl/locale/mozILocaleService.idl
intl/locale/tests/gtest/TestLocaleService.cpp
intl/locale/tests/unit/test_localeService.js
intl/locale/tests/unit/test_localeService_negotiateLanguages.js
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -711,25 +711,19 @@ BrowserGlue.prototype = {
       iconURL: "resource:///chrome/browser/content/browser/defaultthemes/dark.icon.svg",
       textcolor: "white",
       accentcolor: "black",
       author: vendorShortName,
     });
 
 
     // Initialize the default l10n resource sources for L10nRegistry.
-    const multilocalePath = "resource://gre/res/multilocale.json";
-    L10nRegistry.bootstrap = fetch(multilocalePath).then(d => d.json()).then(({ locales }) => {
-      const toolkitSource = new FileSource("toolkit", locales, "resource://gre/localization/{locale}/");
-      L10nRegistry.registerSource(toolkitSource);
-      const appSource = new FileSource("app", locales, "resource://app/localization/{locale}/");
-      L10nRegistry.registerSource(appSource);
-    }).catch(e => {
-      Services.console.logStringMessage(`Could not load multilocale.json. Error: ${e}`);
-    });
+    let locales = Services.locale.getPackagedLocales();
+    const appSource = new FileSource("app", locales, "resource://app/localization/{locale}/");
+    L10nRegistry.registerSource(appSource);
 
     Services.obs.notifyObservers(null, "browser-ui-startup-complete");
   },
 
   _checkForOldBuildUpdates() {
     // check for update if our build is old
     if (AppConstants.MOZ_UPDATER &&
         Services.prefs.getBoolPref("app.update.enabled") &&
--- a/chrome/nsChromeRegistryChrome.cpp
+++ b/chrome/nsChromeRegistryChrome.cpp
@@ -730,22 +730,16 @@ nsChromeRegistryChrome::ManifestLocale(M
   // We use mainPackage as the package we track for reporting new locales being
   // registered. For most cases it will be "global", but for Fennec it will be
   // "browser".
   nsAutoCString mainPackage;
   nsresult rv = OverrideLocalePackage(NS_LITERAL_CSTRING("global"), mainPackage);
   if (NS_FAILED(rv)) {
     return;
   }
-
-  if (mainPackage.Equals(package)) {
-    // We should refresh the LocaleService, since the available
-    // locales changed.
-    LocaleService::GetInstance()->AvailableLocalesChanged();
-  }
 }
 
 void
 nsChromeRegistryChrome::ManifestSkin(ManifestProcessingContext& cx, int lineno,
                                      char *const * argv, int flags)
 {
   char* package = argv[0];
   char* provider = argv[1];
--- a/intl/l10n/L10nRegistry.jsm
+++ b/intl/l10n/L10nRegistry.jsm
@@ -104,44 +104,44 @@ const L10nRegistry = {
    *
    * @param {FileSource} source
    */
   registerSource(source) {
     if (this.sources.has(source.name)) {
       throw new Error(`Source with name "${source.name}" already registered.`);
     }
     this.sources.set(source.name, source);
-    Services.obs.notifyObservers(null, 'l10n:available-locales-changed', null);
+    Services.locale.setAvailableLocales(this.getAvailableLocales());
   },
 
   /**
    * Updates an existing source in the L10nRegistry
    *
    * That will usually happen when a new version of a source becomes
    * available (for example, an updated version of a language pack).
    *
    * @param {FileSource} source
    */
   updateSource(source) {
     if (!this.sources.has(source.name)) {
       throw new Error(`Source with name "${source.name}" is not registered.`);
     }
     this.sources.set(source.name, source);
     this.ctxCache.clear();
-    Services.obs.notifyObservers(null, 'l10n:available-locales-changed', null);
+    Services.locale.setAvailableLocales(this.getAvailableLocales());
   },
 
   /**
    * Removes a source from the L10nRegistry.
    *
    * @param {String} sourceId
    */
   removeSource(sourceName) {
     this.sources.delete(sourceName);
-    Services.obs.notifyObservers(null, 'l10n:available-locales-changed', null);
+    Services.locale.setAvailableLocales(this.getAvailableLocales());
   },
 
   /**
    * Returns a list of locales for which at least one source
    * has resources.
    *
    * @returns {Array<String>}
    */
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -116,22 +116,18 @@ class L10nError extends Error {
  *
  * In the future, we may want to allow certain modules to override this
  * with a different negotitation strategy to allow for the module to
  * be localized into a different language - for example DevTools.
  */
 function defaultGenerateMessages(resourceIds) {
   const availableLocales = L10nRegistry.getAvailableLocales();
 
-  const requestedLocales = LocaleService.getRequestedLocales();
-  const defaultLocale = LocaleService.defaultLocale;
-  const locales = LocaleService.negotiateLanguages(
-    requestedLocales, availableLocales, defaultLocale,
-  );
-  return L10nRegistry.generateContexts(locales, resourceIds);
+  const appLocales = LocaleService.getAppLocalesAsLangTags();
+  return L10nRegistry.generateContexts(appLocales, resourceIds);
 }
 
 /**
  * The `Localization` class is a central high-level API for vanilla
  * JavaScript use of Fluent.
  * It combines language negotiation, MessageContext and I/O to
  * provide a scriptable API to format translations.
  */
@@ -253,39 +249,36 @@ class Localization {
     const [val] = await this.formatValues([[id, args]]);
     return val;
   }
 
   /**
    * Register weak observers on events that will trigger cache invalidation
    */
   registerObservers() {
-    ObserverService.addObserver(this, 'l10n:available-locales-changed', true);
-    ObserverService.addObserver(this, 'intl:requested-locales-changed', true);
+    ObserverService.addObserver(this, 'intl:app-locales-changed', true);
   }
 
   /**
    * Unregister observers on events that will trigger cache invalidation
    */
   unregisterObservers() {
-    ObserverService.removeObserver(this, 'l10n:available-locales-changed');
-    ObserverService.removeObserver(this, 'intl:requested-locales-changed');
+    ObserverService.removeObserver(this, 'intl:app-locales-changed');
   }
 
   /**
    * Default observer handler method.
    *
    * @param {String} subject
    * @param {String} topic
    * @param {Object} data
    */
   observe(subject, topic, data) {
     switch (topic) {
-      case 'l10n:available-locales-changed':
-      case 'intl:requested-locales-changed':
+      case 'intl:app-locales-changed':
         this.onLanguageChange();
         break;
       default:
         break;
     }
   }
 
   /**
--- a/intl/locale/LocaleService.cpp
+++ b/intl/locale/LocaleService.cpp
@@ -7,16 +7,18 @@
 
 #include <algorithm>  // find_if()
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Omnijar.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
 #include "mozilla/intl/MozLocale.h"
 #include "mozilla/intl/OSPreferences.h"
+#include "nsDirectoryService.h"
+#include "nsDirectoryServiceDefs.h"
 #include "nsIObserverService.h"
 #include "nsIToolkitChromeRegistry.h"
 #include "nsStringEnumerator.h"
 #include "nsXULAppAPI.h"
 #include "nsZipArchive.h"
 
 #include "unicode/uloc.h"
 
@@ -62,46 +64,58 @@ SanitizeForBCP47(nsACString& aLocale, bo
   int32_t len = uloc_toLanguageTag(locale.get(), langTag, LANG_TAG_CAPACITY,
                                    strict, &err);
   if (U_SUCCESS(err) && len > 0) {
     aLocale.Assign(langTag, len);
   }
   return U_SUCCESS(err);
 }
 
+/**
+ * This function splits an input string by `,` delimiter, sanitizes the result
+ * language tags and returns them to the caller.
+ */
+static void
+SplitLocaleListStringIntoArray(nsACString& str, nsTArray<nsCString>& aRetVal)
+{
+  if (str.Length() > 0) {
+    for (const nsACString& part : str.Split(',')) {
+      nsAutoCString locale(part);
+      if (locale.EqualsLiteral("ja-JP-mac")) {
+        // This is a hack required to handle the special Mozilla `ja-JP-mac` locale.
+        if (!aRetVal.Contains(locale)) {
+          aRetVal.AppendElement(locale);
+        }
+      } else if (SanitizeForBCP47(locale, true)) {
+        if (!aRetVal.Contains(locale)) {
+          aRetVal.AppendElement(locale);
+        }
+      }
+    }
+  }
+}
+
 static bool
 ReadRequestedLocales(nsTArray<nsCString>& aRetVal)
 {
   nsAutoCString str;
   nsresult rv = Preferences::GetCString(REQUESTED_LOCALES_PREF, str);
 
   // We handle three scenarios here:
   //
   // 1) The pref is not set - use default locale
   // 2) The pref is set to "" - use OS locales
   // 3) The pref is set to a value - parse the locale list and use it
   if (NS_SUCCEEDED(rv)) {
-    if (str.Length() > 0) {
-      for (const nsACString& part : str.Split(',')) {
-        nsAutoCString locale(part);
-        if (locale.EqualsLiteral("ja-JP-mac")) {
-          // This is a hack required to handle the special Mozilla `ja-JP-mac` locale.
-          if (!aRetVal.Contains(locale)) {
-            aRetVal.AppendElement(locale);
-          }
-        } else if (SanitizeForBCP47(locale, true)) {
-          if (!aRetVal.Contains(locale)) {
-            aRetVal.AppendElement(locale);
-          }
-        }
-      }
-    } else {
+    if (str.Length() == 0) {
       // If the pref string is empty, we'll take requested locales
       // from the OS.
       OSPreferences::GetInstance()->GetSystemLocales(aRetVal);
+    } else {
+      SplitLocaleListStringIntoArray(str, aRetVal);
     }
   } else {
     nsAutoCString defaultLocale;
     LocaleService::GetInstance()->GetDefaultLocale(defaultLocale);
     aRetVal.AppendElement(defaultLocale);
   }
 
   // Last fallback locale is a locale for the requested locale chain.
@@ -111,81 +125,56 @@ ReadRequestedLocales(nsTArray<nsCString>
   // which follows the default locale the build is in.
   LocaleService::GetInstance()->GetLastFallbackLocale(str);
   if (!aRetVal.Contains(str)) {
     aRetVal.AppendElement(str);
   }
   return true;
 }
 
-static bool
-ReadAvailableLocales(nsTArray<nsCString>& aRetVal)
-{
-  nsCOMPtr<nsIToolkitChromeRegistry> cr =
-    mozilla::services::GetToolkitChromeRegistryService();
-  if (!cr) {
-    return false;
-  }
-
-  nsCOMPtr<nsIUTF8StringEnumerator> localesEnum;
-
-  nsresult rv =
-    cr->GetLocalesForPackage(NS_LITERAL_CSTRING("global"), getter_AddRefs(localesEnum));
-  if (!NS_SUCCEEDED(rv)) {
-    return false;
-  }
-
-  bool more;
-  while (NS_SUCCEEDED(rv = localesEnum->HasMore(&more)) && more) {
-    nsAutoCString localeStr;
-    rv = localesEnum->GetNext(localeStr);
-    if (!NS_SUCCEEDED(rv)) {
-      return false;
-    }
-
-    aRetVal.AppendElement(localeStr);
-  }
-  return !aRetVal.IsEmpty();
-}
-
 LocaleService::LocaleService(bool aIsServer)
   :mIsServer(aIsServer)
 {
 }
 
 /**
  * This function performs the actual language negotiation for the API.
  *
  * Currently it collects the locale ID used by nsChromeRegistry and
  * adds hardcoded default locale as a fallback.
  */
 void
 LocaleService::NegotiateAppLocales(nsTArray<nsCString>& aRetVal)
 {
-  nsAutoCString defaultLocale;
-  GetDefaultLocale(defaultLocale);
-
   if (mIsServer) {
+    nsAutoCString defaultLocale;
     AutoTArray<nsCString, 100> availableLocales;
     AutoTArray<nsCString, 10> requestedLocales;
+    GetDefaultLocale(defaultLocale);
     GetAvailableLocales(availableLocales);
     GetRequestedLocales(requestedLocales);
 
     NegotiateLanguages(requestedLocales, availableLocales, defaultLocale,
                        LangNegStrategy::Filtering, aRetVal);
   } else {
     // In content process, we will not do any language negotiation.
     // Instead, the language is set manually by SetAppLocales.
     //
     // If this method has been called, it means that we did not fire
     // SetAppLocales yet (happens during initialization).
-    // In that case, all we can do is return the default locale.
+    // In that case, all we can do is return the default or last fallback locale.
+    //
+    // We will return last fallback here, to avoid having to trigger reading
+    // `update.locale` for default locale.
+    //
     // Once SetAppLocales will be called later, it'll fire an event
     // allowing callers to update the locale.
-    aRetVal.AppendElement(defaultLocale);
+    nsAutoCString lastFallbackLocale;
+    GetLastFallbackLocale(lastFallbackLocale);
+    aRetVal.AppendElement(lastFallbackLocale);
   }
 }
 
 LocaleService*
 LocaleService::GetInstance()
 {
   if (!sInstance) {
     sInstance = new LocaleService(XRE_IsParentProcess());
@@ -315,33 +304,27 @@ LocaleService::GetRequestedLocales(nsTAr
   aRetVal = mRequestedLocales;
   return true;
 }
 
 bool
 LocaleService::GetAvailableLocales(nsTArray<nsCString>& aRetVal)
 {
   if (mAvailableLocales.IsEmpty()) {
-    ReadAvailableLocales(mAvailableLocales);
+    // If there are no available locales set, it means that L10nRegistry
+    // did not register its locale pool yet. The best course of action
+    // is to use packaged locales until that happens.
+    GetPackagedLocales(mAvailableLocales);
   }
 
   aRetVal = mAvailableLocales;
   return true;
 }
 
 void
-LocaleService::AvailableLocalesChanged()
-{
-  MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
-  mAvailableLocales.Clear();
-  // In the future we may want to trigger here intl:available-locales-changed
-  LocalesChanged();
-}
-
-void
 LocaleService::RequestedLocalesChanged()
 {
   MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
 
   nsTArray<nsCString> newLocales;
   ReadRequestedLocales(newLocales);
 
   if (mRequestedLocales != newLocales) {
@@ -367,20 +350,16 @@ LocaleService::LocalesChanged()
   nsTArray<nsCString> newLocales;
   NegotiateAppLocales(newLocales);
 
   if (mAppLocales != newLocales) {
     mAppLocales = Move(newLocales);
     nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
     if (obs) {
       obs->NotifyObservers(nullptr, "intl:app-locales-changed", nullptr);
-
-      // Deprecated, please use `intl:app-locales-changed`.
-      // Kept for now for compatibility reasons
-      obs->NotifyObservers(nullptr, "selected-locale-has-changed", nullptr);
     }
   }
 }
 
 // After trying each step of the negotiation algorithm for each requested locale,
 // if a match was found we use this macro to decide whether to return immediately,
 // skip to the next requested locale, or continue searching for additional matches,
 // according to the desired negotiation strategy.
@@ -596,55 +575,127 @@ LocaleService::LanguagesMatch(const nsAC
 
 
 bool
 LocaleService::IsServer()
 {
   return mIsServer;
 }
 
-/**
- * mozILocaleService methods
- */
-
 static char**
 CreateOutArray(const nsTArray<nsCString>& aArray)
 {
   uint32_t n = aArray.Length();
   char** result = static_cast<char**>(moz_xmalloc(n * sizeof(char*)));
   for (uint32_t i = 0; i < n; i++) {
     result[i] = moz_xstrdup(aArray[i].get());
   }
   return result;
 }
 
+static bool
+GetGREFileContents(const char* aFilePath, nsCString* aOutString)
+{
+  // Look for the requested file in omnijar.
+  RefPtr<nsZipArchive> zip = Omnijar::GetReader(Omnijar::GRE);
+  if (zip) {
+    nsZipItemPtr<char> item(zip, aFilePath);
+    if (!item) {
+      return false;
+    }
+    aOutString->Assign(item.Buffer(), item.Length());
+    return true;
+  }
+
+  // If we didn't have an omnijar (i.e. we're running a non-packaged
+  // build), then look in the GRE directory.
+  nsCOMPtr<nsIFile> path;
+  if (NS_FAILED(nsDirectoryService::gService->Get(NS_GRE_DIR,
+                                                  NS_GET_IID(nsIFile),
+                                                  getter_AddRefs(path)))) {
+    return false;
+  }
+
+  path->AppendRelativeNativePath(nsDependentCString(aFilePath));
+  bool result;
+  if (NS_FAILED(path->IsFile(&result)) || !result ||
+      NS_FAILED(path->IsReadable(&result)) || !result) {
+    return false;
+  }
+
+  // This is a small file, only used once, so it's not worth doing some fancy
+  // off-main-thread file I/O or whatever. Just read it.
+  FILE* fp;
+  if (NS_FAILED(path->OpenANSIFileDesc("r", &fp)) || !fp) {
+    return false;
+  }
+
+  fseek(fp, 0, SEEK_END);
+  long len = ftell(fp);
+  rewind(fp);
+  aOutString->SetLength(len);
+  size_t cc = fread(aOutString->BeginWriting(), 1, len, fp);
+
+  fclose(fp);
+
+  return cc == size_t(len);
+}
+
+void
+LocaleService::InitPackagedLocales()
+{
+  MOZ_ASSERT(mPackagedLocales.IsEmpty());
+
+  nsAutoCString localesString;
+  if (GetGREFileContents("res/multilocale.txt", &localesString)) {
+    localesString.Trim(" \t\n\r");
+    // This should never be empty in a correctly-built product.
+    MOZ_ASSERT(!localesString.IsEmpty());
+    SplitLocaleListStringIntoArray(localesString, mPackagedLocales);
+  }
+
+  // Last resort in case of broken build
+  if (mPackagedLocales.IsEmpty()) {
+    nsAutoCString defaultLocale;
+    GetDefaultLocale(defaultLocale);
+    mPackagedLocales.AppendElement(defaultLocale);
+  }
+}
+
+void
+LocaleService::GetPackagedLocales(nsTArray<nsCString>& aRetVal)
+{
+  if (mPackagedLocales.IsEmpty()) {
+    InitPackagedLocales();
+  }
+  aRetVal = mPackagedLocales;
+}
+
+/**
+ * mozILocaleService methods
+ */
+
 NS_IMETHODIMP
 LocaleService::GetDefaultLocale(nsACString& aRetVal)
 {
   // We don't allow this to change during a session (it's set at build/package
   // time), so we cache the result the first time we're called.
   if (mDefaultLocale.IsEmpty()) {
     // Try to get the package locale from update.locale in omnijar. If the
     // update.locale file is not found, item.len will remain 0 and we'll
     // just use our hard-coded default below.
-    // (We could also search for an update.locale file in the GRE resources
-    // directory, to support non-packaged builds, but that seems like a lot
-    // of extra code for what is probably not an important use case.)
-    RefPtr<nsZipArchive> zip = Omnijar::GetReader(Omnijar::GRE);
-    if (zip) {
-      nsZipItemPtr<char> item(zip, "update.locale");
-      size_t len = item.Length();
-      // Ignore any trailing spaces, newlines, etc.
-      while (len > 0 && item.Buffer()[len - 1] <= ' ') {
-        len--;
-      }
-      mDefaultLocale.Assign(item.Buffer(), len);
-    }
+    GetGREFileContents("update.locale", &mDefaultLocale);
+    mDefaultLocale.Trim(" \t\n\r");
+    // This should never be empty.
+    MOZ_ASSERT(!mDefaultLocale.IsEmpty());
+    MOZ_ASSERT(mDefaultLocale.EqualsLiteral("ja-JP-mac")
+        || SanitizeForBCP47(mDefaultLocale, true));
 
-    // Hard-coded fallback, e.g. for non-packaged developer builds.
+    // Hard-coded fallback to allow us to survive even if update.locale was
+    // missing/broken in some way.
     if (mDefaultLocale.IsEmpty()) {
       GetLastFallbackLocale(mDefaultLocale);
     }
   }
 
   aRetVal = mDefaultLocale;
   return NS_OK;
 }
@@ -877,8 +928,45 @@ LocaleService::GetAvailableLocales(uint3
 }
 
 NS_IMETHODIMP
 LocaleService::GetIsAppLocaleRTL(bool* aRetVal)
 {
   (*aRetVal) = IsAppLocaleRTL();
   return NS_OK;
 }
+
+NS_IMETHODIMP
+LocaleService::SetAvailableLocales(const char** aAvailable,
+                                   uint32_t aAvailableCount)
+{
+  nsTArray<nsCString> newLocales;
+
+  for (uint32_t i = 0; i < aAvailableCount; i++) {
+    nsAutoCString locale(aAvailable[i]);
+    if (!locale.EqualsLiteral("ja-JP-mac") &&
+        !SanitizeForBCP47(locale, true)) {
+      NS_ERROR("Invalid language tag provided to SetAvailableLocales!");
+      return NS_ERROR_INVALID_ARG;
+    }
+    newLocales.AppendElement(locale);
+  }
+
+  if (newLocales != mAvailableLocales) {
+    mAvailableLocales = Move(newLocales);
+    LocalesChanged();
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetPackagedLocales(uint32_t* aCount, char*** aOutArray)
+{
+  if (mPackagedLocales.IsEmpty()) {
+    InitPackagedLocales();
+  }
+
+  *aCount = mPackagedLocales.Length();
+  *aOutArray = CreateOutArray(mPackagedLocales);
+
+  return NS_OK;
+}
--- a/intl/locale/LocaleService.h
+++ b/intl/locale/LocaleService.h
@@ -63,19 +63,19 @@ namespace intl {
  * preferred currency, date/time formatting etc.
  *
  * An example of a Locale ID is `en-Latn-US-x-hc-h12-ca-gregory`
  *
  * At the moment we do not support full extension tag system, but we
  * try to be specific when naming APIs, so the service is for locales,
  * but we negotiate between languages etc.
  */
-class LocaleService : public mozILocaleService,
-                      public nsIObserver,
-                      public nsSupportsWeakReference
+class LocaleService final : public mozILocaleService,
+                            public nsIObserver,
+                            public nsSupportsWeakReference
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
   NS_DECL_MOZILOCALESERVICE
 
   /**
    * List of available language negotiation strategies.
@@ -196,29 +196,35 @@ public:
    * Returns a boolean indicating if the attempt to retrieve at least
    * one locale was successful.
    *
    * (See mozILocaleService.idl for a JS-callable version of this.)
    */
   bool GetAvailableLocales(nsTArray<nsCString>& aRetVal);
 
   /**
-   * Those three functions allow to trigger cache invalidation on one of the
+   * Returns a list of locales packaged into the app bundle.
+   *
+   * (See mozILocaleService.idl for a JS-callable version of this.)
+   */
+  void GetPackagedLocales(nsTArray<nsCString>& aRetVal);
+
+  /**
+   * Those two functions allow to trigger cache invalidation on one of the
    * three cached values.
    *
    * In most cases, the functions will be called by the observer in
    * LocaleService itself, but in a couple special cases, we have the
    * other component call this manually instead of sending a global event.
    *
    * If the result differs from the previous list, it will additionally
    * trigger a corresponding event
    *
    * This code should be called only in the server mode..
    */
-  void AvailableLocalesChanged();
   void RequestedLocalesChanged();
   void LocalesChanged();
 
   /**
    * Negotiates the best locales out of an ordered list of requested locales and
    * a list of available locales.
    *
    * Internally it uses the following naming scheme:
@@ -256,22 +262,25 @@ public:
 private:
   void FilterMatches(const nsTArray<nsCString>& aRequested,
                      const nsTArray<nsCString>& aAvailable,
                      LangNegStrategy aStrategy,
                      nsTArray<nsCString>& aRetVal);
 
   void NegotiateAppLocales(nsTArray<nsCString>& aRetVal);
 
+  void InitPackagedLocales();
+
   virtual ~LocaleService();
 
   nsAutoCStringN<16>  mDefaultLocale;
   nsTArray<nsCString> mAppLocales;
   nsTArray<nsCString> mRequestedLocales;
   nsTArray<nsCString> mAvailableLocales;
+  nsTArray<nsCString> mPackagedLocales;
   const bool mIsServer;
 
   static StaticRefPtr<LocaleService> sInstance;
 };
 } // intl
 } // namespace mozilla
 
 #endif /* mozilla_intl_LocaleService_h__ */
--- a/intl/locale/mozILocaleService.idl
+++ b/intl/locale/mozILocaleService.idl
@@ -200,9 +200,35 @@ interface mozILocaleService : nsISupport
    */
   void getAvailableLocales([optional] out unsigned long aCount,
                            [retval, array, size_is(aCount)] out string aLocales);
 
   /**
    * Returns whether the current app locale is RTL.
    */
   readonly attribute boolean isAppLocaleRTL;
+
+  /**
+   * Sets a list of locales the application has resources to be localized into.
+   *
+   * The primary use of this function is to let L10nRegistry communicate all
+   * locale updates.
+   *
+   * The secondary use case is for testing purposes in scenarios in which the
+   * actual resources don't have to be available.
+   * It is recommended for tests to create a mock FileSource and register it in
+   * the L10nRegistry rather than using this call, in order to emulate full
+   * resource availability cycle.
+   *
+   */
+  void setAvailableLocales([array, size_is(aAvailableCount)] in string aAvailable,
+                           [optional] in unsigned long aAvailableCount);
+
+  /**
+   * Returns a list of locales packaged into the app bundle.
+   *
+   * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+   *
+   * (See LocaleService.h for a more C++-friendly version of this.)
+   */
+  void getPackagedLocales([optional] out unsigned long aCount,
+                          [retval, array, size_is(aCount)] out string aOutArray);
 };
--- a/intl/locale/tests/gtest/TestLocaleService.cpp
+++ b/intl/locale/tests/gtest/TestLocaleService.cpp
@@ -13,57 +13,75 @@ using namespace mozilla::intl;
 
 TEST(Intl_Locale_LocaleService, GetAppLocalesAsLangTags) {
   nsTArray<nsCString> appLocales;
   LocaleService::GetInstance()->GetAppLocalesAsLangTags(appLocales);
 
   ASSERT_FALSE(appLocales.IsEmpty());
 }
 
-TEST(Intl_Locale_LocaleService, GetAppLocalesAsLangTags_firstMatchesChromeReg) {
-  nsTArray<nsCString> appLocales;
-  LocaleService::GetInstance()->GetAppLocalesAsLangTags(appLocales);
-
-  nsAutoCString uaLangTag;
-  nsCOMPtr<nsIToolkitChromeRegistry> cr =
-    mozilla::services::GetToolkitChromeRegistryService();
-  if (cr) {
-    cr->GetSelectedLocale(NS_LITERAL_CSTRING("global"), true, uaLangTag);
-  }
-
-  ASSERT_TRUE(appLocales[0].Equals(uaLangTag));
-}
-
 TEST(Intl_Locale_LocaleService, GetAppLocalesAsLangTags_lastIsEnUS) {
   nsAutoCString lastFallbackLocale;
   LocaleService::GetInstance()->GetLastFallbackLocale(lastFallbackLocale);
 
   nsTArray<nsCString> appLocales;
   LocaleService::GetInstance()->GetAppLocalesAsLangTags(appLocales);
 
   int32_t len = appLocales.Length();
   ASSERT_TRUE(appLocales[len - 1].Equals(lastFallbackLocale));
 }
 
-TEST(Intl_Locale_LocaleService, GetRequestedLocales) {
-  nsTArray<nsCString> reqLocales;
-  LocaleService::GetInstance()->GetRequestedLocales(reqLocales);
-
-  int32_t len = reqLocales.Length();
-  ASSERT_TRUE(len > 0);
-}
-
 TEST(Intl_Locale_LocaleService, GetAppLocaleAsLangTag) {
   nsTArray<nsCString> appLocales;
   LocaleService::GetInstance()->GetAppLocalesAsLangTags(appLocales);
 
   nsAutoCString locale;
   LocaleService::GetInstance()->GetAppLocaleAsLangTag(locale);
 
   ASSERT_TRUE(appLocales[0] == locale);
 }
 
+
+TEST(Intl_Locale_LocaleService, GetRegionalPrefsLocales) {
+  nsTArray<nsCString> rpLocales;
+  LocaleService::GetInstance()->GetRegionalPrefsLocales(rpLocales);
+
+  int32_t len = rpLocales.Length();
+  ASSERT_TRUE(len > 0);
+}
+
+TEST(Intl_Locale_LocaleService, GetRequestedLocales) {
+  nsTArray<nsCString> reqLocales;
+  LocaleService::GetInstance()->GetRequestedLocales(reqLocales);
+
+  int32_t len = reqLocales.Length();
+  ASSERT_TRUE(len > 0);
+}
+
+TEST(Intl_Locale_LocaleService, GetAvailableLocales) {
+  nsTArray<nsCString> availableLocales;
+  LocaleService::GetInstance()->GetAvailableLocales(availableLocales);
+
+  int32_t len = availableLocales.Length();
+  ASSERT_TRUE(len > 0);
+}
+
+TEST(Intl_Locale_LocaleService, GetPackagedLocales) {
+  nsTArray<nsCString> packagedLocales;
+  LocaleService::GetInstance()->GetPackagedLocales(packagedLocales);
+
+  int32_t len = packagedLocales.Length();
+  ASSERT_TRUE(len > 0);
+}
+
+TEST(Intl_Locale_LocaleService, GetDefaultLocale) {
+  nsAutoCString locale;
+  LocaleService::GetInstance()->GetDefaultLocale(locale);
+
+  int32_t len = locale.Length();
+  ASSERT_TRUE(len > 0);
+}
+
 TEST(Intl_Locale_LocaleService, IsAppLocaleRTL) {
   // For now we can only test if the method doesn't crash.
   LocaleService::GetInstance()->IsAppLocaleRTL();
   ASSERT_TRUE(true);
-
 }
--- a/intl/locale/tests/unit/test_localeService.js
+++ b/intl/locale/tests/unit/test_localeService.js
@@ -121,48 +121,88 @@ add_test(function test_getRequestedLocal
   Assert.ok(requestedLocale === "tlh", "requestedLocale returns the right value");
 
   Services.prefs.clearUserPref(PREF_REQUESTED_LOCALES);
 
   run_next_test();
 });
 
 add_test(function test_setRequestedLocales() {
-  localeService.setRequestedLocales([]);
-
   localeService.setRequestedLocales(['de-AT', 'de-DE', 'de-CH']);
 
-  let locales = localeService.getRequestedLocales();;
+  let locales = localeService.getRequestedLocales();
   Assert.ok(locales[0] === 'de-AT');
   Assert.ok(locales[1] === 'de-DE');
   Assert.ok(locales[2] === 'de-CH');
 
   run_next_test();
 });
 
 add_test(function test_isAppLocaleRTL() {
   Assert.ok(typeof localeService.isAppLocaleRTL === 'boolean');
 
   run_next_test();
 });
 
+add_test(function test_getPackagedLocales() {
+  const locales = localeService.getPackagedLocales();
+  Assert.ok(locales.length !== 0, "Packaged locales are empty");
+  run_next_test();
+});
+
+add_test(function test_setAvailableLocales() {
+  const avLocales = localeService.getAvailableLocales();
+  localeService.setAvailableLocales(["und", "ar-IR"]);
+
+  let locales = localeService.getAvailableLocales();
+  Assert.ok(locales.length == 2);
+  Assert.ok(locales[0] === 'und');
+  Assert.ok(locales[1] === 'ar-IR');
+
+  localeService.setAvailableLocales(avLocales);
+
+  run_next_test();
+});
+
 /**
  * This test verifies that all values coming from the pref are sanitized.
  */
 add_test(function test_getRequestedLocales_sanitize() {
-  Services.prefs.setCharPref(PREF_REQUESTED_LOCALES, "de,2,#$@#,pl,!a2,DE-at,,;");
+  Services.prefs.setCharPref(PREF_REQUESTED_LOCALES, "de,2,#$@#,pl,ąó,!a2,DE-at,,;");
 
   let locales = localeService.getRequestedLocales();
   Assert.equal(locales[0], "de");
   Assert.equal(locales[1], "pl");
   Assert.equal(locales[2], "de-AT");
   Assert.equal(locales[3], "und");
   Assert.equal(locales[4], localeService.lastFallbackLocale);
   Assert.equal(locales.length, 5);
 
   Services.prefs.clearUserPref(PREF_REQUESTED_LOCALES);
 
   run_next_test();
 });
 
+add_test(function test_handle_ja_JP_mac() {
+  const bkpAvLocales = localeService.getAvailableLocales();
+
+  localeService.setAvailableLocales(["ja-JP-mac", "en-US"]);
+
+  localeService.setRequestedLocales(['ja-JP-mac']);
+
+  let reqLocales = localeService.getRequestedLocales();
+  Assert.ok(reqLocales[0] === 'ja-JP-mac');
+
+  let avLocales = localeService.getAvailableLocales();
+  Assert.ok(avLocales[0] === 'ja-JP-mac');
+
+  let appLocales = localeService.getAppLocalesAsLangTags();
+  Assert.ok(appLocales[0] === 'ja-JP-mac');
+
+  localeService.setAvailableLocales(bkpAvLocales);
+
+  run_next_test();
+});
+
+
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref(PREF_REQUESTED_LOCALES);
 });
--- a/intl/locale/tests/unit/test_localeService_negotiateLanguages.js
+++ b/intl/locale/tests/unit/test_localeService_negotiateLanguages.js
@@ -86,16 +86,17 @@ const data = {
       [[null], [], []],
       [[undefined], [], []],
       [[undefined], [null], []],
       [[undefined], [undefined], []],
       [[null], [null], null, null, []],
       [undefined, ["fr-FR"], []],
       [2, ["fr-FR"], []],
       ["fr-FR", ["fr-FR"], []],
+      [["fą-FŻ"], ["ór_Fń"], []],
       [["fr-FR"], null, []],
       [["fr-FR"], undefined, []],
       [["fr-FR"], 2, []],
       [["fr-FR"], "fr-FR", []],
       [["2"], ["ąóżł"], []],
       [[[]], ["fr-FR"], []],
       [[[]], [[2]], []],
     ],