Bug 1428698 - Align intl::locale::Locale with BCP47. r?jfkthame draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Thu, 25 Jan 2018 14:50:32 -0800
changeset 747691 141a9cae473ed5b29f7e7774496864eff8bfdbb7
parent 747278 20b0802c3f00bd0f440194b869201ac94144732f
push id96986
push userbmo:gandalf@aviary.pl
push dateFri, 26 Jan 2018 17:57:56 +0000
reviewersjfkthame
bugs1428698
milestone60.0a1
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/mozILocaleService.idl
intl/locale/tests/gtest/TestMozLocale.cpp
intl/locale/tests/gtest/moz.build
intl/locale/tests/unit/test_localeService_negotiateLanguages.js
toolkit/mozapps/extensions/test/addons/test_bug397778/install.rdf
--- 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
@@ -227,33 +227,34 @@ public:
    *  Available - locales for which the data is available
    *  Supported - locales negotiated by the algorithm
    *
    * Additionally, if defaultLocale is provided, it adds it to the end of the
    * result list as a "last resort" locale.
    *
    * Strategy is one of the three strategies described at the top of this file.
    *
-   * The result list is ordered according to the order of the requested locales.
+   * The result list is canonicalized and ordered according to the order
+   * of the requested locales.
    *
    * (See mozILocaleService.idl for a JS-callable version of this.)
    */
   bool NegotiateLanguages(const nsTArray<nsCString>& aRequested,
                           const nsTArray<nsCString>& aAvailable,
                           const nsACString& aDefaultLocale,
                           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__ */
--- a/intl/locale/mozILocaleService.idl
+++ b/intl/locale/mozILocaleService.idl
@@ -114,17 +114,18 @@ interface mozILocaleService : nsISupport
    *  Available - locales for which the data is available
    *  Supported - locales negotiated by the algorithm
    *
    * Additionally, if defaultLocale is provided, it adds it to the end of the
    * result list as a "last resort" locale.
    *
    * Strategy is one of the three strategies described at the top of this file.
    *
-   * The result list is ordered according to the order of the requested locales.
+   * The result list is canonicalized and ordered according to the order
+   * of the requested locales.
    *
    * (See LocaleService.h for a more C++-friendly version of this.)
    */
   void negotiateLanguages([array, size_is(aRequestedCount)] in string aRequested,
                           [array, size_is(aAvailableCount)] in string aAvailable,
                           [optional] in string aDefaultLocale,
                           [optional] in long langNegStrategy,
                           [optional] in unsigned long aRequestedCount,
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, []],
--- a/toolkit/mozapps/extensions/test/addons/test_bug397778/install.rdf
+++ b/toolkit/mozapps/extensions/test/addons/test_bug397778/install.rdf
@@ -24,17 +24,17 @@
     
     <em:localized>
       <Description em:locale="de-DE">
         <em:name>de-DE Name</em:name>
       </Description>
     </em:localized>
     
     <em:localized>
-      <Description em:locale="ES-es">
+      <Description em:locale="es-ES">
         <em:name>es-ES Name</em:name>
         <em:description>es-ES Description</em:description>
       </Description>
     </em:localized>
     
     <em:localized>
       <Description em:locale="zh-TW">
         <em:name>zh-TW Name</em:name>