Bug 1570370 - Part 2: Port Intl.Locale to C++. r=jwalden
authorAndré Bargull <andre.bargull@gmail.com>
Fri, 11 Oct 2019 19:24:41 +0000
changeset 497332 4eca32dbe70f9c1d7803c2cb51a266842b4e77ab
parent 497331 5dad4aa34c7fede26690a0d4bbbd9b8614b09dfa
child 497333 64993f76caaf72ed630c44da86250d06dda52ccd
push id97818
push usercbrindusan@mozilla.com
push dateFri, 11 Oct 2019 21:39:29 +0000
treeherderautoland@614162937d31 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalden
bugs1570370
milestone71.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 1570370 - Part 2: Port Intl.Locale to C++. r=jwalden Differential Revision: https://phabricator.services.mozilla.com/D40068
js/public/Class.h
js/src/builtin/intl/LanguageTag.cpp
js/src/builtin/intl/LanguageTag.h
js/src/builtin/intl/Locale.cpp
js/src/builtin/intl/Locale.h
js/src/builtin/intl/NativeLocale.cpp
js/src/moz.build
js/src/vm/CommonPropertyNames.h
js/src/vm/GlobalObject.h
--- a/js/public/Class.h
+++ b/js/public/Class.h
@@ -745,17 +745,17 @@ static const uint32_t JSCLASS_FOREGROUND
 // with the following flags. Failure to use JSCLASS_GLOBAL_FLAGS was
 // previously allowed, but is now an ES5 violation and thus unsupported.
 //
 // JSCLASS_GLOBAL_APPLICATION_SLOTS is the number of slots reserved at
 // the beginning of every global object's slots for use by the
 // application.
 static const uint32_t JSCLASS_GLOBAL_APPLICATION_SLOTS = 5;
 static const uint32_t JSCLASS_GLOBAL_SLOT_COUNT =
-    JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 40;
+    JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 41;
 
 #define JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(n) \
   (JSCLASS_IS_GLOBAL |                     \
    JSCLASS_HAS_RESERVED_SLOTS(JSCLASS_GLOBAL_SLOT_COUNT + (n)))
 #define JSCLASS_GLOBAL_FLAGS JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(0)
 #define JSCLASS_HAS_GLOBAL_FLAG_AND_SLOTS(clasp) \
   (((clasp)->flags & JSCLASS_IS_GLOBAL) &&       \
    JSCLASS_RESERVED_SLOTS(clasp) >= JSCLASS_GLOBAL_SLOT_COUNT)
--- a/js/src/builtin/intl/LanguageTag.cpp
+++ b/js/src/builtin/intl/LanguageTag.cpp
@@ -51,43 +51,58 @@ bool IsStructurallyValidLanguageTag(
 
   // unicode_language_subtag = alpha{2,3} | alpha{5,8};
   size_t length = language.length();
   const CharT* str = language.begin().get();
   return ((2 <= length && length <= 3) || (5 <= length && length <= 8)) &&
          std::all_of(str, str + length, mozilla::IsAsciiLowercaseAlpha<CharT>);
 }
 
+template bool IsStructurallyValidLanguageTag(
+    const mozilla::Range<const Latin1Char>& language);
+template bool IsStructurallyValidLanguageTag(
+    const mozilla::Range<const char16_t>& language);
+
 template <typename CharT>
 bool IsStructurallyValidScriptTag(const mozilla::Range<const CharT>& script) {
   // Tell the analysis the |std::all_of| function can't GC.
   JS::AutoSuppressGCAnalysis nogc;
 
   // unicode_script_subtag = alpha{4} ;
   size_t length = script.length();
   const CharT* str = script.begin().get();
   return length == 4 && mozilla::IsAsciiUppercaseAlpha<CharT>(str[0]) &&
          std::all_of(str + 1, str + length,
                      mozilla::IsAsciiLowercaseAlpha<CharT>);
 }
 
+template bool IsStructurallyValidScriptTag(
+    const mozilla::Range<const Latin1Char>& script);
+template bool IsStructurallyValidScriptTag(
+    const mozilla::Range<const char16_t>& script);
+
 template <typename CharT>
 bool IsStructurallyValidRegionTag(const mozilla::Range<const CharT>& region) {
   // Tell the analysis the |std::all_of| function can't GC.
   JS::AutoSuppressGCAnalysis nogc;
 
   // unicode_region_subtag = (alpha{2} | digit{3}) ;
   size_t length = region.length();
   const CharT* str = region.begin().get();
   return (length == 2 && std::all_of(str, str + length,
                                      mozilla::IsAsciiUppercaseAlpha<CharT>)) ||
          (length == 3 &&
           std::all_of(str, str + length, mozilla::IsAsciiDigit<CharT>));
 }
 
+template bool IsStructurallyValidRegionTag(
+    const mozilla::Range<const Latin1Char>& region);
+template bool IsStructurallyValidRegionTag(
+    const mozilla::Range<const char16_t>& region);
+
 bool IsStructurallyValidVariantTag(const ConstCharRange& variant) {
   // unicode_variant_subtag = (alphanum{5,8} | digit alphanum{3}) ;
   auto isAsciiLowercaseAlphanumeric = [](char c) {
     return mozilla::IsAsciiLowercaseAlpha(c) || mozilla::IsAsciiDigit(c);
   };
   size_t length = variant.length();
   const char* str = variant.begin().get();
   return ((5 <= length && length <= 8) ||
@@ -1491,10 +1506,92 @@ bool LanguageTagParser::canParseUnicodeE
   while (ts.isUnicodeExtensionType(tok)) {
     tok = ts.nextToken();
   }
 
   // Return true if the complete input was successfully parsed.
   return tok.isNone();
 }
 
+bool ParseStandaloneLanguagTag(HandleLinearString str, LanguageSubtag& result) {
+  auto isLanguage = [](const auto* language, size_t length) {
+    // Tell the analysis the |std::all_of| function can't GC.
+    JS::AutoSuppressGCAnalysis nogc;
+
+    using T = std::remove_pointer_t<decltype(language)>;
+    return length >= 2 && length != 4 && length <= 8 &&
+           std::all_of(language, language + length, mozilla::IsAsciiAlpha<T>);
+  };
+
+  JS::AutoCheckCannotGC nogc;
+  if (str->hasLatin1Chars()) {
+    if (!isLanguage(str->latin1Chars(nogc), str->length())) {
+      return false;
+    }
+    result.set(str->latin1Range(nogc));
+  } else {
+    if (!isLanguage(str->twoByteChars(nogc), str->length())) {
+      return false;
+    }
+    result.set(str->twoByteRange(nogc));
+  }
+  result.toLowerCase();
+  return true;
+}
+
+bool ParseStandaloneScriptTag(HandleLinearString str, ScriptSubtag& result) {
+  auto isScript = [](const auto* script, size_t length) {
+    // Tell the analysis the |std::all_of| function can't GC.
+    JS::AutoSuppressGCAnalysis nogc;
+
+    using T = std::remove_pointer_t<decltype(script)>;
+    return length == ScriptLength &&
+           std::all_of(script, script + ScriptLength, mozilla::IsAsciiAlpha<T>);
+  };
+
+  JS::AutoCheckCannotGC nogc;
+  if (str->hasLatin1Chars()) {
+    if (!isScript(str->latin1Chars(nogc), str->length())) {
+      return false;
+    }
+    result.set(str->latin1Range(nogc));
+  } else {
+    if (!isScript(str->twoByteChars(nogc), str->length())) {
+      return false;
+    }
+    result.set(str->twoByteRange(nogc));
+  }
+  result.toTitleCase();
+  return true;
+}
+
+bool ParseStandaloneRegionTag(HandleLinearString str, RegionSubtag& result) {
+  auto isRegion = [](const auto* region, size_t length) {
+    // Tell the analysis the |std::all_of| function can't GC.
+    JS::AutoSuppressGCAnalysis nogc;
+
+    using T = std::remove_pointer_t<decltype(region)>;
+    return (length == AlphaRegionLength &&
+            std::all_of(region, region + AlphaRegionLength,
+                        mozilla::IsAsciiAlpha<T>)) ||
+           (length == DigitRegionLength &&
+            std::all_of(region, region + DigitRegionLength,
+                        mozilla::IsAsciiDigit<T>));
+  };
+
+  JS::AutoCheckCannotGC nogc;
+  if (str->hasLatin1Chars()) {
+    if (!isRegion(str->latin1Chars(nogc), str->length())) {
+      return false;
+    }
+    result.set(str->latin1Range(nogc));
+  } else {
+    if (!isRegion(str->twoByteChars(nogc), str->length())) {
+      return false;
+    }
+    result.set(str->twoByteRange(nogc));
+  }
+  result.toUpperCase();
+  return true;
+}
+
 }  // namespace intl
 }  // namespace js
--- a/js/src/builtin/intl/LanguageTag.h
+++ b/js/src/builtin/intl/LanguageTag.h
@@ -19,16 +19,17 @@
 #include <stddef.h>
 #include <stdint.h>
 #include <string.h>
 #include <utility>
 
 #include "js/AllocPolicy.h"
 #include "js/GCAPI.h"
 #include "js/Result.h"
+#include "js/RootingAPI.h"
 #include "js/Utility.h"
 #include "js/Vector.h"
 
 struct JSContext;
 class JSLinearString;
 class JSString;
 
 namespace js {
@@ -677,13 +678,37 @@ class MOZ_STACK_CLASS LanguageTagParser 
   static bool canParseUnicodeExtension(mozilla::Range<const char> extension);
 
   // Return true iff |unicodeType| can be parsed as a Unicode extension type.
   static bool canParseUnicodeExtensionType(JSLinearString* unicodeType);
 };
 
 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(LanguageTagParser::TokenKind)
 
+/**
+ * Parse a string as a standalone |language| tag. If |str| is a standalone
+ * language tag, store it in case-normalized form in |result| and return true.
+ * Otherwise return false.
+ */
+MOZ_MUST_USE bool ParseStandaloneLanguagTag(JS::Handle<JSLinearString*> str,
+                                            LanguageSubtag& result);
+
+/**
+ * Parse a string as a standalone |script| tag. If |str| is a standalone script
+ * tag, store it in case-normalized form in |result| and return true. Otherwise
+ * return false.
+ */
+MOZ_MUST_USE bool ParseStandaloneScriptTag(JS::Handle<JSLinearString*> str,
+                                           ScriptSubtag& result);
+
+/**
+ * Parse a string as a standalone |region| tag. If |str| is a standalone region
+ * tag, store it in case-normalized form in |result| and return true. Otherwise
+ * return false.
+ */
+MOZ_MUST_USE bool ParseStandaloneRegionTag(JS::Handle<JSLinearString*> str,
+                                           RegionSubtag& result);
+
 }  // namespace intl
 
 }  // namespace js
 
 #endif /* builtin_intl_LanguageTag_h */
--- a/js/src/builtin/intl/Locale.cpp
+++ b/js/src/builtin/intl/Locale.cpp
@@ -166,16 +166,35 @@ JSObject* js::CreateLocalePrototype(JSCo
   }
 
   JSObject* localeProto = CreateLocalePrototype(cx, intl, global);
   if (!localeProto) {
     return false;
   }
 
   global->setReservedSlot(LOCALE_PROTO, ObjectValue(*localeProto));
+
+  {
+    const Value& proto = global->getReservedSlot(NATIVE_LOCALE_PROTO);
+    if (!proto.isUndefined()) {
+      MOZ_ASSERT(proto.isObject());
+      JS_ReportErrorASCII(
+          cx,
+          "the Locale constructor can't be added multiple times in the"
+          "same global");
+      return false;
+    }
+  }
+
+  localeProto = CreateNativeLocalePrototype(cx, intl, global);
+  if (!localeProto) {
+    return false;
+  }
+
+  global->setReservedSlot(NATIVE_LOCALE_PROTO, ObjectValue(*localeProto));
   return true;
 }
 
 bool js::AddLocaleConstructor(JSContext* cx, JS::Handle<JSObject*> intl) {
   return GlobalObject::addLocaleConstructor(cx, intl);
 }
 
 bool js::intl_CreateUninitializedLocale(JSContext* cx, unsigned argc,
--- a/js/src/builtin/intl/Locale.h
+++ b/js/src/builtin/intl/Locale.h
@@ -1,10 +1,10 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
- * vim: set ts=8 sts=4 et sw=4 tw=99:
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
  * 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 builtin_intl_Locale_h
 #define builtin_intl_Locale_h
 
 #include <stdint.h>
@@ -28,16 +28,48 @@ class LocaleObject : public NativeObject
                 "INTERNALS_SLOT must match self-hosting define for internals "
                 "object slot");
 };
 
 extern JSObject* CreateLocalePrototype(JSContext* cx,
                                        JS::Handle<JSObject*> Intl,
                                        JS::Handle<GlobalObject*> global);
 
+class NativeLocaleObject : public NativeObject {
+ public:
+  static const JSClass class_;
+
+  static constexpr uint32_t LANGUAGE_TAG_SLOT = 0;
+  static constexpr uint32_t BASENAME_SLOT = 1;
+  static constexpr uint32_t UNICODE_EXTENSION_SLOT = 2;
+  static constexpr uint32_t SLOT_COUNT = 3;
+
+  /**
+   * Returns the complete language tag, including any extensions and privateuse
+   * subtags.
+   */
+  JSString* languageTag() const {
+    return getFixedSlot(LANGUAGE_TAG_SLOT).toString();
+  }
+
+  /**
+   * Returns the basename subtags, i.e. excluding any extensions and privateuse
+   * subtags.
+   */
+  JSString* baseName() const { return getFixedSlot(BASENAME_SLOT).toString(); }
+
+  const Value& unicodeExtension() const {
+    return getFixedSlot(UNICODE_EXTENSION_SLOT);
+  }
+};
+
+extern JSObject* CreateNativeLocalePrototype(JSContext* cx,
+                                             JS::Handle<JSObject*> Intl,
+                                             JS::Handle<GlobalObject*> global);
+
 /**
  * Creates an uninitialized Intl.Locale object.
  */
 extern MOZ_MUST_USE bool intl_CreateUninitializedLocale(JSContext* cx,
                                                         unsigned argc,
                                                         Value* vp);
 
 /**
new file mode 100644
--- /dev/null
+++ b/js/src/builtin/intl/NativeLocale.cpp
@@ -0,0 +1,1261 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * 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/. */
+
+/* Intl.Locale implementation. */
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Casting.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Range.h"
+#include "mozilla/TextUtils.h"
+
+#include <algorithm>
+#include <iterator>
+#include <string>
+#include <string.h>
+#include <utility>
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/LanguageTag.h"
+#include "builtin/intl/Locale.h"
+#include "gc/Rooting.h"
+#include "js/Conversions.h"
+#include "js/TypeDecls.h"
+#include "js/Wrapper.h"
+#include "util/StringBuffer.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/StringType.h"
+
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+using namespace js::intl::LanguageTagLimits;
+
+using intl::LanguageTag;
+using intl::LanguageTagParser;
+
+const JSClass NativeLocaleObject::class_ = {
+    js_Object_str,
+    JSCLASS_HAS_RESERVED_SLOTS(NativeLocaleObject::SLOT_COUNT),
+};
+
+static inline bool IsLocale(HandleValue v) {
+  return v.isObject() && v.toObject().is<NativeLocaleObject>();
+}
+
+// Return the length of the base-name subtags.
+static size_t BaseNameLength(const LanguageTag& tag) {
+  size_t baseNameLength = tag.language().length();
+  if (tag.script().length() > 0) {
+    baseNameLength += 1 + tag.script().length();
+  }
+  if (tag.region().length() > 0) {
+    baseNameLength += 1 + tag.region().length();
+  }
+  for (const auto& variant : tag.variants()) {
+    baseNameLength += 1 + strlen(variant.get());
+  }
+  return baseNameLength;
+}
+
+struct IndexAndLength {
+  size_t index;
+  size_t length;
+
+  IndexAndLength(size_t index, size_t length) : index(index), length(length){};
+
+  template <typename T>
+  mozilla::Range<const T> rangeOf(const T* ptr) const {
+    return {ptr + index, length};
+  }
+};
+
+// Compute the Unicode extension's index and length in the extension subtag.
+static mozilla::Maybe<IndexAndLength> UnicodeExtensionPosition(
+    const LanguageTag& tag) {
+  size_t index = 0;
+  for (const auto& extension : tag.extensions()) {
+    size_t extensionLength = strlen(extension.get());
+    if (extension[0] == 'u') {
+      return mozilla::Some(IndexAndLength{index, extensionLength});
+    }
+
+    // Add +1 to skip over the preceding separator.
+    index += 1 + extensionLength;
+  }
+  return mozilla::Nothing();
+}
+
+static NativeLocaleObject* CreateLocaleNativeObject(JSContext* cx,
+                                                    HandleObject prototype,
+                                                    const LanguageTag& tag) {
+  RootedObject proto(cx, prototype);
+  if (!proto) {
+    proto = GlobalObject::getOrCreateLocaleNativePrototype(cx, cx->global());
+    if (!proto) {
+      return nullptr;
+    }
+  }
+
+  JSStringBuilder sb(cx);
+  if (!tag.appendTo(cx, sb)) {
+    return nullptr;
+  }
+
+  RootedString tagStr(cx, sb.finishString());
+  if (!tagStr) {
+    return nullptr;
+  }
+
+  size_t baseNameLength = BaseNameLength(tag);
+
+  RootedString baseName(cx, NewDependentString(cx, tagStr, 0, baseNameLength));
+  if (!baseName) {
+    return nullptr;
+  }
+
+  RootedValue unicodeExtension(cx, UndefinedValue());
+  if (auto result = UnicodeExtensionPosition(tag)) {
+    JSString* str = NewDependentString(
+        cx, tagStr, baseNameLength + 1 + result->index, result->length);
+    if (!str) {
+      return nullptr;
+    }
+
+    unicodeExtension.setString(str);
+  }
+
+  auto* locale = NewObjectWithGivenProto<NativeLocaleObject>(cx, proto);
+  if (!locale) {
+    return nullptr;
+  }
+
+  locale->setFixedSlot(NativeLocaleObject::LANGUAGE_TAG_SLOT,
+                       StringValue(tagStr));
+  locale->setFixedSlot(NativeLocaleObject::BASENAME_SLOT,
+                       StringValue(baseName));
+  locale->setFixedSlot(NativeLocaleObject::UNICODE_EXTENSION_SLOT,
+                       unicodeExtension);
+
+  return locale;
+}
+
+static inline bool IsValidUnicodeExtensionValue(JSLinearString* linear) {
+  return linear->length() > 0 &&
+         LanguageTagParser::canParseUnicodeExtensionType(linear);
+}
+
+/** Iterate through (sep keyword) in a valid, lowercased Unicode extension. */
+template <typename CharT>
+class SepKeywordIterator {
+  const CharT* iter_;
+  const CharT* const end_;
+
+ public:
+  SepKeywordIterator(const CharT* unicodeExtensionBegin,
+                     const CharT* unicodeExtensionEnd)
+      : iter_(unicodeExtensionBegin), end_(unicodeExtensionEnd) {}
+
+  /**
+   * Return (sep keyword) in the Unicode locale extension from begin to end.
+   * The first call after all (sep keyword) are consumed returns |nullptr|; no
+   * further calls are allowed.
+   */
+  const CharT* next() {
+    MOZ_ASSERT(iter_ != nullptr,
+               "can't call next() once it's returned nullptr");
+
+    constexpr size_t SepKeyLength = 1 + UnicodeKeyLength;  // "-co"/"-nu"/etc.
+
+    MOZ_ASSERT(iter_ + SepKeyLength <= end_,
+               "overall Unicode locale extension or non-leading subtags must "
+               "be at least key-sized");
+
+    MOZ_ASSERT((iter_[0] == 'u' && iter_[1] == '-') || iter_[0] == '-');
+
+    while (true) {
+      // Skip past '-' so |std::char_traits::find| makes progress. Skipping
+      // 'u' is harmless -- skip or not, |find| returns the first '-'.
+      iter_++;
+
+      // Find the next separator.
+      iter_ = std::char_traits<CharT>::find(
+          iter_, mozilla::PointerRangeSize(iter_, end_), CharT('-'));
+      if (!iter_) {
+        return nullptr;
+      }
+
+      MOZ_ASSERT(iter_ + SepKeyLength <= end_,
+                 "non-leading subtags in a Unicode locale extension are all "
+                 "at least as long as a key");
+
+      if (iter_ + SepKeyLength == end_ ||  // key is terminal subtag
+          iter_[SepKeyLength] == '-') {    // key is followed by more subtags
+        break;
+      }
+    }
+
+    MOZ_ASSERT(iter_[0] == '-');
+    MOZ_ASSERT(mozilla::IsAsciiLowercaseAlpha(iter_[1]) ||
+               mozilla::IsAsciiDigit(iter_[1]));
+    MOZ_ASSERT(mozilla::IsAsciiLowercaseAlpha(iter_[2]));
+    MOZ_ASSERT_IF(iter_ + SepKeyLength < end_, iter_[SepKeyLength] == '-');
+    return iter_;
+  }
+};
+
+/**
+ * 9.2.10 GetOption ( options, property, type, values, fallback )
+ *
+ * If the requested property is present and not-undefined, set the result string
+ * to |ToString(value)|. Otherwise set the result string to nullptr.
+ */
+static bool GetStringOption(JSContext* cx, HandleObject options,
+                            HandlePropertyName name,
+                            MutableHandle<JSLinearString*> string) {
+  // Step 1.
+  RootedValue option(cx);
+  if (!GetProperty(cx, options, options, name, &option)) {
+    return false;
+  }
+
+  // Step 2.
+  JSLinearString* linear = nullptr;
+  if (!option.isUndefined()) {
+    // Steps 2.a-b, 2.d (not applicable).
+
+    // Steps 2.c, 2.e.
+    JSString* str = ToString(cx, option);
+    if (!str) {
+      return false;
+    }
+    linear = str->ensureLinear(cx);
+    if (!linear) {
+      return false;
+    }
+  }
+
+  // Step 3.
+  string.set(linear);
+  return true;
+}
+
+/**
+ * 9.2.10 GetOption ( options, property, type, values, fallback )
+ *
+ * If the requested property is present and not-undefined, set the result string
+ * to |ToString(ToBoolean(value))|. Otherwise set the result string to nullptr.
+ */
+static bool GetBooleanOption(JSContext* cx, HandleObject options,
+                             HandlePropertyName name,
+                             MutableHandle<JSLinearString*> string) {
+  // Step 1.
+  RootedValue option(cx);
+  if (!GetProperty(cx, options, options, name, &option)) {
+    return false;
+  }
+
+  // Step 2.
+  JSLinearString* linear = nullptr;
+  if (!option.isUndefined()) {
+    // Steps 2.a, 2.c-d (not applicable).
+
+    // Steps 2.c, 2.e.
+    JSString* str = BooleanToString(cx, ToBoolean(option));
+    MOZ_ALWAYS_TRUE(linear = str->ensureLinear(cx));
+  }
+
+  // Step 3.
+  string.set(linear);
+  return true;
+}
+
+/**
+ * ApplyOptionsToTag ( tag, options )
+ */
+static bool ApplyOptionsToTag(JSContext* cx, LanguageTag& tag,
+                              HandleObject options) {
+  // Steps 1-2 (Already performed in caller).
+
+  RootedLinearString option(cx);
+
+  // Step 3.
+  if (!GetStringOption(cx, options, cx->names().language, &option)) {
+    return false;
+  }
+
+  // Step 4.
+  intl::LanguageSubtag language;
+  if (option && !intl::ParseStandaloneLanguagTag(option, language)) {
+    if (UniqueChars str = StringToNewUTF8CharsZ(cx, *option)) {
+      JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr,
+                               JSMSG_INVALID_OPTION_VALUE, "language",
+                               str.get());
+    }
+    return false;
+  }
+
+  // Step 5.
+  if (!GetStringOption(cx, options, cx->names().script, &option)) {
+    return false;
+  }
+
+  // Step 6.
+  intl::ScriptSubtag script;
+  if (option && !intl::ParseStandaloneScriptTag(option, script)) {
+    if (UniqueChars str = StringToNewUTF8CharsZ(cx, *option)) {
+      JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr,
+                               JSMSG_INVALID_OPTION_VALUE, "script", str.get());
+    }
+    return false;
+  }
+
+  // Step 7.
+  if (!GetStringOption(cx, options, cx->names().region, &option)) {
+    return false;
+  }
+
+  // Step 8.
+  intl::RegionSubtag region;
+  if (option && !intl::ParseStandaloneRegionTag(option, region)) {
+    if (UniqueChars str = StringToNewUTF8CharsZ(cx, *option)) {
+      JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr,
+                               JSMSG_INVALID_OPTION_VALUE, "region", str.get());
+    }
+    return false;
+  }
+
+  // Step 9 (Already performed in caller).
+
+  // Skip steps 10-13 when no subtags were modified.
+  if (language.length() > 0 || script.length() > 0 || region.length() > 0) {
+    // Step 10.
+    if (language.length() > 0) {
+      tag.setLanguage(language);
+    }
+
+    // Step 11.
+    if (script.length() > 0) {
+      tag.setScript(script);
+    }
+
+    // Step 12.
+    if (region.length() > 0) {
+      tag.setRegion(region);
+    }
+
+    // Step 13.
+    // Optimized to only canonicalize the base-name subtags. All other
+    // canonicalization steps will happen later.
+    if (!tag.canonicalizeBaseName(cx)) {
+      return true;
+    }
+  }
+
+  return true;
+}
+
+/**
+ * ApplyUnicodeExtensionToTag( tag, options, relevantExtensionKeys )
+ */
+static bool ApplyUnicodeExtensionToTag(JSContext* cx, LanguageTag& tag,
+                                       HandleLinearString calendar,
+                                       HandleLinearString collation,
+                                       HandleLinearString hourCycle,
+                                       HandleLinearString caseFirst,
+                                       HandleLinearString numeric,
+                                       HandleLinearString numberingSystem) {
+  // If no Unicode extensions were present in the options object, we can skip
+  // everything below and directly return.
+  if (!calendar && !collation && !caseFirst && !hourCycle && !numeric &&
+      !numberingSystem) {
+    return true;
+  }
+
+  Vector<char, 32> newExtension(cx);
+  if (!newExtension.append('u')) {
+    return false;
+  }
+
+  // Check if there's an existing Unicode extension subtag. (The extension
+  // subtags aren't necessarily sorted, so we can't use binary search here.)
+  const UniqueChars* existingUnicodeExtension =
+      std::find_if(tag.extensions().begin(), tag.extensions().end(),
+                   [](const auto& extension) { return extension[0] == 'u'; });
+
+  const char* unicodeExtensionEnd = nullptr;
+  const char* unicodeExtensionKeywords = nullptr;
+  if (existingUnicodeExtension != tag.extensions().end()) {
+    const char* unicodeExtension = existingUnicodeExtension->get();
+    unicodeExtensionEnd = unicodeExtension + strlen(unicodeExtension);
+
+    SepKeywordIterator<char> iter(unicodeExtension, unicodeExtensionEnd);
+
+    // Find the start of the first keyword.
+    unicodeExtensionKeywords = iter.next();
+
+    // Copy any attributes present before the first keyword.
+    const char* attributesEnd = unicodeExtensionKeywords
+                                    ? unicodeExtensionKeywords
+                                    : unicodeExtensionEnd;
+    if (!newExtension.append(unicodeExtension + 1, attributesEnd)) {
+      return false;
+    }
+  }
+
+  using UnicodeKeyWithSeparator = const char(&)[UnicodeKeyLength + 3];
+
+  auto appendKeyword = [&newExtension](UnicodeKeyWithSeparator key,
+                                       JSLinearString* value) {
+    if (!newExtension.append(key, UnicodeKeyLength + 2)) {
+      return false;
+    }
+
+    JS::AutoCheckCannotGC nogc;
+    return value->hasLatin1Chars()
+               ? newExtension.append(value->latin1Chars(nogc), value->length())
+               : newExtension.append(value->twoByteChars(nogc),
+                                     value->length());
+  };
+
+  // Append the new keywords before any existing keywords. That way any previous
+  // keyword with the same key is detected as a duplicate when canonicalizing
+  // the Unicode extension subtag and gets discarded.
+
+  size_t startNewKeywords = newExtension.length();
+
+  if (calendar) {
+    if (!appendKeyword("-ca-", calendar)) {
+      return false;
+    }
+  }
+  if (collation) {
+    if (!appendKeyword("-co-", collation)) {
+      return false;
+    }
+  }
+  if (hourCycle) {
+    if (!appendKeyword("-hc-", hourCycle)) {
+      return false;
+    }
+  }
+  if (caseFirst) {
+    if (!appendKeyword("-kf-", caseFirst)) {
+      return false;
+    }
+  }
+  if (numeric) {
+    if (!appendKeyword("-kn-", numeric)) {
+      return false;
+    }
+  }
+  if (numberingSystem) {
+    if (!appendKeyword("-nu-", numberingSystem)) {
+      return false;
+    }
+  }
+
+  // Normalize the case of the new keywords.
+  std::transform(newExtension.begin() + startNewKeywords, newExtension.end(),
+                 newExtension.begin() + startNewKeywords, [](char c) {
+                   return mozilla::IsAsciiUppercaseAlpha(c) ? (c | 0x20) : c;
+                 });
+
+  // Append the remaining keywords from the previous Unicode extension subtag.
+  if (unicodeExtensionKeywords) {
+    if (!newExtension.append(unicodeExtensionKeywords, unicodeExtensionEnd)) {
+      return false;
+    }
+  }
+
+  // Null-terminate the new Unicode extension string.
+  if (!newExtension.append('\0')) {
+    return false;
+  }
+
+  // Insert the new Unicode extension string into the language tag.
+  UniqueChars newExtensionChars(newExtension.extractOrCopyRawBuffer());
+  if (!newExtensionChars) {
+    return false;
+  }
+  return tag.setUnicodeExtension(std::move(newExtensionChars));
+}
+
+/**
+ * Intl.Locale( tag[, options] )
+ */
+static bool NativeLocale(JSContext* cx, unsigned argc, Value* vp) {
+  CallArgs args = CallArgsFromVp(argc, vp);
+
+  // Step 1.
+  if (!ThrowIfNotConstructing(cx, args, "Intl.Locale")) {
+    return false;
+  }
+
+  // Steps 2-6 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+  RootedObject proto(cx);
+  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Null, &proto)) {
+    return false;
+  }
+
+  // Step 7.
+  if (args.length() == 0 || (!args[0].isString() && !args[0].isObject())) {
+    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                              JSMSG_INVALID_LOCALES_ELEMENT);
+    return false;
+  }
+
+  // Steps 8-9.
+  RootedString tagStr(cx);
+  if (args[0].isObject()) {
+    JSObject* obj = &args[0].toObject();
+    if (obj->is<NativeLocaleObject>()) {
+      tagStr = obj->as<NativeLocaleObject>().languageTag();
+    } else {
+      JSObject* unwrapped = CheckedUnwrapStatic(obj);
+      if (!unwrapped) {
+        ReportAccessDenied(cx);
+        return false;
+      }
+
+      if (unwrapped->is<NativeLocaleObject>()) {
+        tagStr = unwrapped->as<NativeLocaleObject>().languageTag();
+        if (!cx->compartment()->wrap(cx, &tagStr)) {
+          return false;
+        }
+      } else {
+        tagStr = ToString(cx, args[0]);
+        if (!tagStr) {
+          return false;
+        }
+      }
+    }
+  } else {
+    tagStr = args[0].toString();
+  }
+
+  RootedLinearString tagLinearStr(cx, tagStr->ensureLinear(cx));
+  if (!tagLinearStr) {
+    return false;
+  }
+
+  // ApplyOptionsToTag, steps 2 and 9.
+  LanguageTag tag(cx);
+  if (!LanguageTagParser::parse(cx, tagLinearStr, tag)) {
+    return false;
+  }
+
+  if (!tag.canonicalizeBaseName(cx)) {
+    return false;
+  }
+
+  // Steps 10-11.
+  if (args.hasDefined(1)) {
+    RootedObject options(cx, ToObject(cx, args[1]));
+    if (!options) {
+      return false;
+    }
+
+    // Step 12.
+    if (!ApplyOptionsToTag(cx, tag, options)) {
+      return false;
+    }
+
+    // Step 13 (not applicable).
+
+    // Steps 14, 16.
+    RootedLinearString calendar(cx);
+    if (!GetStringOption(cx, options, cx->names().calendar, &calendar)) {
+      return false;
+    }
+
+    // Step 15.
+    if (calendar) {
+      if (!IsValidUnicodeExtensionValue(calendar)) {
+        if (UniqueChars str = StringToNewUTF8CharsZ(cx, *calendar)) {
+          JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr,
+                                   JSMSG_INVALID_OPTION_VALUE, "calendar",
+                                   str.get());
+        }
+        return false;
+      }
+    }
+
+    // Steps 17, 19.
+    RootedLinearString collation(cx);
+    if (!GetStringOption(cx, options, cx->names().collation, &collation)) {
+      return false;
+    }
+
+    // Step 18.
+    if (collation) {
+      if (!IsValidUnicodeExtensionValue(collation)) {
+        if (UniqueChars str = StringToNewUTF8CharsZ(cx, *collation)) {
+          JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr,
+                                   JSMSG_INVALID_OPTION_VALUE, "collation",
+                                   str.get());
+        }
+        return false;
+      }
+    }
+
+    // Steps 20-21.
+    RootedLinearString hourCycle(cx);
+    if (!GetStringOption(cx, options, cx->names().hourCycle, &hourCycle)) {
+      return false;
+    }
+
+    if (hourCycle) {
+      if (!StringEqualsLiteral(hourCycle, "h11") &&
+          !StringEqualsLiteral(hourCycle, "h12") &&
+          !StringEqualsLiteral(hourCycle, "h23") &&
+          !StringEqualsLiteral(hourCycle, "h24")) {
+        if (UniqueChars str = StringToNewUTF8CharsZ(cx, *hourCycle)) {
+          JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr,
+                                   JSMSG_INVALID_OPTION_VALUE, "hourCycle",
+                                   str.get());
+        }
+        return false;
+      }
+    }
+
+    // Steps 22-23.
+    RootedLinearString caseFirst(cx);
+    if (!GetStringOption(cx, options, cx->names().caseFirst, &caseFirst)) {
+      return false;
+    }
+
+    if (caseFirst) {
+      if (!StringEqualsLiteral(caseFirst, "upper") &&
+          !StringEqualsLiteral(caseFirst, "lower") &&
+          !StringEqualsLiteral(caseFirst, "false")) {
+        if (UniqueChars str = StringToNewUTF8CharsZ(cx, *caseFirst)) {
+          JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr,
+                                   JSMSG_INVALID_OPTION_VALUE, "caseFirst",
+                                   str.get());
+        }
+        return false;
+      }
+    }
+
+    // Steps 24-26.
+    RootedLinearString numeric(cx);
+    if (!GetBooleanOption(cx, options, cx->names().numeric, &numeric)) {
+      return false;
+    }
+
+    // Steps 27, 29.
+    RootedLinearString numberingSystem(cx);
+    if (!GetStringOption(cx, options, cx->names().numberingSystem,
+                         &numberingSystem)) {
+      return false;
+    }
+
+    // Step 28.
+    if (numberingSystem) {
+      if (!IsValidUnicodeExtensionValue(numberingSystem)) {
+        if (UniqueChars str = StringToNewUTF8CharsZ(cx, *numberingSystem)) {
+          JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr,
+                                   JSMSG_INVALID_OPTION_VALUE,
+                                   "numberingSystem", str.get());
+        }
+        return false;
+      }
+    }
+
+    // Step 30.
+    if (!ApplyUnicodeExtensionToTag(cx, tag, calendar, collation, hourCycle,
+                                    caseFirst, numeric, numberingSystem)) {
+      return false;
+    }
+  }
+
+  // ApplyOptionsToTag, steps 9 and 13.
+  // ApplyUnicodeExtensionToTag, step 8.
+  if (!tag.canonicalizeExtensions(
+          cx, LanguageTag::UnicodeExtensionCanonicalForm::Yes)) {
+    return false;
+  }
+
+  // Steps 6, 31-37.
+  JSObject* obj = CreateLocaleNativeObject(cx, proto, tag);
+  if (!obj) {
+    return false;
+  }
+
+  // Step 38.
+  args.rval().setObject(*obj);
+  return true;
+}
+
+using UnicodeKey = const char (&)[UnicodeKeyLength + 1];
+
+// Returns the tuple [index, length] of the `type` in the `keyword` in Unicode
+// locale extension |extension| that has |key| as its `key`. If `keyword` lacks
+// a type, the returned |index| will be where `type` would have been, and
+// |length| will be set to zero.
+template <typename CharT>
+static mozilla::Maybe<IndexAndLength> FindUnicodeExtensionType(
+    const CharT* extension, size_t length, UnicodeKey key) {
+  MOZ_ASSERT(extension[0] == 'u');
+  MOZ_ASSERT(extension[1] == '-');
+
+  const CharT* end = extension + length;
+
+  SepKeywordIterator<CharT> iter(extension, end);
+
+  // Search all keywords until a match was found.
+  const CharT* beginKey;
+  while (true) {
+    beginKey = iter.next();
+    if (!beginKey) {
+      return mozilla::Nothing();
+    }
+
+    // Add +1 to skip over the separator preceding the keyword.
+    MOZ_ASSERT(beginKey[0] == '-');
+    beginKey++;
+
+    // Exit the loop on the first match.
+    if (std::equal(beginKey, beginKey + UnicodeKeyLength, key)) {
+      break;
+    }
+  }
+
+  // Skip over the key.
+  const CharT* beginType = beginKey + UnicodeKeyLength;
+
+  // Find the start of the next keyword.
+  const CharT* endType = iter.next();
+
+  // No further keyword present, the current keyword ends the Unicode extension.
+  if (!endType) {
+    endType = end;
+  }
+
+  // If the keyword has a type, skip over the separator preceding the type.
+  if (beginType != endType) {
+    MOZ_ASSERT(beginType[0] == '-');
+    beginType++;
+  }
+  return mozilla::Some(IndexAndLength{size_t(beginType - extension),
+                                      size_t(endType - beginType)});
+}
+
+static inline auto FindUnicodeExtensionType(JSLinearString* unicodeExtension,
+                                            UnicodeKey key) {
+  JS::AutoCheckCannotGC nogc;
+  return unicodeExtension->hasLatin1Chars()
+             ? FindUnicodeExtensionType(unicodeExtension->latin1Chars(nogc),
+                                        unicodeExtension->length(), key)
+             : FindUnicodeExtensionType(unicodeExtension->twoByteChars(nogc),
+                                        unicodeExtension->length(), key);
+}
+
+// Return the sequence of types for the Unicode extension keyword specified by
+// key or undefined when the keyword isn't present.
+static bool GetUnicodeExtension(JSContext* cx, NativeLocaleObject* locale,
+                                UnicodeKey key, MutableHandleValue value) {
+  // Return undefined when no Unicode extension subtag is present.
+  const Value& unicodeExtensionValue = locale->unicodeExtension();
+  if (unicodeExtensionValue.isUndefined()) {
+    value.setUndefined();
+    return true;
+  }
+
+  JSLinearString* unicodeExtension =
+      unicodeExtensionValue.toString()->ensureLinear(cx);
+  if (!unicodeExtension) {
+    return false;
+  }
+
+  // Find the type of the requested key in the Unicode extension subtag.
+  auto result = FindUnicodeExtensionType(unicodeExtension, key);
+
+  // Return undefined if the requested key isn't present in the extension.
+  if (!result) {
+    value.setUndefined();
+    return true;
+  }
+
+  size_t index = result->index;
+  size_t length = result->length;
+
+  // Otherwise return the type value of the found keyword.
+  JSString* str = NewDependentString(cx, unicodeExtension, index, length);
+  if (!str) {
+    return false;
+  }
+  value.setString(str);
+  return true;
+}
+
+struct BaseNamePartsResult {
+  IndexAndLength language;
+  mozilla::Maybe<IndexAndLength> script;
+  mozilla::Maybe<IndexAndLength> region;
+};
+
+// Returns [language-length, script-index, region-index, region-length].
+template <typename CharT>
+static BaseNamePartsResult BaseNameParts(const CharT* baseName, size_t length) {
+  size_t languageLength;
+  size_t scriptIndex = 0;
+  size_t regionIndex = 0;
+  size_t regionLength = 0;
+
+  // Search the first separator to find the end of the language subtag.
+  if (const CharT* sep = std::char_traits<CharT>::find(baseName, length, '-')) {
+    languageLength = sep - baseName;
+
+    // Add +1 to skip over the separator character.
+    size_t nextSubtag = languageLength + 1;
+
+    // Script subtags are always four characters long, but take care for a four
+    // character long variant subtag. These start with a digit.
+    if ((nextSubtag + ScriptLength == length ||
+         (nextSubtag + ScriptLength < length &&
+          baseName[nextSubtag + ScriptLength] == '-')) &&
+        mozilla::IsAsciiAlpha(baseName[nextSubtag])) {
+      scriptIndex = nextSubtag;
+      nextSubtag = scriptIndex + ScriptLength + 1;
+    }
+
+    // Region subtags can be either two or three characters long.
+    if (nextSubtag < length) {
+      for (size_t rlen : {AlphaRegionLength, DigitRegionLength}) {
+        MOZ_ASSERT(nextSubtag + rlen <= length);
+        if (nextSubtag + rlen == length || baseName[nextSubtag + rlen] == '-') {
+          regionIndex = nextSubtag;
+          regionLength = rlen;
+          break;
+        }
+      }
+    }
+  } else {
+    // No separator found, the base-name consists of just a language subtag.
+    languageLength = length;
+  }
+
+  IndexAndLength language{0, languageLength};
+  MOZ_ASSERT(intl::IsStructurallyValidLanguageTag(language.rangeOf(baseName)));
+
+  mozilla::Maybe<IndexAndLength> script{};
+  if (scriptIndex) {
+    script.emplace(scriptIndex, ScriptLength);
+    MOZ_ASSERT(intl::IsStructurallyValidScriptTag(script->rangeOf(baseName)));
+  }
+
+  mozilla::Maybe<IndexAndLength> region{};
+  if (regionIndex) {
+    region.emplace(regionIndex, regionLength);
+    MOZ_ASSERT(intl::IsStructurallyValidRegionTag(region->rangeOf(baseName)));
+  }
+
+  return {language, script, region};
+}
+
+static inline auto BaseNameParts(JSLinearString* baseName) {
+  JS::AutoCheckCannotGC nogc;
+  return baseName->hasLatin1Chars()
+             ? BaseNameParts(baseName->latin1Chars(nogc), baseName->length())
+             : BaseNameParts(baseName->twoByteChars(nogc), baseName->length());
+}
+
+// Intl.Locale.prototype.maximize ()
+static bool Locale_maximize(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  RootedLinearString tagStr(cx, locale->languageTag()->ensureLinear(cx));
+  if (!tagStr) {
+    return false;
+  }
+
+  LanguageTag tag(cx);
+  if (!LanguageTagParser::parse(cx, tagStr, tag)) {
+    return false;
+  }
+
+  if (!tag.addLikelySubtags(cx)) {
+    return false;
+  }
+
+  // Step 4.
+  auto* result = CreateLocaleNativeObject(cx, nullptr, tag);
+  if (!result) {
+    return false;
+  }
+  args.rval().setObject(*result);
+  return true;
+}
+
+// Intl.Locale.prototype.maximize ()
+static bool Locale_maximize(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_maximize>(cx, args);
+}
+
+// Intl.Locale.prototype.minimize ()
+static bool Locale_minimize(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  RootedLinearString tagStr(cx, locale->languageTag()->ensureLinear(cx));
+  if (!tagStr) {
+    return false;
+  }
+
+  LanguageTag tag(cx);
+  if (!LanguageTagParser::parse(cx, tagStr, tag)) {
+    return false;
+  }
+
+  if (!tag.removeLikelySubtags(cx)) {
+    return false;
+  }
+
+  // Step 4.
+  auto* result = CreateLocaleNativeObject(cx, nullptr, tag);
+  if (!result) {
+    return false;
+  }
+  args.rval().setObject(*result);
+  return true;
+}
+
+// Intl.Locale.prototype.minimize ()
+static bool Locale_minimize(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_minimize>(cx, args);
+}
+
+// Intl.Locale.prototype.toString ()
+static bool Locale_toString(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  args.rval().setString(locale->languageTag());
+  return true;
+}
+
+// Intl.Locale.prototype.toString ()
+static bool Locale_toString(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_toString>(cx, args);
+}
+
+// get Intl.Locale.prototype.baseName
+static bool Locale_baseName(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // FIXME: spec bug - invalid assertion in step 4.
+  // FIXME: spec bug - subtag production names not updated.
+
+  // Steps 3, 5.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  args.rval().setString(locale->baseName());
+  return true;
+}
+
+// get Intl.Locale.prototype.baseName
+static bool Locale_baseName(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_baseName>(cx, args);
+}
+
+// get Intl.Locale.prototype.calendar
+static bool Locale_calendar(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  return GetUnicodeExtension(cx, locale, "ca", args.rval());
+}
+
+// get Intl.Locale.prototype.calendar
+static bool Locale_calendar(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_calendar>(cx, args);
+}
+
+// get Intl.Locale.prototype.collation
+static bool Locale_collation(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  return GetUnicodeExtension(cx, locale, "co", args.rval());
+}
+
+// get Intl.Locale.prototype.collation
+static bool Locale_collation(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_collation>(cx, args);
+}
+
+// get Intl.Locale.prototype.hourCycle
+static bool Locale_hourCycle(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  return GetUnicodeExtension(cx, locale, "hc", args.rval());
+}
+
+// get Intl.Locale.prototype.hourCycle
+static bool Locale_hourCycle(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_hourCycle>(cx, args);
+}
+
+// get Intl.Locale.prototype.caseFirst
+static bool Locale_caseFirst(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  return GetUnicodeExtension(cx, locale, "kf", args.rval());
+}
+
+// get Intl.Locale.prototype.caseFirst
+static bool Locale_caseFirst(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_caseFirst>(cx, args);
+}
+
+// get Intl.Locale.prototype.numeric
+static bool Locale_numeric(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  RootedValue value(cx);
+  if (!GetUnicodeExtension(cx, locale, "kn", &value)) {
+    return false;
+  }
+
+  // FIXME: spec bug - comparison should be against the empty string, too.
+  MOZ_ASSERT(value.isUndefined() || value.isString());
+  args.rval().setBoolean(value.isString() && value.toString()->empty());
+  return true;
+}
+
+// get Intl.Locale.prototype.numeric
+static bool Locale_numeric(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_numeric>(cx, args);
+}
+
+// get Intl.Locale.prototype.numberingSystem
+static bool Intl_Locale_numberingSystem(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  return GetUnicodeExtension(cx, locale, "nu", args.rval());
+}
+
+// get Intl.Locale.prototype.numberingSystem
+static bool Locale_numberingSystem(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Intl_Locale_numberingSystem>(cx, args);
+}
+
+// get Intl.Locale.prototype.language
+static bool Locale_language(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
+  if (!baseName) {
+    return false;
+  }
+
+  // Step 4 (Unnecessary assertion).
+
+  auto language = BaseNameParts(baseName).language;
+
+  size_t index = language.index;
+  size_t length = language.length;
+
+  // Step 5.
+  // FIXME: spec bug - not all production names updated.
+  JSString* str = NewDependentString(cx, baseName, index, length);
+  if (!str) {
+    return false;
+  }
+
+  args.rval().setString(str);
+  return true;
+}
+
+// get Intl.Locale.prototype.language
+static bool Locale_language(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_language>(cx, args);
+}
+
+// get Intl.Locale.prototype.script
+static bool Locale_script(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
+  if (!baseName) {
+    return false;
+  }
+
+  // Step 4 (Unnecessary assertion).
+
+  auto script = BaseNameParts(baseName).script;
+
+  // Step 5.
+  // FIXME: spec bug - not all production names updated.
+  if (!script) {
+    args.rval().setUndefined();
+    return true;
+  }
+
+  size_t index = script->index;
+  size_t length = script->length;
+
+  // Step 6.
+  JSString* str = NewDependentString(cx, baseName, index, length);
+  if (!str) {
+    return false;
+  }
+
+  args.rval().setString(str);
+  return true;
+}
+
+// get Intl.Locale.prototype.script
+static bool Locale_script(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_script>(cx, args);
+}
+
+// get Intl.Locale.prototype.region
+static bool Locale_region(JSContext* cx, const CallArgs& args) {
+  MOZ_ASSERT(IsLocale(args.thisv()));
+
+  // Step 3.
+  auto* locale = &args.thisv().toObject().as<NativeLocaleObject>();
+  JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
+  if (!baseName) {
+    return false;
+  }
+
+  // Step 4 (Unnecessary assertion).
+
+  auto region = BaseNameParts(baseName).region;
+
+  // Step 5.
+  if (!region) {
+    args.rval().setUndefined();
+    return true;
+  }
+
+  size_t index = region->index;
+  size_t length = region->length;
+
+  // Step 6.
+  JSString* str = NewDependentString(cx, baseName, index, length);
+  if (!str) {
+    return false;
+  }
+
+  args.rval().setString(str);
+  return true;
+}
+
+// get Intl.Locale.prototype.region
+static bool Locale_region(JSContext* cx, unsigned argc, Value* vp) {
+  // Steps 1-2.
+  CallArgs args = CallArgsFromVp(argc, vp);
+  return CallNonGenericMethod<IsLocale, Locale_region>(cx, args);
+}
+
+static bool Locale_toSource(JSContext* cx, unsigned argc, Value* vp) {
+  CallArgs args = CallArgsFromVp(argc, vp);
+  args.rval().setString(cx->names().Locale);
+  return true;
+}
+
+static const JSFunctionSpec locale_native_methods[] = {
+    JS_FN("maximize", Locale_maximize, 0, 0),
+    JS_FN("minimize", Locale_minimize, 0, 0),
+    JS_FN(js_toString_str, Locale_toString, 0, 0),
+    JS_FN(js_toSource_str, Locale_toSource, 0, 0), JS_FS_END};
+
+static const JSPropertySpec locale_native_properties[] = {
+    JS_PSG("baseName", Locale_baseName, 0),
+    JS_PSG("calendar", Locale_calendar, 0),
+    JS_PSG("collation", Locale_collation, 0),
+    JS_PSG("hourCycle", Locale_hourCycle, 0),
+    JS_PSG("caseFirst", Locale_caseFirst, 0),
+    JS_PSG("numeric", Locale_numeric, 0),
+    JS_PSG("numberingSystem", Locale_numberingSystem, 0),
+    JS_PSG("language", Locale_language, 0),
+    JS_PSG("script", Locale_script, 0),
+    JS_PSG("region", Locale_region, 0),
+    JS_STRING_SYM_PS(toStringTag, "Intl.Locale", JSPROP_READONLY),
+    JS_PS_END};
+
+JSObject* js::CreateNativeLocalePrototype(JSContext* cx, HandleObject Intl,
+                                          Handle<GlobalObject*> global) {
+  RootedFunction ctor(cx, GlobalObject::createConstructor(
+                              cx, &NativeLocale, cx->names().Locale, 1));
+  if (!ctor) {
+    return nullptr;
+  }
+
+  RootedObject proto(
+      cx, GlobalObject::createBlankPrototype<PlainObject>(cx, global));
+  if (!proto) {
+    return nullptr;
+  }
+
+  if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
+    return nullptr;
+  }
+
+  if (!DefinePropertiesAndFunctions(cx, proto, locale_native_properties,
+                                    locale_native_methods)) {
+    return nullptr;
+  }
+
+  RootedValue ctorValue(cx, ObjectValue(*ctor));
+  if (!DefineDataProperty(cx, Intl, cx->names().locale, ctorValue, 0)) {
+    return nullptr;
+  }
+
+  return proto;
+}
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -378,16 +378,17 @@ if CONFIG['ENABLE_INTL_API']:
     UNIFIED_SOURCES += [
         'builtin/intl/Collator.cpp',
         'builtin/intl/CommonFunctions.cpp',
         'builtin/intl/DateTimeFormat.cpp',
         'builtin/intl/IntlObject.cpp',
         'builtin/intl/LanguageTag.cpp',
         'builtin/intl/LanguageTagGenerated.cpp',
         'builtin/intl/Locale.cpp',
+        'builtin/intl/NativeLocale.cpp',
         'builtin/intl/NumberFormat.cpp',
         'builtin/intl/PluralRules.cpp',
         'builtin/intl/RelativeTimeFormat.cpp',
         'builtin/intl/SharedIntlData.cpp',
         'builtin/intl/UnicodeExtensionsGenerated.cpp',
     ]
 
 if CONFIG['MOZ_INSTRUMENTS']:
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -59,29 +59,31 @@
   MACRO(builder, builder, "builder")                                           \
   MACRO(by, by, "by")                                                          \
   MACRO(byob, byob, "byob")                                                    \
   MACRO(byteAlignment, byteAlignment, "byteAlignment")                         \
   MACRO(byteLength, byteLength, "byteLength")                                  \
   MACRO(byteOffset, byteOffset, "byteOffset")                                  \
   MACRO(bytes, bytes, "bytes")                                                 \
   MACRO(BYTES_PER_ELEMENT, BYTES_PER_ELEMENT, "BYTES_PER_ELEMENT")             \
+  MACRO(calendar, calendar, "calendar")                                        \
   MACRO(call, call, "call")                                                    \
   MACRO(callContentFunction, callContentFunction, "callContentFunction")       \
   MACRO(callee, callee, "callee")                                              \
   MACRO(caller, caller, "caller")                                              \
   MACRO(callFunction, callFunction, "callFunction")                            \
   MACRO(CallRegExpMethodIfWrapped, CallRegExpMethodIfWrapped,                  \
         "CallRegExpMethodIfWrapped")                                           \
   MACRO(cancel, cancel, "cancel")                                              \
   MACRO(case, case_, "case")                                                   \
   MACRO(caseFirst, caseFirst, "caseFirst")                                     \
   MACRO(catch, catch_, "catch")                                                \
   MACRO(class, class_, "class")                                                \
   MACRO(close, close, "close")                                                 \
+  MACRO(collation, collation, "collation")                                     \
   MACRO(Collator, Collator, "Collator")                                        \
   MACRO(collections, collections, "collections")                               \
   MACRO(columnNumber, columnNumber, "columnNumber")                            \
   MACRO(comma, comma, ",")                                                     \
   MACRO(compare, compare, "compare")                                           \
   MACRO(configurable, configurable, "configurable")                            \
   MACRO(const, const_, "const")                                                \
   MACRO(construct, construct, "construct")                                     \
@@ -203,16 +205,17 @@
   MACRO(globalThis, globalThis, "globalThis")                                  \
   MACRO(group, group, "group")                                                 \
   MACRO(Handle, Handle, "Handle")                                              \
   MACRO(has, has, "has")                                                       \
   MACRO(hasOwn, hasOwn, "hasOwn")                                              \
   MACRO(hasOwnProperty, hasOwnProperty, "hasOwnProperty")                      \
   MACRO(highWaterMark, highWaterMark, "highWaterMark")                         \
   MACRO(hour, hour, "hour")                                                    \
+  MACRO(hourCycle, hourCycle, "hourCycle")                                     \
   MACRO(if, if_, "if")                                                         \
   MACRO(ignoreCase, ignoreCase, "ignoreCase")                                  \
   MACRO(ignorePunctuation, ignorePunctuation, "ignorePunctuation")             \
   MACRO(implements, implements, "implements")                                  \
   MACRO(import, import, "import")                                              \
   MACRO(in, in, "in")                                                          \
   MACRO(includes, includes, "includes")                                        \
   MACRO(incumbentGlobal, incumbentGlobal, "incumbentGlobal")                   \
@@ -252,16 +255,17 @@
   MACRO(isPrototypeOf, isPrototypeOf, "isPrototypeOf")                         \
   MACRO(isStepStart, isStepStart, "isStepStart")                               \
   MACRO(IterableToList, IterableToList, "IterableToList")                      \
   MACRO(iterate, iterate, "iterate")                                           \
   MACRO(join, join, "join")                                                    \
   MACRO(js, js, "js")                                                          \
   MACRO(keys, keys, "keys")                                                    \
   MACRO(label, label, "label")                                                 \
+  MACRO(language, language, "language")                                        \
   MACRO(lastIndex, lastIndex, "lastIndex")                                     \
   MACRO(length, length, "length")                                              \
   MACRO(let, let, "let")                                                       \
   MACRO(line, line, "line")                                                    \
   MACRO(lineNumber, lineNumber, "lineNumber")                                  \
   MACRO(literal, literal, "literal")                                           \
   MACRO(loc, loc, "loc")                                                       \
   MACRO(Locale, Locale, "Locale")                                              \
@@ -308,16 +312,17 @@
   MACRO(NFKC, NFKC, "NFKC")                                                    \
   MACRO(NFKD, NFKD, "NFKD")                                                    \
   MACRO(noFilename, noFilename, "noFilename")                                  \
   MACRO(nonincrementalReason, nonincrementalReason, "nonincrementalReason")    \
   MACRO(noStack, noStack, "noStack")                                           \
   MACRO(notation, notation, "notation")                                        \
   MACRO(notes, notes, "notes")                                                 \
   MACRO(NumberFormat, NumberFormat, "NumberFormat")                            \
+  MACRO(numberingSystem, numberingSystem, "numberingSystem")                   \
   MACRO(numeric, numeric, "numeric")                                           \
   MACRO(objectArguments, objectArguments, "[object Arguments]")                \
   MACRO(objectArray, objectArray, "[object Array]")                            \
   MACRO(objectBigInt, objectBigInt, "[object BigInt]")                         \
   MACRO(objectBoolean, objectBoolean, "[object Boolean]")                      \
   MACRO(objectDate, objectDate, "[object Date]")                               \
   MACRO(objectError, objectError, "[object Error]")                            \
   MACRO(objectFunction, objectFunction, "[object Function]")                   \
@@ -358,16 +363,17 @@
   MACRO(reason, reason, "reason")                                              \
   MACRO(RegExpBuiltinExec, RegExpBuiltinExec, "RegExpBuiltinExec")             \
   MACRO(RegExpFlagsGetter, RegExpFlagsGetter, "$RegExpFlagsGetter")            \
   MACRO(RegExpMatcher, RegExpMatcher, "RegExpMatcher")                         \
   MACRO(RegExpSearcher, RegExpSearcher, "RegExpSearcher")                      \
   MACRO(RegExpStringIterator, RegExpStringIterator, "RegExp String Iterator")  \
   MACRO(RegExpTester, RegExpTester, "RegExpTester")                            \
   MACRO(RegExp_prototype_Exec, RegExp_prototype_Exec, "RegExp_prototype_Exec") \
+  MACRO(region, region, "region")                                              \
   MACRO(Reify, Reify, "Reify")                                                 \
   MACRO(reject, reject, "reject")                                              \
   MACRO(rejected, rejected, "rejected")                                        \
   MACRO(relatedYear, relatedYear, "relatedYear")                               \
   MACRO(RelativeTimeFormat, RelativeTimeFormat, "RelativeTimeFormat")          \
   MACRO(RelativeTimeFormatFormat, RelativeTimeFormatFormat,                    \
         "Intl_RelativeTimeFormat_Format")                                      \
   MACRO(RequireObjectCoercible, RequireObjectCoercible,                        \
--- a/js/src/vm/GlobalObject.h
+++ b/js/src/vm/GlobalObject.h
@@ -91,16 +91,17 @@ class GlobalObject : public NativeObject
     COLLATOR_PROTO,
     NUMBER_FORMAT,
     NUMBER_FORMAT_PROTO,
     DATE_TIME_FORMAT,
     DATE_TIME_FORMAT_PROTO,
     PLURAL_RULES_PROTO,
     RELATIVE_TIME_FORMAT_PROTO,
     LOCALE_PROTO,
+    NATIVE_LOCALE_PROTO,
     MODULE_PROTO,
     IMPORT_ENTRY_PROTO,
     EXPORT_ENTRY_PROTO,
     REQUESTED_MODULE_PROTO,
     REGEXP_STATICS,
     RUNTIME_CODEGEN_ENABLED,
     DEBUGGERS,
     INTRINSICS,
@@ -544,16 +545,21 @@ class GlobalObject : public NativeObject
   }
 
   static JSObject* getOrCreateLocalePrototype(JSContext* cx,
                                               Handle<GlobalObject*> global) {
     return getOrCreateObject(cx, global, LOCALE_PROTO, initIntlObject);
   }
 #endif  // ENABLE_INTL_API
 
+  static JSObject* getOrCreateLocaleNativePrototype(
+      JSContext* cx, Handle<GlobalObject*> global) {
+    return getOrCreateObject(cx, global, NATIVE_LOCALE_PROTO, initIntlObject);
+  }
+
   static bool ensureModulePrototypesCreated(JSContext* cx,
                                             Handle<GlobalObject*> global);
 
   static JSObject* getOrCreateModulePrototype(JSContext* cx,
                                               Handle<GlobalObject*> global) {
     return getOrCreateObject(cx, global, MODULE_PROTO, initModuleProto);
   }