Bug 1431260 - Migrate LocaleService::AvailableLocales from ChromeRegistry to multilocale.txt. r?jfkthame draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Tue, 13 Feb 2018 23:41:39 -0800
changeset 757607 bbc2b5baf89bf57d11f9393103a69a2135984283
parent 757133 dc70d241f90df43505ece5ac12261339e9694c50
child 757608 deb18805ce8e7ea51e666a33b7128aa64b07cb65
push id99797
push userbmo:gandalf@aviary.pl
push dateTue, 20 Feb 2018 22:19:29 +0000
reviewersjfkthame
bugs1431260
milestone60.0a1
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]], []],
     ],