Bug 1428698 - Align intl::locale::Locale with BCP47. r=jfkthame
☠☠ backed out by bf540cb73978 ☠ ☠
authorZibi Braniecki <zbraniecki@mozilla.com>
Thu, 25 Jan 2018 14:50:32 -0800
changeset 455886 804f26b2c6b8284b3f862d5e56814e1128bf1d5b
parent 455885 bb5db229b32f07af2b06fb085ba34135413e6c95
child 455887 949b1558f9b9bd4b8975ca5f1ff1ebb75688bcec
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
bugs1428698
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 1428698 - Align intl::locale::Locale with BCP47. r=jfkthame MozReview-Commit-ID: AT9MPppx99p
intl/locale/LocaleService.cpp
intl/locale/LocaleService.h
intl/locale/MozLocale.cpp
intl/locale/MozLocale.h
intl/locale/tests/gtest/TestMozLocale.cpp
intl/locale/tests/gtest/moz.build
intl/locale/tests/unit/test_localeService_negotiateLanguages.js
--- a/intl/locale/LocaleService.cpp
+++ b/intl/locale/LocaleService.cpp
@@ -441,95 +441,98 @@ LocaleService::FilterMatches(const nsTAr
                              LangNegStrategy aStrategy,
                              nsTArray<nsCString>& aRetVal)
 {
   // Local copy of the list of available locales, in Locale form for flexible
   // matching. We will remove entries from this list as they get appended to
   // aRetVal, so that no available locale will be found more than once.
   AutoTArray<Locale, 100> availLocales;
   for (auto& avail : aAvailable) {
-    availLocales.AppendElement(Locale(avail, true));
+    availLocales.AppendElement(Locale(avail));
   }
 
   // Helper to erase an entry from availLocales once we have copied it to
   // the result list. Returns an iterator pointing to the entry that was
   // immediately after the one that was erased (or availLocales.end() if
   // the target was the last in the array).
   auto eraseFromAvail = [&](nsTArray<Locale>::iterator aIter) {
     nsTArray<Locale>::size_type index = aIter - availLocales.begin();
     availLocales.RemoveElementAt(index);
     return availLocales.begin() + index;
   };
 
   for (auto& requested : aRequested) {
+    if (requested.IsEmpty()) {
+      continue;
+    }
 
     // 1) Try to find a simple (case-insensitive) string match for the request.
-    auto matchesExactly = [&](const Locale& aLoc) {
+    auto matchesExactly = [&](Locale& aLoc) {
       return requested.Equals(aLoc.AsString(),
                               nsCaseInsensitiveCStringComparator());
     };
     auto match = std::find_if(availLocales.begin(), availLocales.end(),
                               matchesExactly);
     if (match != availLocales.end()) {
       aRetVal.AppendElement(match->AsString());
       eraseFromAvail(match);
     }
 
     if (!aRetVal.IsEmpty()) {
       HANDLE_STRATEGY;
     }
 
     // 2) Try to match against the available locales treated as ranges.
-    auto findRangeMatches = [&](const Locale& aReq) {
-      auto matchesRange = [&](const Locale& aLoc) {
-        return aReq.Matches(aLoc);
+    auto findRangeMatches = [&](Locale& aReq, bool aAvailRange, bool aReqRange) {
+      auto matchesRange = [&](Locale& aLoc) {
+        return aLoc.Matches(aReq, aAvailRange, aReqRange);
       };
       bool foundMatch = false;
       auto match = availLocales.begin();
       while ((match = std::find_if(match, availLocales.end(),
                                    matchesRange)) != availLocales.end()) {
         aRetVal.AppendElement(match->AsString());
         match = eraseFromAvail(match);
         foundMatch = true;
         if (aStrategy != LangNegStrategy::Filtering) {
           return true; // we only want the first match
         }
       }
       return foundMatch;
     };
 
-    Locale requestedLocale = Locale(requested, false);
-    if (findRangeMatches(requestedLocale)) {
+    Locale requestedLocale = Locale(requested);
+    if (findRangeMatches(requestedLocale, true, false)) {
       HANDLE_STRATEGY;
     }
 
     // 3) Try to match against a maximized version of the requested locale
     if (requestedLocale.AddLikelySubtags()) {
-      if (findRangeMatches(requestedLocale)) {
+      if (findRangeMatches(requestedLocale, true, false)) {
         HANDLE_STRATEGY;
       }
     }
 
     // 4) Try to match against a variant as a range
-    requestedLocale.SetVariantRange();
-    if (findRangeMatches(requestedLocale)) {
+    requestedLocale.ClearVariants();
+    if (findRangeMatches(requestedLocale, true, true)) {
       HANDLE_STRATEGY;
     }
 
     // 5) Try to match against the likely subtag without region
-    if (requestedLocale.AddLikelySubtagsWithoutRegion()) {
-      if (findRangeMatches(requestedLocale)) {
+    requestedLocale.ClearRegion();
+    if (requestedLocale.AddLikelySubtags()) {
+      if (findRangeMatches(requestedLocale, true, false)) {
         HANDLE_STRATEGY;
       }
     }
 
-
     // 6) Try to match against a region as a range
-    requestedLocale.SetRegionRange();
-    if (findRangeMatches(requestedLocale)) {
+    requestedLocale.ClearRegion();
+    if (findRangeMatches(requestedLocale, true, true)) {
       HANDLE_STRATEGY;
     }
   }
 }
 
 bool
 LocaleService::NegotiateLanguages(const nsTArray<nsCString>& aRequested,
                                   const nsTArray<nsCString>& aAvailable,
@@ -587,20 +590,22 @@ LocaleService::Observe(nsISupports *aSub
       RequestedLocalesChanged();
     }
   }
 
   return NS_OK;
 }
 
 bool
-LocaleService::LanguagesMatch(const nsCString& aRequested,
-                              const nsCString& aAvailable)
+LocaleService::LanguagesMatch(const nsACString& aRequested,
+                              const nsACString& aAvailable)
 {
-  return Locale(aRequested, true).LanguageMatches(Locale(aAvailable, true));
+  Locale requested = Locale(aRequested);
+  Locale available = Locale(aAvailable);
+  return requested.GetLanguage().Equals(available.GetLanguage());
 }
 
 
 bool
 LocaleService::IsServer()
 {
   return mIsServer;
 }
--- a/intl/locale/LocaleService.h
+++ b/intl/locale/LocaleService.h
@@ -242,18 +242,18 @@ public:
                           LangNegStrategy aLangNegStrategy,
                           nsTArray<nsCString>& aRetVal);
 
   /**
    * Returns whether the current app locale is RTL.
    */
   bool IsAppLocaleRTL();
 
-  static bool LanguagesMatch(const nsCString& aRequested,
-                             const nsCString& aAvailable);
+  static bool LanguagesMatch(const nsACString& aRequested,
+                             const nsACString& aAvailable);
 
   bool IsServer();
 
 private:
   void FilterMatches(const nsTArray<nsCString>& aRequested,
                      const nsTArray<nsCString>& aAvailable,
                      LangNegStrategy aStrategy,
                      nsTArray<nsCString>& aRetVal);
--- a/intl/locale/MozLocale.cpp
+++ b/intl/locale/MozLocale.cpp
@@ -1,157 +1,212 @@
 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 /* 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/. */
 
 #include "mozilla/intl/MozLocale.h"
 
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+
 #include "unicode/uloc.h"
 
 using namespace mozilla::intl;
 
 /**
  * Note: The file name is `MozLocale` to avoid compilation problems on case-insensitive
  * Windows. The class name is `Locale`.
  */
-Locale::Locale(const nsCString& aLocale, bool aRange)
-  : mLocaleStr(aLocale)
+Locale::Locale(const nsACString& aLocale)
 {
-  int32_t partNum = 0;
+  int32_t position = 0;
+
+  if (!IsASCII(aLocale)) {
+    mIsValid = false;
+    return;
+  }
 
   nsAutoCString normLocale(aLocale);
   normLocale.ReplaceChar('_', '-');
 
-  for (const nsACString& part : normLocale.Split('-')) {
-    switch (partNum) {
-      case 0:
-        if (part.EqualsLiteral("*") ||
-            part.Length() == 2 || part.Length() == 3) {
-          mLanguage.Assign(part);
-        }
-        break;
-      case 1:
-        if (part.EqualsLiteral("*") || part.Length() == 4) {
-          mScript.Assign(part);
-          break;
-        }
-
-        // fallover to region case
-        partNum++;
-        MOZ_FALLTHROUGH;
-      case 2:
-        if (part.EqualsLiteral("*") || part.Length() == 2) {
-          mRegion.Assign(part);
-        }
-        break;
-      case 3:
-        if (part.EqualsLiteral("*") || (part.Length() >= 3 && part.Length() <= 8)) {
-          mVariant.Assign(part);
-        }
-        break;
-    }
-    partNum++;
-  }
-
-  if (aRange) {
-    if (mLanguage.IsEmpty()) {
-      mLanguage.AssignLiteral("*");
-    }
-    if (mScript.IsEmpty()) {
-      mScript.AssignLiteral("*");
-    }
-    if (mRegion.IsEmpty()) {
-      mRegion.AssignLiteral("*");
-    }
-    if (mVariant.IsEmpty()) {
-      mVariant.AssignLiteral("*");
+  /**
+   * BCP47 language tag:
+   *
+   * langtag = language            2*3ALPHA
+   *           ["-" extlang]       3ALPHA *2("-" 3ALPHA)
+   *           ["-" script]        4ALPHA
+   *           ["-" region]        2ALPHA / 3DIGIT
+   *           *("-" variant)      5*8alphanum / (DIGIT 3alphanum)
+   *           *("-" extension)    [0-9a-wy-z] 1*("-" (1*8alphanum))
+   *           ["-" privateuse]    x 1*("-" (1*8alphanum))
+   *
+   * This class currently supports a subset of the full BCP47 language tag
+   * with a single extension of allowing variants to be 3ALPHA to support
+   * `ja-JP-mac` code:
+   *
+   * langtag = language            2*3ALPHA
+   *           ["-" script]        4ALPHA
+   *           ["-" region]        2ALPHA
+   *           *("-" variant)      3*8alphanum
+   *
+   * The `position` variable represents the currently expected section of the tag
+   * and intentionally skips positions (like `extlang`) which may be added later.
+   */
+  for (const nsACString& subTag : normLocale.Split('-')) {
+    auto slen = subTag.Length();
+    if (position == 0) {
+      if (slen < 2 || slen > 3) {
+        mIsValid = false;
+        return;
+      }
+      mLanguage = subTag;
+      ToLowerCase(mLanguage);
+      position = 2;
+    } else if (position <= 2 && slen == 4) {
+      mScript = subTag;
+      ToLowerCase(mScript);
+      mScript.Replace(0, 1, ToUpperCase(mScript[0]));
+      position = 3;
+    } else if (position <= 3 && slen == 2) {
+      mRegion = subTag;
+      ToUpperCase(mRegion);
+      position = 4;
+    } else if (position <= 4 && slen >= 3 && slen <= 8) {
+      // we're quirky here because we allow for variant to be 3 char long.
+      // BCP47 requires variants to be 5-8 char long at lest.
+      //
+      // We do this to support the `ja-JP-mac` quirk that we have.
+      nsAutoCString lcSubTag(subTag);
+      ToLowerCase(lcSubTag);
+      mVariants.InsertElementSorted(lcSubTag);
+      position = 4;
     }
   }
 }
 
-static bool
-SubtagMatches(const nsCString& aSubtag1, const nsCString& aSubtag2)
+bool
+Locale::IsValid()
+{
+  return mIsValid;
+}
+
+const nsCString
+Locale::AsString()
 {
-  return aSubtag1.EqualsLiteral("*") ||
-         aSubtag2.EqualsLiteral("*") ||
-         aSubtag1.Equals(aSubtag2, nsCaseInsensitiveCStringComparator());
+  nsCString tag;
+
+  if (!mIsValid) {
+    tag.AppendLiteral("und");
+    return tag;
+  }
+
+  tag.Append(mLanguage);
+
+  if (!mScript.IsEmpty()) {
+    tag.AppendLiteral("-");
+    tag.Append(mScript);
+  }
+
+  if (!mRegion.IsEmpty()) {
+    tag.AppendLiteral("-");
+    tag.Append(mRegion);
+  }
+
+  for (const auto& variant : mVariants) {
+    tag.AppendLiteral("-");
+    tag.Append(variant);
+  }
+  return tag;
+}
+
+const nsACString&
+Locale::GetLanguage() const
+{
+  return mLanguage;
+}
+
+const nsACString&
+Locale::GetScript() const
+{
+  return mScript;
+}
+
+const nsACString&
+Locale::GetRegion() const
+{
+  return mRegion;
+}
+
+const nsTArray<nsCString>&
+Locale::GetVariants() const
+{
+  return mVariants;
 }
 
 bool
-Locale::Matches(const Locale& aLocale) const
-{
-  return SubtagMatches(mLanguage, aLocale.mLanguage) &&
-         SubtagMatches(mScript, aLocale.mScript) &&
-         SubtagMatches(mRegion, aLocale.mRegion) &&
-         SubtagMatches(mVariant, aLocale.mVariant);
-}
-
-bool
-Locale::LanguageMatches(const Locale& aLocale) const
+Locale::Matches(const Locale& aOther, bool aThisRange, bool aOtherRange) const
 {
-  return SubtagMatches(mLanguage, aLocale.mLanguage) &&
-         SubtagMatches(mScript, aLocale.mScript);
-}
+  if ((!aThisRange || !mLanguage.IsEmpty()) &&
+      (!aOtherRange || !aOther.mLanguage.IsEmpty()) &&
+      !mLanguage.Equals(aOther.mLanguage)) {
+    return false;
+  }
 
-void
-Locale::SetVariantRange()
-{
-  mVariant.AssignLiteral("*");
-}
-
-void
-Locale::SetRegionRange()
-{
-  mRegion.AssignLiteral("*");
+  if ((!aThisRange || !mScript.IsEmpty()) &&
+      (!aOtherRange || !aOther.mScript.IsEmpty()) &&
+      !mScript.Equals(aOther.mScript)) {
+    return false;
+  }
+  if ((!aThisRange || !mRegion.IsEmpty()) &&
+      (!aOtherRange || !aOther.mRegion.IsEmpty()) &&
+      !mRegion.Equals(aOther.mRegion)) {
+    return false;
+  }
+  if ((!aThisRange || !mVariants.IsEmpty()) &&
+      (!aOtherRange || !aOther.mVariants.IsEmpty()) &&
+      mVariants != aOther.mVariants) {
+    return false;
+  }
+  return true;
 }
 
 bool
 Locale::AddLikelySubtags()
 {
-  return AddLikelySubtagsForLocale(mLocaleStr);
-}
-
-bool
-Locale::AddLikelySubtagsWithoutRegion()
-{
-  nsAutoCString locale(mLanguage);
-
-  if (!mScript.IsEmpty()) {
-    locale.Append("-");
-    locale.Append(mScript);
-  }
-
-  // We don't add variant here because likelySubtag doesn't care about it.
-
-  return AddLikelySubtagsForLocale(locale);
-}
-
-bool
-Locale::AddLikelySubtagsForLocale(const nsACString& aLocale)
-{
   const int32_t kLocaleMax = 160;
   char maxLocale[kLocaleMax];
-  nsAutoCString locale(aLocale);
 
   UErrorCode status = U_ZERO_ERROR;
-  uloc_addLikelySubtags(locale.get(), maxLocale, kLocaleMax, &status);
+  uloc_addLikelySubtags(AsString().get(), maxLocale, kLocaleMax, &status);
 
   if (U_FAILURE(status)) {
     return false;
   }
 
   nsDependentCString maxLocStr(maxLocale);
-  Locale loc = Locale(maxLocStr, false);
+  Locale loc = Locale(maxLocStr);
 
   if (loc == *this) {
     return false;
   }
 
   mLanguage = loc.mLanguage;
   mScript = loc.mScript;
   mRegion = loc.mRegion;
 
   // We don't update variant from likelySubtag since it's not going to
   // provide it and we want to preserve the range
 
   return true;
 }
+
+void
+Locale::ClearVariants()
+{
+  mVariants.Clear();
+}
+
+void
+Locale::ClearRegion()
+{
+  mRegion.Truncate();
+}
--- a/intl/locale/MozLocale.h
+++ b/intl/locale/MozLocale.h
@@ -1,64 +1,93 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
-#ifndef mozilla_intl_Locale_h__
-#define mozilla_intl_Locale_h__
+#ifndef mozilla_intl_MozLocale_h__
+#define mozilla_intl_MozLocale_h__
 
 #include "nsString.h"
+#include "nsTArray.h"
 
 namespace mozilla {
 namespace intl {
 
 /**
- * Locale object, a BCP47-style tag decomposed into subtags for
- * matching purposes.
+ * Locale class is a core representation of a single locale.
+ *
+ * A locale is a data object representing a combination of language, script,
+ * region, variant and a set of regional extension preferences that may further specify
+ * particular user choices like calendar, numbering system, etc.
+ *
+ * A locale can be expressed as a language tag string, like a simple "fr" for French,
+ * or a more specific "sr-Cyrl-RS-u-hc-h12" for Serbian in Russia with a Cyrylic script,
+ * and hour cycle selected to be `h12`.
+ *
+ * The format of the language tag follows BCP47 standard and implements a subset of it.
+ * In the future we expect to extend this class to cover more subtags and extensions.
+ *
+ * BCP47: https://tools.ietf.org/html/bcp47
  *
- * If constructed with aRange = true, any missing subtags will be
- * set to "*".
+ * The aim of this class it aid in validation, parsing and canonicalization of the
+ * string.
+ *
+ * It allows the user to input any well-formed BCP47 language tag and operate
+ * on its subtags in a canonicalized form.
+ *
+ * It should be used for all operations on language tags, and together with
+ * LocaleService::NegotiateLanguages for language negotiation.
+ *
+ * Example:
+ *
+ *     Locale loc = Locale("de-at");
+ *
+ *     ASSERT_TRUE(loc.GetLanguage().Equals("de"));
+ *     ASSERT_TRUE(loc.GetScript().IsEmpty());
+ *     ASSERT_TRUE(loc.GetRegion().Equals("AT")); // canonicalized to upper case
+ *
  *
  * Note: The file name is `MozLocale` to avoid compilation problems on case-insensitive
  * Windows. The class name is `Locale`.
  */
 class Locale {
   public:
-    Locale(const nsCString& aLocale, bool aRange);
-
-    bool Matches(const Locale& aLocale) const;
-    bool LanguageMatches(const Locale& aLocale) const;
-
+    explicit Locale(const nsACString& aLocale);
+    explicit Locale(const char* aLocale)
+      : Locale(nsDependentCString(aLocale))
+      { };
 
-    void SetVariantRange();
-    void SetRegionRange();
+    const nsACString& GetLanguage() const;
+    const nsACString& GetScript() const;
+    const nsACString& GetRegion() const;
+    const nsTArray<nsCString>& GetVariants() const;
 
-    // returns false if nothing changed
+    bool IsValid();
+    const nsCString AsString();
+
+    bool Matches(const Locale& aOther, bool aThisRange, bool aOtherRange) const;
     bool AddLikelySubtags();
-    bool AddLikelySubtagsWithoutRegion();
-
-    const nsCString& AsString() const {
-      return mLocaleStr;
-    }
+    void ClearVariants();
+    void ClearRegion();
 
     bool operator== (const Locale& aOther) {
-      const auto& cmp = nsCaseInsensitiveCStringComparator();
-      return mLanguage.Equals(aOther.mLanguage, cmp) &&
-             mScript.Equals(aOther.mScript, cmp) &&
-             mRegion.Equals(aOther.mRegion, cmp) &&
-             mVariant.Equals(aOther.mVariant, cmp);
+      return mLanguage.Equals(aOther.mLanguage) &&
+             mScript.Equals(aOther.mScript) &&
+             mRegion.Equals(aOther.mRegion) &&
+             mVariants == aOther.mVariants;
+
     }
 
   private:
-    const nsCString& mLocaleStr;
-    nsCString mLanguage;
-    nsCString mScript;
-    nsCString mRegion;
-    nsCString mVariant;
-
-    bool AddLikelySubtagsForLocale(const nsACString& aLocale);
+    nsAutoCStringN<3> mLanguage;
+    nsAutoCStringN<4> mScript;
+    nsAutoCStringN<2> mRegion;
+    nsTArray<nsCString> mVariants;
+    bool mIsValid = true;
 };
 
 } // intl
 } // namespace mozilla
 
-#endif /* mozilla_intl_Locale_h__ */
+DECLARE_USE_COPY_CONSTRUCTORS(mozilla::intl::Locale)
+
+#endif /* mozilla_intl_MozLocale_h__ */
new file mode 100644
--- /dev/null
+++ b/intl/locale/tests/gtest/TestMozLocale.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/intl/MozLocale.h"
+
+using namespace mozilla::intl;
+
+
+TEST(Intl_Locale_Locale, Locale) {
+  Locale loc = Locale("en-US");
+
+  ASSERT_TRUE(loc.GetLanguage().Equals("en"));
+  ASSERT_TRUE(loc.GetRegion().Equals("US"));
+}
+
+TEST(Intl_Locale_Locale, AsString) {
+  Locale loc = Locale("ja-jp-windows");
+
+  ASSERT_TRUE(loc.AsString().Equals("ja-JP-windows"));
+}
+
+TEST(Intl_Locale_Locale, GetSubTags) {
+  Locale loc = Locale("en-latn-us-macos");
+
+  ASSERT_TRUE(loc.GetLanguage().Equals("en"));
+  ASSERT_TRUE(loc.GetScript().Equals("Latn"));
+  ASSERT_TRUE(loc.GetRegion().Equals("US"));
+  ASSERT_TRUE(loc.GetVariants().Length() == 1);
+  ASSERT_TRUE(loc.GetVariants()[0].Equals("macos"));
+}
+
+TEST(Intl_Locale_Locale, Matches) {
+  Locale loc = Locale("en-US");
+
+  Locale loc2 = Locale("en-GB");
+  ASSERT_FALSE(loc == loc2);
+
+  Locale loc3 = Locale("en-US");
+  ASSERT_TRUE(loc == loc3);
+
+  Locale loc4 = Locale("En_us");
+  ASSERT_TRUE(loc == loc4);
+}
+
+TEST(Intl_Locale_Locale, MatchesRange) {
+  Locale loc = Locale("en-US");
+
+  Locale loc2 = Locale("en-Latn-US");
+  ASSERT_FALSE(loc == loc2);
+  ASSERT_TRUE(loc.Matches(loc2, true, false));
+  ASSERT_FALSE(loc.Matches(loc2, false, true));
+  ASSERT_FALSE(loc.Matches(loc2, false, false));
+  ASSERT_TRUE(loc.Matches(loc2, true, true));
+
+  Locale loc3 = Locale("en");
+  ASSERT_FALSE(loc == loc3);
+  ASSERT_TRUE(loc.Matches(loc3, false, true));
+  ASSERT_FALSE(loc.Matches(loc3, true, false));
+  ASSERT_FALSE(loc.Matches(loc3, false, false));
+  ASSERT_TRUE(loc.Matches(loc3, true, true));
+}
--- a/intl/locale/tests/gtest/moz.build
+++ b/intl/locale/tests/gtest/moz.build
@@ -4,12 +4,13 @@
 # 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/.
 
 UNIFIED_SOURCES += [
     'TestCollation.cpp',
     'TestDateTimeFormat.cpp',
     'TestLocaleService.cpp',
     'TestLocaleServiceNegotiate.cpp',
+    'TestMozLocale.cpp',
     'TestOSPreferences.cpp',
 ]
 
 FINAL_LIBRARY = 'xul-gtest'
--- a/intl/locale/tests/unit/test_localeService_negotiateLanguages.js
+++ b/intl/locale/tests/unit/test_localeService_negotiateLanguages.js
@@ -7,90 +7,84 @@ const localeService =
   .getService(Components.interfaces.mozILocaleService);
 
 const data = {
   "filtering": {
     "exact match": [
       [["en"], ["en"], ["en"]],
       [["en-US"], ["en-US"], ["en-US"]],
       [["en-Latn-US"], ["en-Latn-US"], ["en-Latn-US"]],
-      [["en-Latn-US-mac"], ["en-Latn-US-mac"], ["en-Latn-US-mac"]],
+      [["en-Latn-US-windows"], ["en-Latn-US-windows"], ["en-Latn-US-windows"]],
       [["fr-FR"], ["de", "it", "fr-FR"], ["fr-FR"]],
       [["fr", "pl", "de-DE"], ["pl", "en-US", "de-DE"], ["pl", "de-DE"]],
     ],
     "available as range": [
       [["en-US"], ["en"], ["en"]],
       [["en-Latn-US"], ["en-US"], ["en-US"]],
-      [["en-US-mac"], ["en-US"], ["en-US"]],
+      [["en-US-windows"], ["en-US"], ["en-US"]],
       [["fr-CA", "de-DE"], ["fr", "it", "de"], ["fr", "de"]],
-      [["ja-JP-mac"], ["ja"], ["ja"]],
+      [["ja-JP-windows"], ["ja"], ["ja"]],
       [["en-Latn-GB", "en-Latn-IN"], ["en-IN", "en-GB"], ["en-GB", "en-IN"]],
     ],
     "should match on likely subtag": [
       [["en"], ["en-GB", "de", "en-US"], ["en-US", "en-GB"]],
       [["en"], ["en-Latn-GB", "de", "en-Latn-US"], ["en-Latn-US", "en-Latn-GB"]],
       [["fr"], ["fr-CA", "fr-FR"], ["fr-FR", "fr-CA"]],
       [["az-IR"], ["az-Latn", "az-Arab"], ["az-Arab"]],
       [["sr-RU"], ["sr-Cyrl", "sr-Latn"], ["sr-Latn"]],
       [["sr"], ["sr-Latn", "sr-Cyrl"], ["sr-Cyrl"]],
       [["zh-GB"], ["zh-Hans", "zh-Hant"], ["zh-Hant"]],
       [["sr", "ru"], ["sr-Latn", "ru"], ["ru"]],
       [["sr-RU"], ["sr-Latn-RO", "sr-Cyrl"], ["sr-Latn-RO"]],
     ],
     "should match likelySubtag region over other regions": [
       [["en-CA"], ["en-ZA", "en-GB", "en-US"], ["en-US", "en-ZA", "en-GB"]],
     ],
-    "should match on a requested locale as a range": [
-      [["en-*-US"], ["en-US"], ["en-US"]],
-      [["en-Latn-US-*"], ["en-Latn-US"], ["en-Latn-US"]],
-      [["en-*-US-*"], ["en-US"], ["en-US"]],
-    ],
     "should match cross-region": [
       [["en"], ["en-US"], ["en-US"]],
       [["en-US"], ["en-GB"], ["en-GB"]],
       [["en-Latn-US"], ["en-Latn-GB"], ["en-Latn-GB"]],
-      // This is a cross-region check, because the requested Locale
-      // is really lang: en, script: *, region: undefined
-      [["en-*"], ["en-US"], ["en-US"]],
     ],
     "should match cross-variant": [
-      [["en-US-mac"], ["en-US-win"], ["en-US-win"]],
+      [["en-US-linux"], ["en-US-windows"], ["en-US-windows"]],
     ],
     "should prioritize properly": [
       // exact match first
-      [["en-US"], ["en-US-mac", "en", "en-US"], ["en-US", "en", "en-US-mac"]],
+      [["en-US"], ["en-US-windows", "en", "en-US"], ["en-US", "en", "en-US-windows"]],
       // available as range second
       [["en-Latn-US"], ["en-GB", "en-US"], ["en-US", "en-GB"]],
       // likely subtags third
       [["en"], ["en-Cyrl-US", "en-Latn-US"], ["en-Latn-US"]],
       // variant range fourth
-      [["en-US-mac"], ["en-US-win", "en-GB-mac"], ["en-US-win", "en-GB-mac"]],
+      [["en-US-macos"], ["en-US-windows", "en-GB-macos"], ["en-US-windows", "en-GB-macos"]],
       // regional range fifth
-      [["en-US-mac"], ["en-GB-win"], ["en-GB-win"]],
-    ],
-    "should prioritize properly (extra tests)": [
+      [["en-US-macos"], ["en-GB-windows"], ["en-GB-windows"]],
       [["en-US"], ["en-GB", "en"], ["en", "en-GB"]],
+      [["fr-CA-macos", "de-DE"], ["de-DE", "fr-FR-windows"], ["fr-FR-windows", "de-DE"]],
     ],
     "should handle default locale properly": [
       [["fr"], ["de", "it"], []],
       [["fr"], ["de", "it"], "en-US", ["en-US"]],
       [["fr"], ["de", "en-US"], "en-US", ["en-US"]],
       [["fr", "de-DE"], ["de-DE", "fr-CA"], "en-US", ["fr-CA", "de-DE", "en-US"]],
     ],
     "should handle all matches on the 1st higher than any on the 2nd": [
-      [["fr-CA-mac", "de-DE"], ["de-DE", "fr-FR-win"], ["fr-FR-win", "de-DE"]],
+      [["fr-CA-macos", "de-DE"], ["de-DE", "fr-FR-windows"], ["fr-FR-windows", "de-DE"]],
     ],
     "should handle cases and underscores": [
       [["fr_FR"], ["fr-FR"], ["fr-FR"]],
-      [["fr_fr"], ["fr-fr"], ["fr-fr"]],
-      [["fr_Fr"], ["fr-fR"], ["fr-fR"]],
+      [["fr_fr"], ["fr-fr"], ["fr-FR"]],
+      [["fr_Fr"], ["fr-fR"], ["fr-FR"]],
       [["fr_lAtN_fr"], ["fr-Latn-FR"], ["fr-Latn-FR"]],
-      [["fr_FR"], ["fr_FR"], ["fr_FR"]],
-      [["fr-FR"], ["fr_FR"], ["fr_FR"]],
-      [["fr_Cyrl_FR_mac"], ["fr_Cyrl_fr-mac"], ["fr_Cyrl_fr-mac"]],
+      [["fr_FR"], ["fr_FR"], ["fr-FR"]],
+      [["fr-FR"], ["fr_FR"], ["fr-FR"]],
+      [["fr_Cyrl_FR_macos"], ["fr_Cyrl_fr-macos"], ["fr-Cyrl-FR-macos"]],
+    ],
+    "should handle mozilla specific 3-letter variants": [
+      [["ja-JP-mac", "de-DE"], ["ja-JP-mac", "de-DE"], ["ja-JP-mac", "de-DE"]],
     ],
     "should not crash on invalid input": [
       [null, ["fr-FR"], []],
       [[null], [], []],
       [[undefined], [], []],
       [[undefined], [null], []],
       [[undefined], [undefined], []],
       [[null], [null], null, null, []],