summaryrefslogtreecommitdiffstats
path: root/js/src/builtin/intl
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/builtin/intl')
-rw-r--r--js/src/builtin/intl/Collator.cpp472
-rw-r--r--js/src/builtin/intl/Collator.h104
-rw-r--r--js/src/builtin/intl/Collator.js468
-rw-r--r--js/src/builtin/intl/CommonFunctions.cpp149
-rw-r--r--js/src/builtin/intl/CommonFunctions.h103
-rw-r--r--js/src/builtin/intl/CommonFunctions.js992
-rw-r--r--js/src/builtin/intl/CurrencyDataGenerated.js78
-rw-r--r--js/src/builtin/intl/DateTimeFormat.cpp1580
-rw-r--r--js/src/builtin/intl/DateTimeFormat.h188
-rw-r--r--js/src/builtin/intl/DateTimeFormat.js1008
-rw-r--r--js/src/builtin/intl/DecimalNumber.cpp263
-rw-r--r--js/src/builtin/intl/DecimalNumber.h117
-rw-r--r--js/src/builtin/intl/DisplayNames.cpp551
-rw-r--r--js/src/builtin/intl/DisplayNames.h79
-rw-r--r--js/src/builtin/intl/DisplayNames.js418
-rw-r--r--js/src/builtin/intl/FormatBuffer.h155
-rw-r--r--js/src/builtin/intl/IcuMemoryUsage.java260
-rw-r--r--js/src/builtin/intl/IntlObject.cpp910
-rw-r--r--js/src/builtin/intl/IntlObject.h82
-rw-r--r--js/src/builtin/intl/IntlObject.js81
-rw-r--r--js/src/builtin/intl/LanguageTag.cpp193
-rw-r--r--js/src/builtin/intl/LanguageTag.h91
-rw-r--r--js/src/builtin/intl/ListFormat.cpp373
-rw-r--r--js/src/builtin/intl/ListFormat.h69
-rw-r--r--js/src/builtin/intl/ListFormat.js330
-rw-r--r--js/src/builtin/intl/Locale.cpp1517
-rw-r--r--js/src/builtin/intl/Locale.h61
-rw-r--r--js/src/builtin/intl/NumberFormat.cpp1374
-rw-r--r--js/src/builtin/intl/NumberFormat.h129
-rw-r--r--js/src/builtin/intl/NumberFormat.js1210
-rw-r--r--js/src/builtin/intl/NumberingSystems.yaml82
-rw-r--r--js/src/builtin/intl/NumberingSystemsGenerated.h83
-rw-r--r--js/src/builtin/intl/PluralRules.cpp423
-rw-r--r--js/src/builtin/intl/PluralRules.h98
-rw-r--r--js/src/builtin/intl/PluralRules.js390
-rw-r--r--js/src/builtin/intl/RelativeTimeFormat.cpp403
-rw-r--r--js/src/builtin/intl/RelativeTimeFormat.h87
-rw-r--r--js/src/builtin/intl/RelativeTimeFormat.js329
-rw-r--r--js/src/builtin/intl/SanctionedSimpleUnitIdentifiers.yaml58
-rw-r--r--js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js56
-rw-r--r--js/src/builtin/intl/SharedIntlData.cpp754
-rw-r--r--js/src/builtin/intl/SharedIntlData.h335
-rw-r--r--js/src/builtin/intl/StringAsciiChars.h77
-rw-r--r--js/src/builtin/intl/TimeZoneDataGenerated.h142
-rwxr-xr-xjs/src/builtin/intl/make_intl_data.py4123
45 files changed, 20845 insertions, 0 deletions
diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp
new file mode 100644
index 0000000000..449de1dc45
--- /dev/null
+++ b/js/src/builtin/intl/Collator.cpp
@@ -0,0 +1,472 @@
+/* -*- 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.Collator implementation. */
+
+#include "builtin/intl/Collator.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/Collator.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/Span.h"
+
+#include "builtin/Array.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/FormatBuffer.h"
+#include "builtin/intl/LanguageTag.h"
+#include "builtin/intl/SharedIntlData.h"
+#include "gc/GCContext.h"
+#include "js/PropertySpec.h"
+#include "js/StableStringChars.h"
+#include "js/TypeDecls.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/Runtime.h"
+#include "vm/StringType.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/GeckoProfiler-inl.h"
+#include "vm/JSObject-inl.h"
+
+using namespace js;
+
+using JS::AutoStableStringChars;
+
+using js::intl::ReportInternalError;
+using js::intl::SharedIntlData;
+
+const JSClassOps CollatorObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ CollatorObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ nullptr, // trace
+};
+
+const JSClass CollatorObject::class_ = {
+ "Intl.Collator",
+ JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_Collator) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &CollatorObject::classOps_, &CollatorObject::classSpec_};
+
+const JSClass& CollatorObject::protoClass_ = PlainObject::class_;
+
+static bool collator_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().Collator);
+ return true;
+}
+
+static const JSFunctionSpec collator_static_methods[] = {
+ JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf",
+ 1, 0),
+ JS_FS_END};
+
+static const JSFunctionSpec collator_methods[] = {
+ JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0),
+ JS_FN(js_toSource_str, collator_toSource, 0, 0), JS_FS_END};
+
+static const JSPropertySpec collator_properties[] = {
+ JS_SELF_HOSTED_GET("compare", "$Intl_Collator_compare_get", 0),
+ JS_STRING_SYM_PS(toStringTag, "Intl.Collator", JSPROP_READONLY), JS_PS_END};
+
+static bool Collator(JSContext* cx, unsigned argc, Value* vp);
+
+const ClassSpec CollatorObject::classSpec_ = {
+ GenericCreateConstructor<Collator, 0, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<CollatorObject>,
+ collator_static_methods,
+ nullptr,
+ collator_methods,
+ collator_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+/**
+ * 10.1.2 Intl.Collator([ locales [, options]])
+ *
+ * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
+ */
+static bool Collator(JSContext* cx, const CallArgs& args) {
+ AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.Collator");
+
+ // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
+
+ // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+ RootedObject proto(cx);
+ if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Collator, &proto)) {
+ return false;
+ }
+
+ Rooted<CollatorObject*> collator(
+ cx, NewObjectWithClassProto<CollatorObject>(cx, proto));
+ if (!collator) {
+ return false;
+ }
+
+ HandleValue locales = args.get(0);
+ HandleValue options = args.get(1);
+
+ // Step 6.
+ if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator,
+ locales, options)) {
+ return false;
+ }
+
+ args.rval().setObject(*collator);
+ return true;
+}
+
+static bool Collator(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ return Collator(cx, args);
+}
+
+bool js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 2);
+ MOZ_ASSERT(!args.isConstructing());
+
+ return Collator(cx, args);
+}
+
+void js::CollatorObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ MOZ_ASSERT(gcx->onMainThread());
+
+ if (mozilla::intl::Collator* coll = obj->as<CollatorObject>().getCollator()) {
+ intl::RemoveICUCellMemory(gcx, obj, CollatorObject::EstimatedMemoryUse);
+ delete coll;
+ }
+}
+
+bool js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+ auto keywords =
+ mozilla::intl::Collator::GetBcp47KeywordValuesForLocale(locale.get());
+ if (keywords.isErr()) {
+ ReportInternalError(cx, keywords.unwrapErr());
+ return false;
+ }
+
+ RootedObject collations(cx, NewDenseEmptyArray(cx));
+ if (!collations) {
+ return false;
+ }
+
+ // The first element of the collations array must be |null| per
+ // ES2017 Intl, 10.2.3 Internal Slots.
+ if (!NewbornArrayPush(cx, collations, NullValue())) {
+ return false;
+ }
+
+ for (auto result : keywords.unwrap()) {
+ if (result.isErr()) {
+ ReportInternalError(cx);
+ return false;
+ }
+ mozilla::Span<const char> collation = result.unwrap();
+
+ // Per ECMA-402, 10.2.3, we don't include standard and search:
+ // "The values 'standard' and 'search' must not be used as elements in
+ // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co
+ // array."
+ static constexpr auto standard = mozilla::MakeStringSpan("standard");
+ static constexpr auto search = mozilla::MakeStringSpan("search");
+ if (collation == standard || collation == search) {
+ continue;
+ }
+
+ JSString* jscollation = NewStringCopy<CanGC>(cx, collation);
+ if (!jscollation) {
+ return false;
+ }
+ if (!NewbornArrayPush(cx, collations, StringValue(jscollation))) {
+ return false;
+ }
+ }
+
+ args.rval().setObject(*collations);
+ return true;
+}
+
+/**
+ * Returns a new mozilla::intl::Collator with the locale and collation options
+ * of the given Collator.
+ */
+static mozilla::intl::Collator* NewIntlCollator(
+ JSContext* cx, Handle<CollatorObject*> collator) {
+ RootedValue value(cx);
+
+ RootedObject internals(cx, intl::GetInternalsObject(cx, collator));
+ if (!internals) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
+ return nullptr;
+ }
+
+ mozilla::intl::Locale tag;
+ {
+ Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx));
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!intl::ParseLocale(cx, locale, tag)) {
+ return nullptr;
+ }
+ }
+
+ using mozilla::intl::Collator;
+
+ Collator::Options options{};
+
+ if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) {
+ return nullptr;
+ }
+
+ enum class Usage { Search, Sort };
+
+ Usage usage;
+ {
+ JSLinearString* str = value.toString()->ensureLinear(cx);
+ if (!str) {
+ return nullptr;
+ }
+
+ if (StringEqualsLiteral(str, "search")) {
+ usage = Usage::Search;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(str, "sort"));
+ usage = Usage::Sort;
+ }
+ }
+
+ JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
+
+ // ICU expects collation as Unicode locale extensions on locale.
+ if (usage == Usage::Search) {
+ if (!keywords.emplaceBack("co", cx->names().search)) {
+ return nullptr;
+ }
+
+ // Search collations can't select a different collation, so the collation
+ // property is guaranteed to be "default".
+#ifdef DEBUG
+ if (!GetProperty(cx, internals, internals, cx->names().collation, &value)) {
+ return nullptr;
+ }
+
+ JSLinearString* collation = value.toString()->ensureLinear(cx);
+ if (!collation) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(StringEqualsLiteral(collation, "default"));
+#endif
+ } else {
+ if (!GetProperty(cx, internals, internals, cx->names().collation, &value)) {
+ return nullptr;
+ }
+
+ JSLinearString* collation = value.toString()->ensureLinear(cx);
+ if (!collation) {
+ return nullptr;
+ }
+
+ // Set collation as a Unicode locale extension when it was specified.
+ if (!StringEqualsLiteral(collation, "default")) {
+ if (!keywords.emplaceBack("co", collation)) {
+ return nullptr;
+ }
+ }
+ }
+
+ // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of the
+ // Unicode extension subtag. We're then relying on ICU to follow RFC 6067,
+ // which states that any trailing keywords using the same key should be
+ // ignored.
+ if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
+ return nullptr;
+ }
+
+ intl::FormatBuffer<char> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+
+ UniqueChars locale = buffer.extractStringZ();
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* sensitivity = value.toString()->ensureLinear(cx);
+ if (!sensitivity) {
+ return nullptr;
+ }
+ if (StringEqualsLiteral(sensitivity, "base")) {
+ options.sensitivity = Collator::Sensitivity::Base;
+ } else if (StringEqualsLiteral(sensitivity, "accent")) {
+ options.sensitivity = Collator::Sensitivity::Accent;
+ } else if (StringEqualsLiteral(sensitivity, "case")) {
+ options.sensitivity = Collator::Sensitivity::Case;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(sensitivity, "variant"));
+ options.sensitivity = Collator::Sensitivity::Variant;
+ }
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation,
+ &value)) {
+ return nullptr;
+ }
+ options.ignorePunctuation = value.toBoolean();
+
+ if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) {
+ return nullptr;
+ }
+ if (!value.isUndefined()) {
+ options.numeric = value.toBoolean();
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) {
+ return nullptr;
+ }
+ if (!value.isUndefined()) {
+ JSLinearString* caseFirst = value.toString()->ensureLinear(cx);
+ if (!caseFirst) {
+ return nullptr;
+ }
+ if (StringEqualsLiteral(caseFirst, "upper")) {
+ options.caseFirst = Collator::CaseFirst::Upper;
+ } else if (StringEqualsLiteral(caseFirst, "lower")) {
+ options.caseFirst = Collator::CaseFirst::Lower;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(caseFirst, "false"));
+ options.caseFirst = Collator::CaseFirst::False;
+ }
+ }
+
+ auto collResult = Collator::TryCreate(locale.get());
+ if (collResult.isErr()) {
+ ReportInternalError(cx, collResult.unwrapErr());
+ return nullptr;
+ }
+ auto coll = collResult.unwrap();
+
+ auto optResult = coll->SetOptions(options);
+ if (optResult.isErr()) {
+ ReportInternalError(cx, optResult.unwrapErr());
+ return nullptr;
+ }
+
+ return coll.release();
+}
+
+static mozilla::intl::Collator* GetOrCreateCollator(
+ JSContext* cx, Handle<CollatorObject*> collator) {
+ // Obtain a cached mozilla::intl::Collator object.
+ mozilla::intl::Collator* coll = collator->getCollator();
+ if (coll) {
+ return coll;
+ }
+
+ coll = NewIntlCollator(cx, collator);
+ if (!coll) {
+ return nullptr;
+ }
+ collator->setCollator(coll);
+
+ intl::AddICUCellMemory(collator, CollatorObject::EstimatedMemoryUse);
+ return coll;
+}
+
+static bool intl_CompareStrings(JSContext* cx, mozilla::intl::Collator* coll,
+ HandleString str1, HandleString str2,
+ MutableHandleValue result) {
+ MOZ_ASSERT(str1);
+ MOZ_ASSERT(str2);
+
+ if (str1 == str2) {
+ result.setInt32(0);
+ return true;
+ }
+
+ AutoStableStringChars stableChars1(cx);
+ if (!stableChars1.initTwoByte(cx, str1)) {
+ return false;
+ }
+
+ AutoStableStringChars stableChars2(cx);
+ if (!stableChars2.initTwoByte(cx, str2)) {
+ return false;
+ }
+
+ mozilla::Range<const char16_t> chars1 = stableChars1.twoByteRange();
+ mozilla::Range<const char16_t> chars2 = stableChars2.twoByteRange();
+
+ result.setInt32(coll->CompareStrings(chars1, chars2));
+ return true;
+}
+
+bool js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+ MOZ_ASSERT(args[0].isObject());
+ MOZ_ASSERT(args[1].isString());
+ MOZ_ASSERT(args[2].isString());
+
+ Rooted<CollatorObject*> collator(cx,
+ &args[0].toObject().as<CollatorObject>());
+
+ mozilla::intl::Collator* coll = GetOrCreateCollator(cx, collator);
+ if (!coll) {
+ return false;
+ }
+
+ // Use the UCollator to actually compare the strings.
+ RootedString str1(cx, args[1].toString());
+ RootedString str2(cx, args[2].toString());
+ return intl_CompareStrings(cx, coll, str1, str2, args.rval());
+}
+
+bool js::intl_isUpperCaseFirst(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+
+ RootedString locale(cx, args[0].toString());
+ bool isUpperFirst;
+ if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) {
+ return false;
+ }
+
+ args.rval().setBoolean(isUpperFirst);
+ return true;
+}
diff --git a/js/src/builtin/intl/Collator.h b/js/src/builtin/intl/Collator.h
new file mode 100644
index 0000000000..764c838ce8
--- /dev/null
+++ b/js/src/builtin/intl/Collator.h
@@ -0,0 +1,104 @@
+/* -*- 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_Collator_h
+#define builtin_intl_Collator_h
+
+#include <stdint.h>
+
+#include "builtin/SelfHostingDefines.h"
+#include "js/Class.h"
+#include "vm/NativeObject.h"
+
+namespace mozilla::intl {
+class Collator;
+}
+
+namespace js {
+
+/******************** Collator ********************/
+
+class CollatorObject : public NativeObject {
+ public:
+ static const JSClass class_;
+ static const JSClass& protoClass_;
+
+ static constexpr uint32_t INTERNALS_SLOT = 0;
+ static constexpr uint32_t INTL_COLLATOR_SLOT = 1;
+ static constexpr uint32_t SLOT_COUNT = 2;
+
+ static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT,
+ "INTERNALS_SLOT must match self-hosting define for internals "
+ "object slot");
+
+ // Estimated memory use for UCollator (see IcuMemoryUsage).
+ static constexpr size_t EstimatedMemoryUse = 1128;
+
+ mozilla::intl::Collator* getCollator() const {
+ const auto& slot = getFixedSlot(INTL_COLLATOR_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::Collator*>(slot.toPrivate());
+ }
+
+ void setCollator(mozilla::intl::Collator* collator) {
+ setFixedSlot(INTL_COLLATOR_SLOT, PrivateValue(collator));
+ }
+
+ private:
+ static const JSClassOps classOps_;
+ static const ClassSpec classSpec_;
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+};
+
+/**
+ * Returns a new instance of the standard built-in Collator constructor.
+ * Self-hosted code cannot cache this constructor (as it does for others in
+ * Utilities.js) because it is initialized after self-hosted code is compiled.
+ *
+ * Usage: collator = intl_Collator(locales, options)
+ */
+[[nodiscard]] extern bool intl_Collator(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns an array with the collation type identifiers per Unicode
+ * Technical Standard 35, Unicode Locale Data Markup Language, for the
+ * collations supported for the given locale. "standard" and "search" are
+ * excluded.
+ *
+ * Usage: collations = intl_availableCollations(locale)
+ */
+[[nodiscard]] extern bool intl_availableCollations(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Compares x and y (which must be String values), and returns a number less
+ * than 0 if x < y, 0 if x = y, or a number greater than 0 if x > y according
+ * to the sort order for the locale and collation options of the given
+ * Collator.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 10.3.2.
+ *
+ * Usage: result = intl_CompareStrings(collator, x, y)
+ */
+[[nodiscard]] extern bool intl_CompareStrings(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns true if the given locale sorts upper-case before lower-case
+ * characters.
+ *
+ * Usage: result = intl_isUpperCaseFirst(locale)
+ */
+[[nodiscard]] extern bool intl_isUpperCaseFirst(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+} // namespace js
+
+#endif /* builtin_intl_Collator_h */
diff --git a/js/src/builtin/intl/Collator.js b/js/src/builtin/intl/Collator.js
new file mode 100644
index 0000000000..b82f07e3d4
--- /dev/null
+++ b/js/src/builtin/intl/Collator.js
@@ -0,0 +1,468 @@
+/* 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/. */
+
+/* Portions Copyright Norbert Lindenberg 2011-2012. */
+
+/**
+ * Compute an internal properties object from |lazyCollatorData|.
+ */
+function resolveCollatorInternals(lazyCollatorData) {
+ assert(IsObject(lazyCollatorData), "lazy data not an object?");
+
+ var internalProps = std_Object_create(null);
+
+ var Collator = collatorInternalProperties;
+
+ // Step 5.
+ internalProps.usage = lazyCollatorData.usage;
+
+ // Steps 6-7.
+ var collatorIsSorting = lazyCollatorData.usage === "sort";
+ var localeData = collatorIsSorting
+ ? Collator.sortLocaleData
+ : Collator.searchLocaleData;
+
+ // Compute effective locale.
+ // Step 16.
+ var relevantExtensionKeys = Collator.relevantExtensionKeys;
+
+ // Step 17.
+ var r = ResolveLocale(
+ "Collator",
+ lazyCollatorData.requestedLocales,
+ lazyCollatorData.opt,
+ relevantExtensionKeys,
+ localeData
+ );
+
+ // Step 18.
+ internalProps.locale = r.locale;
+
+ // Step 19.
+ var collation = r.co;
+
+ // Step 20.
+ if (collation === null) {
+ collation = "default";
+ }
+
+ // Step 21.
+ internalProps.collation = collation;
+
+ // Step 22.
+ internalProps.numeric = r.kn === "true";
+
+ // Step 23.
+ internalProps.caseFirst = r.kf;
+
+ // Compute remaining collation options.
+ // Step 25.
+ var s = lazyCollatorData.rawSensitivity;
+ if (s === undefined) {
+ // In theory the default sensitivity for the "search" collator is
+ // locale dependent; in reality the CLDR/ICU default strength is
+ // always tertiary. Therefore use "variant" as the default value for
+ // both collation modes.
+ s = "variant";
+ }
+
+ // Step 26.
+ internalProps.sensitivity = s;
+
+ // Step 28.
+ internalProps.ignorePunctuation = lazyCollatorData.ignorePunctuation;
+
+ // The caller is responsible for associating |internalProps| with the right
+ // object using |setInternalProperties|.
+ return internalProps;
+}
+
+/**
+ * Returns an object containing the Collator internal properties of |obj|.
+ */
+function getCollatorInternals(obj) {
+ assert(IsObject(obj), "getCollatorInternals called with non-object");
+ assert(
+ intl_GuardToCollator(obj) !== null,
+ "getCollatorInternals called with non-Collator"
+ );
+
+ var internals = getIntlObjectInternals(obj);
+ assert(
+ internals.type === "Collator",
+ "bad type escaped getIntlObjectInternals"
+ );
+
+ // If internal properties have already been computed, use them.
+ var internalProps = maybeInternalProperties(internals);
+ if (internalProps) {
+ return internalProps;
+ }
+
+ // Otherwise it's time to fully create them.
+ internalProps = resolveCollatorInternals(internals.lazyData);
+ setInternalProperties(internals, internalProps);
+ return internalProps;
+}
+
+/**
+ * Initializes an object as a Collator.
+ *
+ * This method is complicated a moderate bit by its implementing initialization
+ * as a *lazy* concept. Everything that must happen now, does -- but we defer
+ * all the work we can until the object is actually used as a Collator. This
+ * later work occurs in |resolveCollatorInternals|; steps not noted here occur
+ * there.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 10.1.1.
+ */
+function InitializeCollator(collator, locales, options) {
+ assert(IsObject(collator), "InitializeCollator called with non-object");
+ assert(
+ intl_GuardToCollator(collator) !== null,
+ "InitializeCollator called with non-Collator"
+ );
+
+ // Lazy Collator data has the following structure:
+ //
+ // {
+ // requestedLocales: List of locales,
+ // usage: "sort" / "search",
+ // opt: // opt object computed in InitializeCollator
+ // {
+ // localeMatcher: "lookup" / "best fit",
+ // co: string matching a Unicode extension type / undefined
+ // kn: true / false / undefined,
+ // kf: "upper" / "lower" / "false" / undefined
+ // }
+ // rawSensitivity: "base" / "accent" / "case" / "variant" / undefined,
+ // ignorePunctuation: true / false
+ // }
+ //
+ // Note that lazy data is only installed as a final step of initialization,
+ // so every Collator lazy data object has *all* these properties, never a
+ // subset of them.
+ var lazyCollatorData = std_Object_create(null);
+
+ // Step 1.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+ lazyCollatorData.requestedLocales = requestedLocales;
+
+ // Steps 2-3.
+ //
+ // If we ever need more speed here at startup, we should try to detect the
+ // case where |options === undefined| and then directly use the default
+ // value for each option. For now, just keep it simple.
+ if (options === undefined) {
+ options = std_Object_create(null);
+ } else {
+ options = ToObject(options);
+ }
+
+ // Compute options that impact interpretation of locale.
+ // Step 4.
+ var u = GetOption(options, "usage", "string", ["sort", "search"], "sort");
+ lazyCollatorData.usage = u;
+
+ // Step 8.
+ var opt = new_Record();
+ lazyCollatorData.opt = opt;
+
+ // Steps 9-10.
+ var matcher = GetOption(
+ options,
+ "localeMatcher",
+ "string",
+ ["lookup", "best fit"],
+ "best fit"
+ );
+ opt.localeMatcher = matcher;
+
+ // https://github.com/tc39/ecma402/pull/459
+ var collation = GetOption(
+ options,
+ "collation",
+ "string",
+ undefined,
+ undefined
+ );
+ if (collation !== undefined) {
+ collation = intl_ValidateAndCanonicalizeUnicodeExtensionType(
+ collation,
+ "collation",
+ "co"
+ );
+ }
+ opt.co = collation;
+
+ // Steps 11-13.
+ var numericValue = GetOption(
+ options,
+ "numeric",
+ "boolean",
+ undefined,
+ undefined
+ );
+ if (numericValue !== undefined) {
+ numericValue = numericValue ? "true" : "false";
+ }
+ opt.kn = numericValue;
+
+ // Steps 14-15.
+ var caseFirstValue = GetOption(
+ options,
+ "caseFirst",
+ "string",
+ ["upper", "lower", "false"],
+ undefined
+ );
+ opt.kf = caseFirstValue;
+
+ // Compute remaining collation options.
+ // Step 24.
+ var s = GetOption(
+ options,
+ "sensitivity",
+ "string",
+ ["base", "accent", "case", "variant"],
+ undefined
+ );
+ lazyCollatorData.rawSensitivity = s;
+
+ // Step 27.
+ var ip = GetOption(options, "ignorePunctuation", "boolean", undefined, false);
+ lazyCollatorData.ignorePunctuation = ip;
+
+ // Step 29.
+ //
+ // We've done everything that must be done now: mark the lazy data as fully
+ // computed and install it.
+ initializeIntlObject(collator, "Collator", lazyCollatorData);
+}
+
+/**
+ * Returns the subset of the given locale list for which this locale list has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 10.2.2.
+ */
+function Intl_Collator_supportedLocalesOf(locales /*, options*/) {
+ var options = arguments.length > 1 ? arguments[1] : undefined;
+
+ // Step 1.
+ var availableLocales = "Collator";
+
+ // Step 2.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+
+ // Step 3.
+ return SupportedLocales(availableLocales, requestedLocales, options);
+}
+
+/**
+ * Collator internal properties.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.1 and 10.2.3.
+ */
+var collatorInternalProperties = {
+ sortLocaleData: collatorSortLocaleData,
+ searchLocaleData: collatorSearchLocaleData,
+ relevantExtensionKeys: ["co", "kf", "kn"],
+};
+
+/**
+ * Returns the actual locale used when a collator for |locale| is constructed.
+ */
+function collatorActualLocale(locale) {
+ assert(typeof locale === "string", "locale should be string");
+
+ // If |locale| is the default locale (e.g. da-DK), but only supported
+ // through a fallback (da), we need to get the actual locale before we
+ // can call intl_isUpperCaseFirst. Also see intl_BestAvailableLocale.
+ return BestAvailableLocaleIgnoringDefault("Collator", locale);
+}
+
+/**
+ * Returns the default caseFirst values for the given locale. The first
+ * element in the returned array denotes the default value per ES2017 Intl,
+ * 9.1 Internal slots of Service Constructors.
+ */
+function collatorSortCaseFirst(locale) {
+ var actualLocale = collatorActualLocale(locale);
+ if (intl_isUpperCaseFirst(actualLocale)) {
+ return ["upper", "false", "lower"];
+ }
+
+ // Default caseFirst values for all other languages.
+ return ["false", "lower", "upper"];
+}
+
+/**
+ * Returns the default caseFirst value for the given locale.
+ */
+function collatorSortCaseFirstDefault(locale) {
+ var actualLocale = collatorActualLocale(locale);
+ if (intl_isUpperCaseFirst(actualLocale)) {
+ return "upper";
+ }
+
+ // Default caseFirst value for all other languages.
+ return "false";
+}
+
+function collatorSortLocaleData() {
+ /* eslint-disable object-shorthand */
+ return {
+ co: intl_availableCollations,
+ kn: function() {
+ return ["false", "true"];
+ },
+ kf: collatorSortCaseFirst,
+ default: {
+ co: function() {
+ // The first element of the collations array must be |null|
+ // per ES2017 Intl, 10.2.3 Internal Slots.
+ return null;
+ },
+ kn: function() {
+ return "false";
+ },
+ kf: collatorSortCaseFirstDefault,
+ },
+ };
+ /* eslint-enable object-shorthand */
+}
+
+function collatorSearchLocaleData() {
+ /* eslint-disable object-shorthand */
+ return {
+ co: function() {
+ return [null];
+ },
+ kn: function() {
+ return ["false", "true"];
+ },
+ kf: function() {
+ return ["false", "lower", "upper"];
+ },
+ default: {
+ co: function() {
+ return null;
+ },
+ kn: function() {
+ return "false";
+ },
+ kf: function() {
+ return "false";
+ },
+ },
+ };
+ /* eslint-enable object-shorthand */
+}
+
+/**
+ * Create function to be cached and returned by Intl.Collator.prototype.compare.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 10.3.3.1.
+ */
+function createCollatorCompare(collator) {
+ // This function is not inlined in $Intl_Collator_compare_get to avoid
+ // creating a call-object on each call to $Intl_Collator_compare_get.
+ return function(x, y) {
+ // Step 1 (implicit).
+
+ // Step 2.
+ assert(IsObject(collator), "collatorCompareToBind called with non-object");
+ assert(
+ intl_GuardToCollator(collator) !== null,
+ "collatorCompareToBind called with non-Collator"
+ );
+
+ // Steps 3-6
+ var X = ToString(x);
+ var Y = ToString(y);
+
+ // Step 7.
+ return intl_CompareStrings(collator, X, Y);
+ };
+}
+
+/**
+ * Returns a function bound to this Collator that compares x (converted to a
+ * String value) and y (converted to a String value),
+ * and returns a number less than 0 if x < y, 0 if x = y, or a number greater
+ * than 0 if x > y according to the sort order for the locale and collation
+ * options of this Collator object.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 10.3.3.
+ */
+// Uncloned functions with `$` prefix are allocated as extended function
+// to store the original name in `SetCanonicalName`.
+function $Intl_Collator_compare_get() {
+ // Step 1.
+ var collator = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(collator) ||
+ (collator = intl_GuardToCollator(collator)) === null
+ ) {
+ return callFunction(
+ intl_CallCollatorMethodIfWrapped,
+ this,
+ "$Intl_Collator_compare_get"
+ );
+ }
+
+ var internals = getCollatorInternals(collator);
+
+ // Step 4.
+ if (internals.boundCompare === undefined) {
+ // Steps 4.a-c.
+ internals.boundCompare = createCollatorCompare(collator);
+ }
+
+ // Step 5.
+ return internals.boundCompare;
+}
+SetCanonicalName($Intl_Collator_compare_get, "get compare");
+
+/**
+ * Returns the resolved options for a Collator object.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 10.3.4.
+ */
+function Intl_Collator_resolvedOptions() {
+ // Step 1.
+ var collator = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(collator) ||
+ (collator = intl_GuardToCollator(collator)) === null
+ ) {
+ return callFunction(
+ intl_CallCollatorMethodIfWrapped,
+ this,
+ "Intl_Collator_resolvedOptions"
+ );
+ }
+
+ var internals = getCollatorInternals(collator);
+
+ // Steps 4-5.
+ var result = {
+ locale: internals.locale,
+ usage: internals.usage,
+ sensitivity: internals.sensitivity,
+ ignorePunctuation: internals.ignorePunctuation,
+ collation: internals.collation,
+ numeric: internals.numeric,
+ caseFirst: internals.caseFirst,
+ };
+
+ // Step 6.
+ return result;
+}
diff --git a/js/src/builtin/intl/CommonFunctions.cpp b/js/src/builtin/intl/CommonFunctions.cpp
new file mode 100644
index 0000000000..86f555baa5
--- /dev/null
+++ b/js/src/builtin/intl/CommonFunctions.cpp
@@ -0,0 +1,149 @@
+/* -*- 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/. */
+
+/* Operations used to implement multiple Intl.* classes. */
+
+#include "builtin/intl/CommonFunctions.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/ICUError.h"
+#include "mozilla/TextUtils.h"
+
+#include <algorithm>
+
+#include "gc/GCEnum.h"
+#include "gc/ZoneAllocator.h"
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_INTERNAL_INTL_ERROR
+#include "js/Value.h"
+#include "vm/JSAtomState.h"
+#include "vm/JSContext.h"
+#include "vm/JSObject.h"
+#include "vm/SelfHosting.h"
+#include "vm/Stack.h"
+#include "vm/StringType.h"
+
+#include "gc/GCContext-inl.h"
+
+bool js::intl::InitializeObject(JSContext* cx, JS::Handle<JSObject*> obj,
+ JS::Handle<PropertyName*> initializer,
+ JS::Handle<JS::Value> locales,
+ JS::Handle<JS::Value> options) {
+ FixedInvokeArgs<3> args(cx);
+
+ args[0].setObject(*obj);
+ args[1].set(locales);
+ args[2].set(options);
+
+ RootedValue ignored(cx);
+ if (!CallSelfHostedFunction(cx, initializer, JS::NullHandleValue, args,
+ &ignored)) {
+ return false;
+ }
+
+ MOZ_ASSERT(ignored.isUndefined(),
+ "Unexpected return value from non-legacy Intl object initializer");
+ return true;
+}
+
+bool js::intl::LegacyInitializeObject(JSContext* cx, JS::Handle<JSObject*> obj,
+ JS::Handle<PropertyName*> initializer,
+ JS::Handle<JS::Value> thisValue,
+ JS::Handle<JS::Value> locales,
+ JS::Handle<JS::Value> options,
+ DateTimeFormatOptions dtfOptions,
+ JS::MutableHandle<JS::Value> result) {
+ FixedInvokeArgs<5> args(cx);
+
+ args[0].setObject(*obj);
+ args[1].set(thisValue);
+ args[2].set(locales);
+ args[3].set(options);
+ args[4].setBoolean(dtfOptions == DateTimeFormatOptions::EnableMozExtensions);
+
+ if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args, result)) {
+ return false;
+ }
+
+ MOZ_ASSERT(result.isObject(),
+ "Legacy Intl object initializer must return an object");
+ return true;
+}
+
+JSObject* js::intl::GetInternalsObject(JSContext* cx,
+ JS::Handle<JSObject*> obj) {
+ FixedInvokeArgs<1> args(cx);
+
+ args[0].setObject(*obj);
+
+ RootedValue v(cx);
+ if (!js::CallSelfHostedFunction(cx, cx->names().getInternals, NullHandleValue,
+ args, &v)) {
+ return nullptr;
+ }
+
+ return &v.toObject();
+}
+
+void js::intl::ReportInternalError(JSContext* cx) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INTERNAL_INTL_ERROR);
+}
+
+void js::intl::ReportInternalError(JSContext* cx,
+ mozilla::intl::ICUError error) {
+ switch (error) {
+ case mozilla::intl::ICUError::OutOfMemory:
+ ReportOutOfMemory(cx);
+ return;
+ case mozilla::intl::ICUError::InternalError:
+ ReportInternalError(cx);
+ return;
+ case mozilla::intl::ICUError::OverflowError:
+ ReportAllocationOverflow(cx);
+ return;
+ }
+ MOZ_CRASH("Unexpected ICU error");
+}
+
+const js::intl::OldStyleLanguageTagMapping
+ js::intl::oldStyleLanguageTagMappings[] = {
+ {"pa-PK", "pa-Arab-PK"}, {"zh-CN", "zh-Hans-CN"},
+ {"zh-HK", "zh-Hant-HK"}, {"zh-SG", "zh-Hans-SG"},
+ {"zh-TW", "zh-Hant-TW"},
+};
+
+js::UniqueChars js::intl::EncodeLocale(JSContext* cx, JSString* locale) {
+ MOZ_ASSERT(locale->length() > 0);
+
+ js::UniqueChars chars = EncodeAscii(cx, locale);
+
+#ifdef DEBUG
+ // Ensure the returned value contains only valid BCP 47 characters.
+ // (Lambdas can't be placed inside MOZ_ASSERT, so move the checks in an
+ // #ifdef block.)
+ if (chars) {
+ auto alnumOrDash = [](char c) {
+ return mozilla::IsAsciiAlphanumeric(c) || c == '-';
+ };
+ MOZ_ASSERT(mozilla::IsAsciiAlpha(chars[0]));
+ MOZ_ASSERT(
+ std::all_of(chars.get(), chars.get() + locale->length(), alnumOrDash));
+ }
+#endif
+
+ return chars;
+}
+
+void js::intl::AddICUCellMemory(JSObject* obj, size_t nbytes) {
+ // Account the (estimated) number of bytes allocated by an ICU object against
+ // the JSObject's zone.
+ AddCellMemory(obj, nbytes, MemoryUse::ICUObject);
+}
+
+void js::intl::RemoveICUCellMemory(JS::GCContext* gcx, JSObject* obj,
+ size_t nbytes) {
+ gcx->removeCellMemory(obj, nbytes, MemoryUse::ICUObject);
+}
diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h
new file mode 100644
index 0000000000..3f9681c2b7
--- /dev/null
+++ b/js/src/builtin/intl/CommonFunctions.h
@@ -0,0 +1,103 @@
+/* -*- 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_CommonFunctions_h
+#define builtin_intl_CommonFunctions_h
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "js/RootingAPI.h"
+#include "js/Utility.h"
+
+namespace mozilla::intl {
+enum class ICUError : uint8_t;
+}
+
+namespace js {
+
+class PropertyName;
+
+namespace intl {
+
+/**
+ * Initialize a new Intl.* object using the named self-hosted function.
+ */
+extern bool InitializeObject(JSContext* cx, JS::Handle<JSObject*> obj,
+ JS::Handle<PropertyName*> initializer,
+ JS::Handle<JS::Value> locales,
+ JS::Handle<JS::Value> options);
+
+enum class DateTimeFormatOptions {
+ Standard,
+ EnableMozExtensions,
+};
+
+/**
+ * Initialize an existing object as an Intl.* object using the named
+ * self-hosted function. This is only for a few old Intl.* constructors, for
+ * legacy reasons -- new ones should use the function above instead.
+ */
+extern bool LegacyInitializeObject(JSContext* cx, JS::Handle<JSObject*> obj,
+ JS::Handle<PropertyName*> initializer,
+ JS::Handle<JS::Value> thisValue,
+ JS::Handle<JS::Value> locales,
+ JS::Handle<JS::Value> options,
+ DateTimeFormatOptions dtfOptions,
+ JS::MutableHandle<JS::Value> result);
+
+/**
+ * Returns the object holding the internal properties for obj.
+ */
+extern JSObject* GetInternalsObject(JSContext* cx, JS::Handle<JSObject*> obj);
+
+/** Report an Intl internal error not directly tied to a spec step. */
+extern void ReportInternalError(JSContext* cx);
+
+/** Report an Intl internal error not directly tied to a spec step. */
+extern void ReportInternalError(JSContext* cx, mozilla::intl::ICUError error);
+
+/**
+ * The last-ditch locale is used if none of the available locales satisfies a
+ * request. "en-GB" is used based on the assumptions that English is the most
+ * common second language, that both en-GB and en-US are normally available in
+ * an implementation, and that en-GB is more representative of the English used
+ * in other locales.
+ */
+static inline const char* LastDitchLocale() { return "en-GB"; }
+
+/**
+ * Certain old, commonly-used language tags that lack a script, are expected to
+ * nonetheless imply one. This object maps these old-style tags to modern
+ * equivalents.
+ */
+struct OldStyleLanguageTagMapping {
+ const char* const oldStyle;
+ const char* const modernStyle;
+
+ // Provide a constructor to catch missing initializers in the mappings array.
+ constexpr OldStyleLanguageTagMapping(const char* oldStyle,
+ const char* modernStyle)
+ : oldStyle(oldStyle), modernStyle(modernStyle) {}
+};
+
+extern const OldStyleLanguageTagMapping oldStyleLanguageTagMappings[5];
+
+extern JS::UniqueChars EncodeLocale(JSContext* cx, JSString* locale);
+
+// The inline capacity we use for a Vector<char16_t>. Use this to ensure that
+// our uses of ICU string functions, below and elsewhere, will try to fill the
+// buffer's entire inline capacity before growing it and heap-allocating.
+constexpr size_t INITIAL_CHAR_BUFFER_SIZE = 32;
+
+void AddICUCellMemory(JSObject* obj, size_t nbytes);
+
+void RemoveICUCellMemory(JS::GCContext* gcx, JSObject* obj, size_t nbytes);
+} // namespace intl
+
+} // namespace js
+
+#endif /* builtin_intl_CommonFunctions_h */
diff --git a/js/src/builtin/intl/CommonFunctions.js b/js/src/builtin/intl/CommonFunctions.js
new file mode 100644
index 0000000000..c369c49afa
--- /dev/null
+++ b/js/src/builtin/intl/CommonFunctions.js
@@ -0,0 +1,992 @@
+/* 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/. */
+
+/* Portions Copyright Norbert Lindenberg 2011-2012. */
+
+#ifdef DEBUG
+#define assertIsValidAndCanonicalLanguageTag(locale, desc) \
+ do { \
+ let canonical = intl_TryValidateAndCanonicalizeLanguageTag(locale); \
+ assert(canonical !== null, \
+ `${desc} is a structurally valid language tag`); \
+ assert(canonical === locale, \
+ `${desc} is a canonicalized language tag`); \
+ } while (false)
+#else
+#define assertIsValidAndCanonicalLanguageTag(locale, desc) ; // Elided assertion.
+#endif
+
+/**
+ * Returns the start index of a "Unicode locale extension sequence", which the
+ * specification defines as: "any substring of a language tag that starts with
+ * a separator '-' and the singleton 'u' and includes the maximum sequence of
+ * following non-singleton subtags and their preceding '-' separators."
+ *
+ * Alternatively, this may be defined as: the components of a language tag that
+ * match the `unicode_locale_extensions` production in UTS 35.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 6.2.1.
+ */
+function startOfUnicodeExtensions(locale) {
+ assert(typeof locale === "string", "locale is a string");
+
+ // Search for "-u-" marking the start of a Unicode extension sequence.
+ var start = callFunction(std_String_indexOf, locale, "-u-");
+ if (start < 0) {
+ return -1;
+ }
+
+ // And search for "-x-" marking the start of any privateuse component to
+ // handle the case when "-u-" was only found within a privateuse subtag.
+ var privateExt = callFunction(std_String_indexOf, locale, "-x-");
+ if (privateExt >= 0 && privateExt < start) {
+ return -1;
+ }
+
+ return start;
+}
+
+/**
+ * Returns the end index of a Unicode locale extension sequence.
+ */
+function endOfUnicodeExtensions(locale, start) {
+ assert(typeof locale === "string", "locale is a string");
+ assert(0 <= start && start < locale.length, "start is an index into locale");
+ assert(
+ Substring(locale, start, 3) === "-u-",
+ "start points to Unicode extension sequence"
+ );
+
+ // Search for the start of the next singleton or privateuse subtag.
+ //
+ // Begin searching after the smallest possible Unicode locale extension
+ // sequence, namely |"-u-" 2alphanum|. End searching once the remaining
+ // characters can't fit the smallest possible singleton or privateuse
+ // subtag, namely |"-x-" alphanum|. Note the reduced end-limit means
+ // indexing inside the loop is always in-range.
+ for (var i = start + 5, end = locale.length - 4; i <= end; i++) {
+ if (locale[i] !== "-") {
+ continue;
+ }
+ if (locale[i + 2] === "-") {
+ return i;
+ }
+
+ // Skip over (i + 1) and (i + 2) because we've just verified they
+ // aren't "-", so the next possible delimiter can only be at (i + 3).
+ i += 2;
+ }
+
+ // If no singleton or privateuse subtag was found, the Unicode extension
+ // sequence extends until the end of the string.
+ return locale.length;
+}
+
+/**
+ * Removes Unicode locale extension sequences from the given language tag.
+ */
+function removeUnicodeExtensions(locale) {
+ assertIsValidAndCanonicalLanguageTag(
+ locale,
+ "locale with possible Unicode extension"
+ );
+
+ var start = startOfUnicodeExtensions(locale);
+ if (start < 0) {
+ return locale;
+ }
+
+ var end = endOfUnicodeExtensions(locale, start);
+
+ var left = Substring(locale, 0, start);
+ var right = Substring(locale, end, locale.length - end);
+ var combined = left + right;
+
+ assertIsValidAndCanonicalLanguageTag(combined, "the recombined locale");
+ assert(
+ startOfUnicodeExtensions(combined) < 0,
+ "recombination failed to remove all Unicode locale extension sequences"
+ );
+
+ return combined;
+}
+
+/**
+ * Returns Unicode locale extension sequences from the given language tag.
+ */
+function getUnicodeExtensions(locale) {
+ assertIsValidAndCanonicalLanguageTag(locale, "locale with Unicode extension");
+
+ var start = startOfUnicodeExtensions(locale);
+ assert(start >= 0, "start of Unicode extension sequence not found");
+ var end = endOfUnicodeExtensions(locale, start);
+
+ return Substring(locale, start, end - start);
+}
+
+/**
+ * Returns true if the input contains only ASCII alphabetical characters.
+ */
+function IsASCIIAlphaString(s) {
+ assert(typeof s === "string", "IsASCIIAlphaString");
+
+ for (var i = 0; i < s.length; i++) {
+ var c = callFunction(std_String_charCodeAt, s, i);
+ if (!((0x41 <= c && c <= 0x5a) || (0x61 <= c && c <= 0x7a))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+var localeCache = {
+ runtimeDefaultLocale: undefined,
+ defaultLocale: undefined,
+};
+
+/**
+ * Returns the BCP 47 language tag for the host environment's current locale.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 6.2.4.
+ */
+function DefaultLocale() {
+ if (intl_IsRuntimeDefaultLocale(localeCache.runtimeDefaultLocale)) {
+ return localeCache.defaultLocale;
+ }
+
+ // If we didn't have a cache hit, compute the candidate default locale.
+ var runtimeDefaultLocale = intl_RuntimeDefaultLocale();
+ var locale = intl_supportedLocaleOrFallback(runtimeDefaultLocale);
+
+ assertIsValidAndCanonicalLanguageTag(locale, "the computed default locale");
+ assert(
+ startOfUnicodeExtensions(locale) < 0,
+ "the computed default locale must not contain a Unicode extension sequence"
+ );
+
+ // Cache the computed locale until the runtime default locale changes.
+ localeCache.defaultLocale = locale;
+ localeCache.runtimeDefaultLocale = runtimeDefaultLocale;
+
+ return locale;
+}
+
+/**
+ * Canonicalizes a locale list.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.1.
+ */
+function CanonicalizeLocaleList(locales) {
+ // Step 1.
+ if (locales === undefined) {
+ return [];
+ }
+
+ // Step 3 (and the remaining steps).
+ var tag = intl_ValidateAndCanonicalizeLanguageTag(locales, false);
+ if (tag !== null) {
+ assert(
+ typeof tag === "string",
+ "intl_ValidateAndCanonicalizeLanguageTag returns a string value"
+ );
+ return [tag];
+ }
+
+ // Step 2.
+ var seen = [];
+
+ // Step 4.
+ var O = ToObject(locales);
+
+ // Step 5.
+ var len = ToLength(O.length);
+
+ // Step 6.
+ var k = 0;
+
+ // Step 7.
+ while (k < len) {
+ // Steps 7.a-c.
+ if (k in O) {
+ // Step 7.c.i.
+ var kValue = O[k];
+
+ // Step 7.c.ii.
+ if (!(typeof kValue === "string" || IsObject(kValue))) {
+ ThrowTypeError(JSMSG_INVALID_LOCALES_ELEMENT);
+ }
+
+ // Steps 7.c.iii-iv.
+ var tag = intl_ValidateAndCanonicalizeLanguageTag(kValue, true);
+ assert(
+ typeof tag === "string",
+ "ValidateAndCanonicalizeLanguageTag returns a string value"
+ );
+
+ // Step 7.c.v.
+ if (callFunction(std_Array_indexOf, seen, tag) === -1) {
+ DefineDataProperty(seen, seen.length, tag);
+ }
+ }
+
+ // Step 7.d.
+ k++;
+ }
+
+ // Step 8.
+ return seen;
+}
+
+/**
+ * Compares a BCP 47 language tag against the locales in availableLocales
+ * and returns the best available match. Uses the fallback
+ * mechanism of RFC 4647, section 3.4.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.2.
+ * Spec: RFC 4647, section 3.4.
+ */
+function BestAvailableLocale(availableLocales, locale) {
+ return intl_BestAvailableLocale(availableLocales, locale, DefaultLocale());
+}
+
+/**
+ * Identical to BestAvailableLocale, but does not consider the default locale
+ * during computation.
+ */
+function BestAvailableLocaleIgnoringDefault(availableLocales, locale) {
+ return intl_BestAvailableLocale(availableLocales, locale, null);
+}
+
+/**
+ * Compares a BCP 47 language priority list against the set of locales in
+ * availableLocales and determines the best available language to meet the
+ * request. Options specified through Unicode extension subsequences are
+ * ignored in the lookup, but information about such subsequences is returned
+ * separately.
+ *
+ * This variant is based on the Lookup algorithm of RFC 4647 section 3.4.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.3.
+ * Spec: RFC 4647, section 3.4.
+ */
+function LookupMatcher(availableLocales, requestedLocales) {
+ // Step 1.
+ var result = new_Record();
+
+ // Step 2.
+ for (var i = 0; i < requestedLocales.length; i++) {
+ var locale = requestedLocales[i];
+
+ // Step 2.a.
+ var noExtensionsLocale = removeUnicodeExtensions(locale);
+
+ // Step 2.b.
+ var availableLocale = BestAvailableLocale(
+ availableLocales,
+ noExtensionsLocale
+ );
+
+ // Step 2.c.
+ if (availableLocale !== undefined) {
+ // Step 2.c.i.
+ result.locale = availableLocale;
+
+ // Step 2.c.ii.
+ if (locale !== noExtensionsLocale) {
+ result.extension = getUnicodeExtensions(locale);
+ }
+
+ // Step 2.c.iii.
+ return result;
+ }
+ }
+
+ // Steps 3-4.
+ result.locale = DefaultLocale();
+
+ // Step 5.
+ return result;
+}
+
+/**
+ * Compares a BCP 47 language priority list against the set of locales in
+ * availableLocales and determines the best available language to meet the
+ * request. Options specified through Unicode extension subsequences are
+ * ignored in the lookup, but information about such subsequences is returned
+ * separately.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.4.
+ */
+function BestFitMatcher(availableLocales, requestedLocales) {
+ // this implementation doesn't have anything better
+ return LookupMatcher(availableLocales, requestedLocales);
+}
+
+/**
+ * Returns the Unicode extension value subtags for the requested key subtag.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.5.
+ */
+function UnicodeExtensionValue(extension, key) {
+ assert(typeof extension === "string", "extension is a string value");
+ assert(
+ callFunction(std_String_startsWith, extension, "-u-") &&
+ getUnicodeExtensions("und" + extension) === extension,
+ "extension is a Unicode extension subtag"
+ );
+ assert(typeof key === "string", "key is a string value");
+
+ // Step 1.
+ assert(key.length === 2, "key is a Unicode extension key subtag");
+
+ // Step 2.
+ var size = extension.length;
+
+ // Step 3.
+ var searchValue = "-" + key + "-";
+
+ // Step 4.
+ var pos = callFunction(std_String_indexOf, extension, searchValue);
+
+ // Step 5.
+ if (pos !== -1) {
+ // Step 5.a.
+ var start = pos + 4;
+
+ // Step 5.b.
+ var end = start;
+
+ // Step 5.c.
+ var k = start;
+
+ // Steps 5.d-e.
+ while (true) {
+ // Step 5.e.i.
+ var e = callFunction(std_String_indexOf, extension, "-", k);
+
+ // Step 5.e.ii.
+ var len = e === -1 ? size - k : e - k;
+
+ // Step 5.e.iii.
+ if (len === 2) {
+ break;
+ }
+
+ // Step 5.e.iv.
+ if (e === -1) {
+ end = size;
+ break;
+ }
+
+ // Step 5.e.v.
+ end = e;
+ k = e + 1;
+ }
+
+ // Step 5.f.
+ return callFunction(String_substring, extension, start, end);
+ }
+
+ // Step 6.
+ searchValue = "-" + key;
+
+ // Steps 7-8.
+ if (callFunction(std_String_endsWith, extension, searchValue)) {
+ return "";
+ }
+
+ // Step 9 (implicit).
+}
+
+/**
+ * Compares a BCP 47 language priority list against availableLocales and
+ * determines the best available language to meet the request. Options specified
+ * through Unicode extension subsequences are negotiated separately, taking the
+ * caller's relevant extensions and locale data as well as client-provided
+ * options into consideration.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.6.
+ */
+function ResolveLocale(
+ availableLocales,
+ requestedLocales,
+ options,
+ relevantExtensionKeys,
+ localeData
+) {
+ // Steps 1-3.
+ var matcher = options.localeMatcher;
+ var r =
+ matcher === "lookup"
+ ? LookupMatcher(availableLocales, requestedLocales)
+ : BestFitMatcher(availableLocales, requestedLocales);
+
+ // Step 4.
+ var foundLocale = r.locale;
+ var extension = r.extension;
+
+ // Step 5.
+ var result = new_Record();
+
+ // Step 6.
+ result.dataLocale = foundLocale;
+
+ // Step 7.
+ var supportedExtension = "-u";
+
+ // In this implementation, localeData is a function, not an object.
+ var localeDataProvider = localeData();
+
+ // Step 8.
+ for (var i = 0; i < relevantExtensionKeys.length; i++) {
+ var key = relevantExtensionKeys[i];
+
+ // Steps 8.a-h (The locale data is only computed when needed).
+ var keyLocaleData = undefined;
+ var value = undefined;
+
+ // Locale tag may override.
+
+ // Step 8.g.
+ var supportedExtensionAddition = "";
+
+ // Step 8.h.
+ if (extension !== undefined) {
+ // Step 8.h.i.
+ var requestedValue = UnicodeExtensionValue(extension, key);
+
+ // Step 8.h.ii.
+ if (requestedValue !== undefined) {
+ // Steps 8.a-d.
+ keyLocaleData = callFunction(
+ localeDataProvider[key],
+ null,
+ foundLocale
+ );
+
+ // Step 8.h.ii.1.
+ if (requestedValue !== "") {
+ // Step 8.h.ii.1.a.
+ if (
+ callFunction(std_Array_indexOf, keyLocaleData, requestedValue) !==
+ -1
+ ) {
+ value = requestedValue;
+ supportedExtensionAddition = "-" + key + "-" + value;
+ }
+ } else {
+ // Step 8.h.ii.2.
+
+ // According to the LDML spec, if there's no type value,
+ // and true is an allowed value, it's used.
+ if (callFunction(std_Array_indexOf, keyLocaleData, "true") !== -1) {
+ value = "true";
+ supportedExtensionAddition = "-" + key;
+ }
+ }
+ }
+ }
+
+ // Options override all.
+
+ // Step 8.i.i.
+ var optionsValue = options[key];
+
+ // Step 8.i.ii.
+ assert(
+ typeof optionsValue === "string" ||
+ optionsValue === undefined ||
+ optionsValue === null,
+ "unexpected type for options value"
+ );
+
+ // Steps 8.i, 8.i.iii.1.
+ if (optionsValue !== undefined && optionsValue !== value) {
+ // Steps 8.a-d.
+ if (keyLocaleData === undefined) {
+ keyLocaleData = callFunction(
+ localeDataProvider[key],
+ null,
+ foundLocale
+ );
+ }
+
+ // Step 8.i.iii.
+ if (callFunction(std_Array_indexOf, keyLocaleData, optionsValue) !== -1) {
+ value = optionsValue;
+ supportedExtensionAddition = "";
+ }
+ }
+
+ // Locale data provides default value.
+ if (value === undefined) {
+ // Steps 8.a-f.
+ value =
+ keyLocaleData === undefined
+ ? callFunction(localeDataProvider.default[key], null, foundLocale)
+ : keyLocaleData[0];
+ }
+
+ // Step 8.j.
+ assert(
+ typeof value === "string" || value === null,
+ "unexpected locale data value"
+ );
+ result[key] = value;
+
+ // Step 8.k.
+ supportedExtension += supportedExtensionAddition;
+ }
+
+ // Step 9.
+ if (supportedExtension.length > 2) {
+ foundLocale = addUnicodeExtension(foundLocale, supportedExtension);
+ }
+
+ // Step 10.
+ result.locale = foundLocale;
+
+ // Step 11.
+ return result;
+}
+
+/**
+ * Adds a Unicode extension subtag to a locale.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.6.
+ */
+function addUnicodeExtension(locale, extension) {
+ assert(typeof locale === "string", "locale is a string value");
+ assert(
+ !callFunction(std_String_startsWith, locale, "x-"),
+ "unexpected privateuse-only locale"
+ );
+ assert(
+ startOfUnicodeExtensions(locale) < 0,
+ "Unicode extension subtag already present in locale"
+ );
+
+ assert(typeof extension === "string", "extension is a string value");
+ assert(
+ callFunction(std_String_startsWith, extension, "-u-") &&
+ getUnicodeExtensions("und" + extension) === extension,
+ "extension is a Unicode extension subtag"
+ );
+
+ // Step 9.a.
+ var privateIndex = callFunction(std_String_indexOf, locale, "-x-");
+
+ // Steps 9.b-c.
+ if (privateIndex === -1) {
+ locale += extension;
+ } else {
+ var preExtension = callFunction(String_substring, locale, 0, privateIndex);
+ var postExtension = callFunction(String_substring, locale, privateIndex);
+ locale = preExtension + extension + postExtension;
+ }
+
+ // Steps 9.d-e (Step 9.e is not required in this implementation, because we don't canonicalize
+ // Unicode extension subtags).
+ assertIsValidAndCanonicalLanguageTag(locale, "locale after concatenation");
+
+ return locale;
+}
+
+/**
+ * Returns the subset of requestedLocales for which availableLocales has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.7.
+ */
+function LookupSupportedLocales(availableLocales, requestedLocales) {
+ // Step 1.
+ var subset = [];
+
+ // Step 2.
+ for (var i = 0; i < requestedLocales.length; i++) {
+ var locale = requestedLocales[i];
+
+ // Step 2.a.
+ var noExtensionsLocale = removeUnicodeExtensions(locale);
+
+ // Step 2.b.
+ var availableLocale = BestAvailableLocale(
+ availableLocales,
+ noExtensionsLocale
+ );
+
+ // Step 2.c.
+ if (availableLocale !== undefined) {
+ DefineDataProperty(subset, subset.length, locale);
+ }
+ }
+
+ // Step 3.
+ return subset;
+}
+
+/**
+ * Returns the subset of requestedLocales for which availableLocales has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.8.
+ */
+function BestFitSupportedLocales(availableLocales, requestedLocales) {
+ // don't have anything better
+ return LookupSupportedLocales(availableLocales, requestedLocales);
+}
+
+/**
+ * Returns the subset of requestedLocales for which availableLocales has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.9.
+ */
+function SupportedLocales(availableLocales, requestedLocales, options) {
+ // Step 1.
+ var matcher;
+ if (options !== undefined) {
+ // Step 1.a.
+ options = ToObject(options);
+
+ // Step 1.b
+ matcher = options.localeMatcher;
+ if (matcher !== undefined) {
+ matcher = ToString(matcher);
+ if (matcher !== "lookup" && matcher !== "best fit") {
+ ThrowRangeError(JSMSG_INVALID_LOCALE_MATCHER, matcher);
+ }
+ }
+ }
+
+ // Steps 2-5.
+ return matcher === undefined || matcher === "best fit"
+ ? BestFitSupportedLocales(availableLocales, requestedLocales)
+ : LookupSupportedLocales(availableLocales, requestedLocales);
+}
+
+/**
+ * Extracts a property value from the provided options object, converts it to
+ * the required type, checks whether it is one of a list of allowed values,
+ * and fills in a fallback value if necessary.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.10.
+ */
+function GetOption(options, property, type, values, fallback) {
+ // Step 1.
+ var value = options[property];
+
+ // Step 2.
+ if (value !== undefined) {
+ // Steps 2.a-c.
+ if (type === "boolean") {
+ value = ToBoolean(value);
+ } else if (type === "string") {
+ value = ToString(value);
+ } else {
+ assert(false, "GetOption");
+ }
+
+ // Step 2.d.
+ if (
+ values !== undefined &&
+ callFunction(std_Array_indexOf, values, value) === -1
+ ) {
+ ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, property, `"${value}"`);
+ }
+
+ // Step 2.e.
+ return value;
+ }
+
+ // Step 3.
+ return fallback;
+}
+
+/**
+ * Extracts a property value from the provided options object, converts it to
+ * a boolean or string, checks whether it is one of a list of allowed values,
+ * and fills in a fallback value if necessary.
+ */
+function GetStringOrBooleanOption(
+ options,
+ property,
+ values,
+ trueValue,
+ falsyValue,
+ fallback
+) {
+ assert(IsObject(values), "GetStringOrBooleanOption");
+
+ // Step 1.
+ var value = options[property];
+
+ // Step 2.
+ if (value === undefined) {
+ return fallback;
+ }
+
+ // Step 3.
+ if (value === true) {
+ return trueValue;
+ }
+
+ // Steps 4-5.
+ if (!value) {
+ return falsyValue;
+ }
+
+ // Step 6.
+ value = ToString(value);
+
+ // Step 7.
+ if (callFunction(std_Array_indexOf, values, value) === -1) {
+ return fallback;
+ }
+
+ // Step 8.
+ return value;
+}
+
+/**
+ * The abstract operation DefaultNumberOption converts value to a Number value,
+ * checks whether it is in the allowed range, and fills in a fallback value if
+ * necessary.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.11.
+ */
+function DefaultNumberOption(value, minimum, maximum, fallback) {
+ assert(
+ typeof minimum === "number" && (minimum | 0) === minimum,
+ "DefaultNumberOption"
+ );
+ assert(
+ typeof maximum === "number" && (maximum | 0) === maximum,
+ "DefaultNumberOption"
+ );
+ assert(
+ fallback === undefined ||
+ (typeof fallback === "number" && (fallback | 0) === fallback),
+ "DefaultNumberOption"
+ );
+ assert(
+ fallback === undefined || (minimum <= fallback && fallback <= maximum),
+ "DefaultNumberOption"
+ );
+
+ // Step 1.
+ if (value === undefined) {
+ return fallback;
+ }
+
+ // Step 2.
+ value = ToNumber(value);
+
+ // Step 3.
+ if (Number_isNaN(value) || value < minimum || value > maximum) {
+ ThrowRangeError(JSMSG_INVALID_DIGITS_VALUE, value);
+ }
+
+ // Step 4.
+ // Apply bitwise-or to convert -0 to +0 per ES2017, 5.2 and to ensure the
+ // result is an int32 value.
+ return std_Math_floor(value) | 0;
+}
+
+/**
+ * Extracts a property value from the provided options object, converts it to a
+ * Number value, checks whether it is in the allowed range, and fills in a
+ * fallback value if necessary.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.12.
+ */
+function GetNumberOption(options, property, minimum, maximum, fallback) {
+ // Steps 1-2.
+ return DefaultNumberOption(options[property], minimum, maximum, fallback);
+}
+
+// Symbols in the self-hosting compartment can't be cloned, use a separate
+// object to hold the actual symbol value.
+// TODO: Can we add support to clone symbols?
+var intlFallbackSymbolHolder = { value: undefined };
+
+/**
+ * The [[FallbackSymbol]] symbol of the %Intl% intrinsic object.
+ *
+ * This symbol is used to implement the legacy constructor semantics for
+ * Intl.DateTimeFormat and Intl.NumberFormat.
+ */
+function intlFallbackSymbol() {
+ var fallbackSymbol = intlFallbackSymbolHolder.value;
+ if (!fallbackSymbol) {
+ let Symbol = GetBuiltinConstructor("Symbol");
+ fallbackSymbol = Symbol("IntlLegacyConstructedSymbol");
+ intlFallbackSymbolHolder.value = fallbackSymbol;
+ }
+ return fallbackSymbol;
+}
+
+/**
+ * Initializes the INTL_INTERNALS_OBJECT_SLOT of the given object.
+ */
+function initializeIntlObject(obj, type, lazyData) {
+ assert(IsObject(obj), "Non-object passed to initializeIntlObject");
+ assert(
+ (type === "Collator" && intl_GuardToCollator(obj) !== null) ||
+ (type === "DateTimeFormat" && intl_GuardToDateTimeFormat(obj) !== null) ||
+ (type === "DisplayNames" && intl_GuardToDisplayNames(obj) !== null) ||
+ (type === "ListFormat" && intl_GuardToListFormat(obj) !== null) ||
+ (type === "NumberFormat" && intl_GuardToNumberFormat(obj) !== null) ||
+ (type === "PluralRules" && intl_GuardToPluralRules(obj) !== null) ||
+ (type === "RelativeTimeFormat" &&
+ intl_GuardToRelativeTimeFormat(obj) !== null),
+ "type must match the object's class"
+ );
+ assert(IsObject(lazyData), "non-object lazy data");
+
+ // The meaning of an internals object for an object |obj| is as follows.
+ //
+ // The .type property indicates the type of Intl object that |obj| is. It
+ // must be one of:
+ // - Collator
+ // - DateTimeFormat
+ // - DisplayNames
+ // - ListFormat
+ // - NumberFormat
+ // - PluralRules
+ // - RelativeTimeFormat
+ //
+ // The .lazyData property stores information needed to compute -- without
+ // observable side effects -- the actual internal Intl properties of
+ // |obj|. If it is non-null, then the actual internal properties haven't
+ // been computed, and .lazyData must be processed by
+ // |setInternalProperties| before internal Intl property values are
+ // available. If it is null, then the .internalProps property contains an
+ // object whose properties are the internal Intl properties of |obj|.
+
+ var internals = std_Object_create(null);
+ internals.type = type;
+ internals.lazyData = lazyData;
+ internals.internalProps = null;
+
+ assert(
+ UnsafeGetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT) === undefined,
+ "Internal slot already initialized?"
+ );
+ UnsafeSetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT, internals);
+}
+
+/**
+ * Set the internal properties object for an |internals| object previously
+ * associated with lazy data.
+ */
+function setInternalProperties(internals, internalProps) {
+ assert(IsObject(internals.lazyData), "lazy data must exist already");
+ assert(IsObject(internalProps), "internalProps argument should be an object");
+
+ // Set in reverse order so that the .lazyData nulling is a barrier.
+ internals.internalProps = internalProps;
+ internals.lazyData = null;
+}
+
+/**
+ * Get the existing internal properties out of a non-newborn |internals|, or
+ * null if none have been computed.
+ */
+function maybeInternalProperties(internals) {
+ assert(IsObject(internals), "non-object passed to maybeInternalProperties");
+ var lazyData = internals.lazyData;
+ if (lazyData) {
+ return null;
+ }
+ assert(
+ IsObject(internals.internalProps),
+ "missing lazy data and computed internals"
+ );
+ return internals.internalProps;
+}
+
+/**
+ * Return |obj|'s internals object (*not* the object holding its internal
+ * properties!), with structure specified above.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 10.3.
+ * Spec: ECMAScript Internationalization API Specification, 11.3.
+ * Spec: ECMAScript Internationalization API Specification, 12.3.
+ */
+function getIntlObjectInternals(obj) {
+ assert(IsObject(obj), "getIntlObjectInternals called with non-Object");
+ assert(
+ intl_GuardToCollator(obj) !== null ||
+ intl_GuardToDateTimeFormat(obj) !== null ||
+ intl_GuardToDisplayNames(obj) !== null ||
+ intl_GuardToListFormat(obj) !== null ||
+ intl_GuardToNumberFormat(obj) !== null ||
+ intl_GuardToPluralRules(obj) !== null ||
+ intl_GuardToRelativeTimeFormat(obj) !== null,
+ "getIntlObjectInternals called with non-Intl object"
+ );
+
+ var internals = UnsafeGetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT);
+
+ assert(IsObject(internals), "internals not an object");
+ assert(hasOwn("type", internals), "missing type");
+ assert(
+ (internals.type === "Collator" && intl_GuardToCollator(obj) !== null) ||
+ (internals.type === "DateTimeFormat" &&
+ intl_GuardToDateTimeFormat(obj) !== null) ||
+ (internals.type === "DisplayNames" &&
+ intl_GuardToDisplayNames(obj) !== null) ||
+ (internals.type === "ListFormat" &&
+ intl_GuardToListFormat(obj) !== null) ||
+ (internals.type === "NumberFormat" &&
+ intl_GuardToNumberFormat(obj) !== null) ||
+ (internals.type === "PluralRules" &&
+ intl_GuardToPluralRules(obj) !== null) ||
+ (internals.type === "RelativeTimeFormat" &&
+ intl_GuardToRelativeTimeFormat(obj) !== null),
+ "type must match the object's class"
+ );
+ assert(hasOwn("lazyData", internals), "missing lazyData");
+ assert(hasOwn("internalProps", internals), "missing internalProps");
+
+ return internals;
+}
+
+/**
+ * Get the internal properties of known-Intl object |obj|. For use only by
+ * C++ code that knows what it's doing!
+ */
+function getInternals(obj) {
+ var internals = getIntlObjectInternals(obj);
+
+ // If internal properties have already been computed, use them.
+ var internalProps = maybeInternalProperties(internals);
+ if (internalProps) {
+ return internalProps;
+ }
+
+ // Otherwise it's time to fully create them.
+ var type = internals.type;
+ if (type === "Collator") {
+ internalProps = resolveCollatorInternals(internals.lazyData);
+ } else if (type === "DateTimeFormat") {
+ internalProps = resolveDateTimeFormatInternals(internals.lazyData);
+ } else if (type === "DisplayNames") {
+ internalProps = resolveDisplayNamesInternals(internals.lazyData);
+ } else if (type === "ListFormat") {
+ internalProps = resolveListFormatInternals(internals.lazyData);
+ } else if (type === "NumberFormat") {
+ internalProps = resolveNumberFormatInternals(internals.lazyData);
+ } else if (type === "PluralRules") {
+ internalProps = resolvePluralRulesInternals(internals.lazyData);
+ } else {
+ internalProps = resolveRelativeTimeFormatInternals(internals.lazyData);
+ }
+ setInternalProperties(internals, internalProps);
+ return internalProps;
+}
diff --git a/js/src/builtin/intl/CurrencyDataGenerated.js b/js/src/builtin/intl/CurrencyDataGenerated.js
new file mode 100644
index 0000000000..b2fed80bd6
--- /dev/null
+++ b/js/src/builtin/intl/CurrencyDataGenerated.js
@@ -0,0 +1,78 @@
+// Generated by make_intl_data.py. DO NOT EDIT.
+// Version: 2022-04-01
+
+/**
+ * Mapping from currency codes to the number of decimal digits used for them.
+ * Default is 2 digits.
+ *
+ * Spec: ISO 4217 Currency and Funds Code List.
+ * http://www.currency-iso.org/en/home/tables/table-a1.html
+ */
+var currencyDigits = {
+ // Bahraini Dinar (BAHRAIN)
+ BHD: 3,
+ // Burundi Franc (BURUNDI)
+ BIF: 0,
+ // Unidad de Fomento (CHILE)
+ CLF: 4,
+ // Chilean Peso (CHILE)
+ CLP: 0,
+ // Djibouti Franc (DJIBOUTI)
+ DJF: 0,
+ // Guinean Franc (GUINEA)
+ GNF: 0,
+ // Iraqi Dinar (IRAQ)
+ IQD: 3,
+ // Iceland Krona (ICELAND)
+ ISK: 0,
+ // Jordanian Dinar (JORDAN)
+ JOD: 3,
+ // Yen (JAPAN)
+ JPY: 0,
+ // Comorian Franc (COMOROS (THE))
+ KMF: 0,
+ // Won (KOREA (THE REPUBLIC OF))
+ KRW: 0,
+ // Kuwaiti Dinar (KUWAIT)
+ KWD: 3,
+ // Libyan Dinar (LIBYA)
+ LYD: 3,
+ // Rial Omani (OMAN)
+ OMR: 3,
+ // Guarani (PARAGUAY)
+ PYG: 0,
+ // Rwanda Franc (RWANDA)
+ RWF: 0,
+ // Tunisian Dinar (TUNISIA)
+ TND: 3,
+ // Uganda Shilling (UGANDA)
+ UGX: 0,
+ // Uruguay Peso en Unidades Indexadas (UI) (URUGUAY)
+ UYI: 0,
+ // Unidad Previsional (URUGUAY)
+ UYW: 4,
+ // Dong (VIET NAM)
+ VND: 0,
+ // Vatu (VANUATU)
+ VUV: 0,
+ // CFA Franc BEAC (CAMEROON)
+ // CFA Franc BEAC (CENTRAL AFRICAN REPUBLIC (THE))
+ // CFA Franc BEAC (CHAD)
+ // CFA Franc BEAC (CONGO (THE))
+ // CFA Franc BEAC (EQUATORIAL GUINEA)
+ // CFA Franc BEAC (GABON)
+ XAF: 0,
+ // CFA Franc BCEAO (BENIN)
+ // CFA Franc BCEAO (BURKINA FASO)
+ // CFA Franc BCEAO (CÔTE D'IVOIRE)
+ // CFA Franc BCEAO (GUINEA-BISSAU)
+ // CFA Franc BCEAO (MALI)
+ // CFA Franc BCEAO (NIGER (THE))
+ // CFA Franc BCEAO (SENEGAL)
+ // CFA Franc BCEAO (TOGO)
+ XOF: 0,
+ // CFP Franc (FRENCH POLYNESIA)
+ // CFP Franc (NEW CALEDONIA)
+ // CFP Franc (WALLIS AND FUTUNA)
+ XPF: 0,
+};
diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp
new file mode 100644
index 0000000000..bb5f259b9a
--- /dev/null
+++ b/js/src/builtin/intl/DateTimeFormat.cpp
@@ -0,0 +1,1580 @@
+/* -*- 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.DateTimeFormat implementation. */
+
+#include "builtin/intl/DateTimeFormat.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/Calendar.h"
+#include "mozilla/intl/DateIntervalFormat.h"
+#include "mozilla/intl/DateTimeFormat.h"
+#include "mozilla/intl/DateTimePart.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/intl/TimeZone.h"
+#include "mozilla/Range.h"
+#include "mozilla/Span.h"
+
+#include "builtin/Array.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/FormatBuffer.h"
+#include "builtin/intl/LanguageTag.h"
+#include "builtin/intl/SharedIntlData.h"
+#include "gc/GCContext.h"
+#include "js/Date.h"
+#include "js/experimental/Intl.h" // JS::AddMozDateTimeFormatConstructor
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "js/GCAPI.h"
+#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties
+#include "js/PropertySpec.h"
+#include "js/StableStringChars.h"
+#include "vm/DateTime.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/Runtime.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/GeckoProfiler-inl.h"
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+using JS::AutoStableStringChars;
+using JS::ClippedTime;
+using JS::TimeClip;
+
+using js::intl::DateTimeFormatOptions;
+using js::intl::FormatBuffer;
+using js::intl::INITIAL_CHAR_BUFFER_SIZE;
+using js::intl::SharedIntlData;
+
+const JSClassOps DateTimeFormatObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ DateTimeFormatObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ nullptr, // trace
+};
+
+const JSClass DateTimeFormatObject::class_ = {
+ "Intl.DateTimeFormat",
+ JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_DateTimeFormat) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &DateTimeFormatObject::classOps_, &DateTimeFormatObject::classSpec_};
+
+const JSClass& DateTimeFormatObject::protoClass_ = PlainObject::class_;
+
+static bool dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().DateTimeFormat);
+ return true;
+}
+
+static const JSFunctionSpec dateTimeFormat_static_methods[] = {
+ JS_SELF_HOSTED_FN("supportedLocalesOf",
+ "Intl_DateTimeFormat_supportedLocalesOf", 1, 0),
+ JS_FS_END};
+
+static const JSFunctionSpec dateTimeFormat_methods[] = {
+ JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions",
+ 0, 0),
+ JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 1,
+ 0),
+ JS_SELF_HOSTED_FN("formatRange", "Intl_DateTimeFormat_formatRange", 2, 0),
+ JS_SELF_HOSTED_FN("formatRangeToParts",
+ "Intl_DateTimeFormat_formatRangeToParts", 2, 0),
+ JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0),
+ JS_FS_END};
+
+static const JSPropertySpec dateTimeFormat_properties[] = {
+ JS_SELF_HOSTED_GET("format", "$Intl_DateTimeFormat_format_get", 0),
+ JS_STRING_SYM_PS(toStringTag, "Intl.DateTimeFormat", JSPROP_READONLY),
+ JS_PS_END};
+
+static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp);
+
+const ClassSpec DateTimeFormatObject::classSpec_ = {
+ GenericCreateConstructor<DateTimeFormat, 0, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<DateTimeFormatObject>,
+ dateTimeFormat_static_methods,
+ nullptr,
+ dateTimeFormat_methods,
+ dateTimeFormat_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+/**
+ * 12.2.1 Intl.DateTimeFormat([ locales [, options]])
+ *
+ * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
+ */
+static bool DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct,
+ DateTimeFormatOptions dtfOptions) {
+ AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.DateTimeFormat");
+
+ // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
+
+ // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+ JSProtoKey protoKey = dtfOptions == DateTimeFormatOptions::Standard
+ ? JSProto_DateTimeFormat
+ : JSProto_Null;
+ RootedObject proto(cx);
+ if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey, &proto)) {
+ return false;
+ }
+
+ Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
+ dateTimeFormat = NewObjectWithClassProto<DateTimeFormatObject>(cx, proto);
+ if (!dateTimeFormat) {
+ return false;
+ }
+
+ RootedValue thisValue(
+ cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv());
+ HandleValue locales = args.get(0);
+ HandleValue options = args.get(1);
+
+ // Step 3.
+ return intl::LegacyInitializeObject(
+ cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, thisValue,
+ locales, options, dtfOptions, args.rval());
+}
+
+static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ return DateTimeFormat(cx, args, args.isConstructing(),
+ DateTimeFormatOptions::Standard);
+}
+
+static bool MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // Don't allow to call mozIntl.DateTimeFormat as a function. That way we
+ // don't need to worry how to handle the legacy initialization semantics
+ // when applied on mozIntl.DateTimeFormat.
+ if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) {
+ return false;
+ }
+
+ return DateTimeFormat(cx, args, true,
+ DateTimeFormatOptions::EnableMozExtensions);
+}
+
+bool js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 2);
+ MOZ_ASSERT(!args.isConstructing());
+ // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it
+ // cannot be used with "new", but it still has to be treated as a
+ // constructor.
+ return DateTimeFormat(cx, args, true, DateTimeFormatOptions::Standard);
+}
+
+void js::DateTimeFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ MOZ_ASSERT(gcx->onMainThread());
+
+ auto* dateTimeFormat = &obj->as<DateTimeFormatObject>();
+ mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat();
+ mozilla::intl::DateIntervalFormat* dif =
+ dateTimeFormat->getDateIntervalFormat();
+
+ if (df) {
+ intl::RemoveICUCellMemory(
+ gcx, obj, DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
+
+ delete df;
+ }
+
+ if (dif) {
+ intl::RemoveICUCellMemory(
+ gcx, obj, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
+
+ delete dif;
+ }
+}
+
+bool JS::AddMozDateTimeFormatConstructor(JSContext* cx,
+ JS::Handle<JSObject*> intl) {
+ RootedObject ctor(
+ cx, GlobalObject::createConstructor(cx, MozDateTimeFormat,
+ cx->names().DateTimeFormat, 0));
+ if (!ctor) {
+ return false;
+ }
+
+ RootedObject proto(
+ cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global()));
+ if (!proto) {
+ return false;
+ }
+
+ if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
+ return false;
+ }
+
+ // 12.3.2
+ if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) {
+ return false;
+ }
+
+ // 12.4.4 and 12.4.5
+ if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) {
+ return false;
+ }
+
+ // 12.4.2 and 12.4.3
+ if (!JS_DefineProperties(cx, proto, dateTimeFormat_properties)) {
+ return false;
+ }
+
+ RootedValue ctorValue(cx, ObjectValue(*ctor));
+ return DefineDataProperty(cx, intl, cx->names().DateTimeFormat, ctorValue, 0);
+}
+
+static bool DefaultCalendar(JSContext* cx, const UniqueChars& locale,
+ MutableHandleValue rval) {
+ auto calendar = mozilla::intl::Calendar::TryCreate(locale.get());
+ if (calendar.isErr()) {
+ intl::ReportInternalError(cx, calendar.unwrapErr());
+ return false;
+ }
+
+ auto type = calendar.unwrap()->GetBcp47Type();
+ if (type.isErr()) {
+ intl::ReportInternalError(cx, type.unwrapErr());
+ return false;
+ }
+
+ JSString* str = NewStringCopy<CanGC>(cx, type.unwrap());
+ if (!str) {
+ return false;
+ }
+
+ rval.setString(str);
+ return true;
+}
+
+bool js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+
+ RootedObject calendars(cx, NewDenseEmptyArray(cx));
+ if (!calendars) {
+ return false;
+ }
+
+ // We need the default calendar for the locale as the first result.
+ RootedValue defaultCalendar(cx);
+ if (!DefaultCalendar(cx, locale, &defaultCalendar)) {
+ return false;
+ }
+
+ if (!NewbornArrayPush(cx, calendars, defaultCalendar)) {
+ return false;
+ }
+
+ // Now get the calendars that "would make a difference", i.e., not the
+ // default.
+ auto keywords =
+ mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale(locale.get());
+ if (keywords.isErr()) {
+ intl::ReportInternalError(cx, keywords.unwrapErr());
+ return false;
+ }
+
+ for (auto keyword : keywords.unwrap()) {
+ if (keyword.isErr()) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ JSString* jscalendar = NewStringCopy<CanGC>(cx, keyword.unwrap());
+ if (!jscalendar) {
+ return false;
+ }
+ if (!NewbornArrayPush(cx, calendars, StringValue(jscalendar))) {
+ return false;
+ }
+ }
+
+ args.rval().setObject(*calendars);
+ return true;
+}
+
+bool js::intl_defaultCalendar(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+
+ return DefaultCalendar(cx, locale, args.rval());
+}
+
+bool js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+
+ RootedString timeZone(cx, args[0].toString());
+ Rooted<JSAtom*> validatedTimeZone(cx);
+ if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) {
+ return false;
+ }
+
+ if (validatedTimeZone) {
+ cx->markAtom(validatedTimeZone);
+ args.rval().setString(validatedTimeZone);
+ } else {
+ args.rval().setNull();
+ }
+
+ return true;
+}
+
+bool js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+
+ // Some time zone names are canonicalized differently by ICU -- handle
+ // those first:
+ RootedString timeZone(cx, args[0].toString());
+ Rooted<JSAtom*> ianaTimeZone(cx);
+ if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(
+ cx, timeZone, &ianaTimeZone)) {
+ return false;
+ }
+
+ if (ianaTimeZone) {
+ cx->markAtom(ianaTimeZone);
+ args.rval().setString(ianaTimeZone);
+ return true;
+ }
+
+ AutoStableStringChars stableChars(cx);
+ if (!stableChars.initTwoByte(cx, timeZone)) {
+ return false;
+ }
+
+ FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> canonicalTimeZone(cx);
+ auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID(
+ stableChars.twoByteRange(), canonicalTimeZone);
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ JSString* str = canonicalTimeZone.toString(cx);
+ if (!str) {
+ return false;
+ }
+
+ args.rval().setString(str);
+ return true;
+}
+
+bool js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 0);
+
+ // The current default might be stale, because JS::ResetTimeZone() doesn't
+ // immediately update ICU's default time zone. So perform an update if
+ // needed.
+ js::ResyncICUDefaultTimeZone();
+
+ FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> timeZone(cx);
+ auto result = mozilla::intl::TimeZone::GetDefaultTimeZone(timeZone);
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ JSString* str = timeZone.toString(cx);
+ if (!str) {
+ return false;
+ }
+
+ args.rval().setString(str);
+ return true;
+}
+
+bool js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 0);
+
+ auto timeZone = mozilla::intl::TimeZone::TryCreate();
+ if (timeZone.isErr()) {
+ intl::ReportInternalError(cx, timeZone.unwrapErr());
+ return false;
+ }
+
+ auto offset = timeZone.unwrap()->GetRawOffsetMs();
+ if (offset.isErr()) {
+ intl::ReportInternalError(cx, offset.unwrapErr());
+ return false;
+ }
+
+ args.rval().setInt32(offset.unwrap());
+ return true;
+}
+
+bool js::intl_isDefaultTimeZone(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString() || args[0].isUndefined());
+
+ // |undefined| is the default value when the Intl runtime caches haven't
+ // yet been initialized. Handle it the same way as a cache miss.
+ if (args[0].isUndefined()) {
+ args.rval().setBoolean(false);
+ return true;
+ }
+
+ // The current default might be stale, because JS::ResetTimeZone() doesn't
+ // immediately update ICU's default time zone. So perform an update if
+ // needed.
+ js::ResyncICUDefaultTimeZone();
+
+ FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> chars(cx);
+ auto result = mozilla::intl::TimeZone::GetDefaultTimeZone(chars);
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ JSLinearString* str = args[0].toString()->ensureLinear(cx);
+ if (!str) {
+ return false;
+ }
+
+ bool equals;
+ if (str->length() == chars.length()) {
+ JS::AutoCheckCannotGC nogc;
+ equals =
+ str->hasLatin1Chars()
+ ? EqualChars(str->latin1Chars(nogc), chars.data(), str->length())
+ : EqualChars(str->twoByteChars(nogc), chars.data(), str->length());
+ } else {
+ equals = false;
+ }
+
+ args.rval().setBoolean(equals);
+ return true;
+}
+
+enum class HourCycle {
+ // 12 hour cycle, from 0 to 11.
+ H11,
+
+ // 12 hour cycle, from 1 to 12.
+ H12,
+
+ // 24 hour cycle, from 0 to 23.
+ H23,
+
+ // 24 hour cycle, from 1 to 24.
+ H24
+};
+
+static UniqueChars DateTimeFormatLocale(
+ JSContext* cx, HandleObject internals,
+ mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hourCycle =
+ mozilla::Nothing()) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
+ return nullptr;
+ }
+
+ // ICU expects calendar, numberingSystem, and hourCycle as Unicode locale
+ // extensions on locale.
+
+ mozilla::intl::Locale tag;
+ {
+ Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx));
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!intl::ParseLocale(cx, locale, tag)) {
+ return nullptr;
+ }
+ }
+
+ JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
+
+ if (!GetProperty(cx, internals, internals, cx->names().calendar, &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* calendar = value.toString()->ensureLinear(cx);
+ if (!calendar) {
+ return nullptr;
+ }
+
+ if (!keywords.emplaceBack("ca", calendar)) {
+ return nullptr;
+ }
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
+ &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
+ if (!numberingSystem) {
+ return nullptr;
+ }
+
+ if (!keywords.emplaceBack("nu", numberingSystem)) {
+ return nullptr;
+ }
+ }
+
+ if (hourCycle) {
+ JSAtom* hourCycleStr;
+ switch (*hourCycle) {
+ case mozilla::intl::DateTimeFormat::HourCycle::H11:
+ hourCycleStr = cx->names().h11;
+ break;
+ case mozilla::intl::DateTimeFormat::HourCycle::H12:
+ hourCycleStr = cx->names().h12;
+ break;
+ case mozilla::intl::DateTimeFormat::HourCycle::H23:
+ hourCycleStr = cx->names().h23;
+ break;
+ case mozilla::intl::DateTimeFormat::HourCycle::H24:
+ hourCycleStr = cx->names().h24;
+ break;
+ }
+
+ if (!keywords.emplaceBack("hc", hourCycleStr)) {
+ return nullptr;
+ }
+ }
+
+ // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
+ // the Unicode extension subtag. We're then relying on ICU to follow RFC
+ // 6067, which states that any trailing keywords using the same key
+ // should be ignored.
+ if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
+ return nullptr;
+ }
+
+ FormatBuffer<char> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+ return buffer.extractStringZ();
+}
+
+static bool AssignTextComponent(
+ JSContext* cx, HandleObject internals, Handle<PropertyName*> property,
+ mozilla::Maybe<mozilla::intl::DateTimeFormat::Text>* text) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, property, &value)) {
+ return false;
+ }
+
+ if (value.isString()) {
+ JSLinearString* string = value.toString()->ensureLinear(cx);
+ if (!string) {
+ return false;
+ }
+ if (StringEqualsLiteral(string, "narrow")) {
+ *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Narrow);
+ } else if (StringEqualsLiteral(string, "short")) {
+ *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short);
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(string, "long"));
+ *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Long);
+ }
+ } else {
+ MOZ_ASSERT(value.isUndefined());
+ }
+
+ return true;
+}
+
+static bool AssignNumericComponent(
+ JSContext* cx, HandleObject internals, Handle<PropertyName*> property,
+ mozilla::Maybe<mozilla::intl::DateTimeFormat::Numeric>* numeric) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, property, &value)) {
+ return false;
+ }
+
+ if (value.isString()) {
+ JSLinearString* string = value.toString()->ensureLinear(cx);
+ if (!string) {
+ return false;
+ }
+ if (StringEqualsLiteral(string, "numeric")) {
+ *numeric = mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::Numeric);
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(string, "2-digit"));
+ *numeric =
+ mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::TwoDigit);
+ }
+ } else {
+ MOZ_ASSERT(value.isUndefined());
+ }
+
+ return true;
+}
+
+static bool AssignMonthComponent(
+ JSContext* cx, HandleObject internals, Handle<PropertyName*> property,
+ mozilla::Maybe<mozilla::intl::DateTimeFormat::Month>* month) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, property, &value)) {
+ return false;
+ }
+
+ if (value.isString()) {
+ JSLinearString* string = value.toString()->ensureLinear(cx);
+ if (!string) {
+ return false;
+ }
+ if (StringEqualsLiteral(string, "numeric")) {
+ *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Numeric);
+ } else if (StringEqualsLiteral(string, "2-digit")) {
+ *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::TwoDigit);
+ } else if (StringEqualsLiteral(string, "long")) {
+ *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Long);
+ } else if (StringEqualsLiteral(string, "short")) {
+ *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Short);
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(string, "narrow"));
+ *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Narrow);
+ }
+ } else {
+ MOZ_ASSERT(value.isUndefined());
+ }
+
+ return true;
+}
+
+static bool AssignTimeZoneNameComponent(
+ JSContext* cx, HandleObject internals, Handle<PropertyName*> property,
+ mozilla::Maybe<mozilla::intl::DateTimeFormat::TimeZoneName>* tzName) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, property, &value)) {
+ return false;
+ }
+
+ if (value.isString()) {
+ JSLinearString* string = value.toString()->ensureLinear(cx);
+ if (!string) {
+ return false;
+ }
+ if (StringEqualsLiteral(string, "long")) {
+ *tzName =
+ mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Long);
+ } else if (StringEqualsLiteral(string, "short")) {
+ *tzName =
+ mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Short);
+ } else if (StringEqualsLiteral(string, "shortOffset")) {
+ *tzName = mozilla::Some(
+ mozilla::intl::DateTimeFormat::TimeZoneName::ShortOffset);
+ } else if (StringEqualsLiteral(string, "longOffset")) {
+ *tzName = mozilla::Some(
+ mozilla::intl::DateTimeFormat::TimeZoneName::LongOffset);
+ } else if (StringEqualsLiteral(string, "shortGeneric")) {
+ *tzName = mozilla::Some(
+ mozilla::intl::DateTimeFormat::TimeZoneName::ShortGeneric);
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(string, "longGeneric"));
+ *tzName = mozilla::Some(
+ mozilla::intl::DateTimeFormat::TimeZoneName::LongGeneric);
+ }
+ } else {
+ MOZ_ASSERT(value.isUndefined());
+ }
+
+ return true;
+}
+
+static bool AssignHourCycleComponent(
+ JSContext* cx, HandleObject internals, Handle<PropertyName*> property,
+ mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle>* hourCycle) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, property, &value)) {
+ return false;
+ }
+
+ if (value.isString()) {
+ JSLinearString* string = value.toString()->ensureLinear(cx);
+ if (!string) {
+ return false;
+ }
+ if (StringEqualsLiteral(string, "h11")) {
+ *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H11);
+ } else if (StringEqualsLiteral(string, "h12")) {
+ *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H12);
+ } else if (StringEqualsLiteral(string, "h23")) {
+ *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H23);
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(string, "h24"));
+ *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H24);
+ }
+ } else {
+ MOZ_ASSERT(value.isUndefined());
+ }
+
+ return true;
+}
+
+static bool AssignHour12Component(JSContext* cx, HandleObject internals,
+ mozilla::Maybe<bool>* hour12) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, cx->names().hour12, &value)) {
+ return false;
+ }
+ if (value.isBoolean()) {
+ *hour12 = mozilla::Some(value.toBoolean());
+ } else {
+ MOZ_ASSERT(value.isUndefined());
+ }
+
+ return true;
+}
+
+static bool AssignDateTimeLength(
+ JSContext* cx, HandleObject internals, Handle<PropertyName*> property,
+ mozilla::Maybe<mozilla::intl::DateTimeFormat::Style>* style) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, property, &value)) {
+ return false;
+ }
+
+ if (value.isString()) {
+ JSLinearString* string = value.toString()->ensureLinear(cx);
+ if (!string) {
+ return false;
+ }
+ if (StringEqualsLiteral(string, "full")) {
+ *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Full);
+ } else if (StringEqualsLiteral(string, "long")) {
+ *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ } else if (StringEqualsLiteral(string, "medium")) {
+ *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Medium);
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(string, "short"));
+ *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ }
+ } else {
+ MOZ_ASSERT(value.isUndefined());
+ }
+
+ return true;
+}
+
+/**
+ * Returns a new mozilla::intl::DateTimeFormat with the locale and date-time
+ * formatting options of the given DateTimeFormat.
+ */
+static mozilla::intl::DateTimeFormat* NewDateTimeFormat(
+ JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) {
+ RootedValue value(cx);
+
+ RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat));
+ if (!internals) {
+ return nullptr;
+ }
+
+ UniqueChars locale = DateTimeFormatLocale(cx, internals);
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) {
+ return nullptr;
+ }
+
+ AutoStableStringChars timeZone(cx);
+ if (!timeZone.initTwoByte(cx, value.toString())) {
+ return nullptr;
+ }
+
+ mozilla::Range<const char16_t> timeZoneChars = timeZone.twoByteRange();
+
+ if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) {
+ return nullptr;
+ }
+ bool hasPattern = value.isString();
+
+ if (!GetProperty(cx, internals, internals, cx->names().timeStyle, &value)) {
+ return nullptr;
+ }
+ bool hasStyle = value.isString();
+ if (!hasStyle) {
+ if (!GetProperty(cx, internals, internals, cx->names().dateStyle, &value)) {
+ return nullptr;
+ }
+ hasStyle = value.isString();
+ }
+
+ mozilla::UniquePtr<mozilla::intl::DateTimeFormat> df = nullptr;
+ if (hasPattern) {
+ // This is a DateTimeFormat defined by a pattern option. This is internal
+ // to Mozilla, and not part of the ECMA-402 API.
+ if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) {
+ return nullptr;
+ }
+
+ AutoStableStringChars pattern(cx);
+ if (!pattern.initTwoByte(cx, value.toString())) {
+ return nullptr;
+ }
+
+ auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromPattern(
+ mozilla::MakeStringSpan(locale.get()), pattern.twoByteRange(),
+ mozilla::Some(timeZoneChars));
+ if (dfResult.isErr()) {
+ intl::ReportInternalError(cx, dfResult.unwrapErr());
+ return nullptr;
+ }
+
+ df = dfResult.unwrap();
+ } else if (hasStyle) {
+ // This is a DateTimeFormat defined by a time style or date style.
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ if (!AssignDateTimeLength(cx, internals, cx->names().timeStyle,
+ &style.time)) {
+ return nullptr;
+ }
+ if (!AssignDateTimeLength(cx, internals, cx->names().dateStyle,
+ &style.date)) {
+ return nullptr;
+ }
+ if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle,
+ &style.hourCycle)) {
+ return nullptr;
+ }
+
+ if (!AssignHour12Component(cx, internals, &style.hour12)) {
+ return nullptr;
+ }
+
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+
+ mozilla::intl::DateTimePatternGenerator* gen =
+ sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
+ if (!gen) {
+ return nullptr;
+ }
+ auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromStyle(
+ mozilla::MakeStringSpan(locale.get()), style, gen,
+ mozilla::Some(timeZoneChars));
+ if (dfResult.isErr()) {
+ intl::ReportInternalError(cx, dfResult.unwrapErr());
+ return nullptr;
+ }
+ df = dfResult.unwrap();
+ } else {
+ // This is a DateTimeFormat defined by a components bag.
+ mozilla::intl::DateTimeFormat::ComponentsBag bag;
+
+ if (!AssignTextComponent(cx, internals, cx->names().era, &bag.era)) {
+ return nullptr;
+ }
+ if (!AssignNumericComponent(cx, internals, cx->names().year, &bag.year)) {
+ return nullptr;
+ }
+ if (!AssignMonthComponent(cx, internals, cx->names().month, &bag.month)) {
+ return nullptr;
+ }
+ if (!AssignNumericComponent(cx, internals, cx->names().day, &bag.day)) {
+ return nullptr;
+ }
+ if (!AssignTextComponent(cx, internals, cx->names().weekday,
+ &bag.weekday)) {
+ return nullptr;
+ }
+ if (!AssignNumericComponent(cx, internals, cx->names().hour, &bag.hour)) {
+ return nullptr;
+ }
+ if (!AssignNumericComponent(cx, internals, cx->names().minute,
+ &bag.minute)) {
+ return nullptr;
+ }
+ if (!AssignNumericComponent(cx, internals, cx->names().second,
+ &bag.second)) {
+ return nullptr;
+ }
+ if (!AssignTimeZoneNameComponent(cx, internals, cx->names().timeZoneName,
+ &bag.timeZoneName)) {
+ return nullptr;
+ }
+ if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle,
+ &bag.hourCycle)) {
+ return nullptr;
+ }
+ if (!AssignTextComponent(cx, internals, cx->names().dayPeriod,
+ &bag.dayPeriod)) {
+ return nullptr;
+ }
+ if (!AssignHour12Component(cx, internals, &bag.hour12)) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals,
+ cx->names().fractionalSecondDigits, &value)) {
+ return nullptr;
+ }
+ if (value.isInt32()) {
+ bag.fractionalSecondDigits = mozilla::Some(value.toInt32());
+ } else {
+ MOZ_ASSERT(value.isUndefined());
+ }
+
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+ auto* dtpg = sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
+ if (!dtpg) {
+ return nullptr;
+ }
+
+ auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromComponents(
+ mozilla::MakeStringSpan(locale.get()), bag, dtpg,
+ mozilla::Some(timeZoneChars));
+ if (dfResult.isErr()) {
+ intl::ReportInternalError(cx, dfResult.unwrapErr());
+ return nullptr;
+ }
+ df = dfResult.unwrap();
+ }
+
+ // ECMAScript requires the Gregorian calendar to be used from the beginning
+ // of ECMAScript time.
+ df->SetStartTimeIfGregorian(StartOfTime);
+
+ return df.release();
+}
+
+static mozilla::intl::DateTimeFormat* GetOrCreateDateTimeFormat(
+ JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) {
+ // Obtain a cached mozilla::intl::DateTimeFormat object.
+ mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat();
+ if (df) {
+ return df;
+ }
+
+ df = NewDateTimeFormat(cx, dateTimeFormat);
+ if (!df) {
+ return nullptr;
+ }
+ dateTimeFormat->setDateFormat(df);
+
+ intl::AddICUCellMemory(dateTimeFormat,
+ DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
+ return df;
+}
+
+template <typename T>
+static bool SetResolvedProperty(JSContext* cx, HandleObject resolved,
+ Handle<PropertyName*> name,
+ mozilla::Maybe<T> intlProp) {
+ if (!intlProp) {
+ return true;
+ }
+ JSString* str = NewStringCopyZ<CanGC>(
+ cx, mozilla::intl::DateTimeFormat::ToString(*intlProp));
+ if (!str) {
+ return false;
+ }
+ RootedValue value(cx, StringValue(str));
+ return DefineDataProperty(cx, resolved, name, value);
+}
+
+bool js::intl_resolveDateTimeFormatComponents(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+ MOZ_ASSERT(args[0].isObject());
+ MOZ_ASSERT(args[1].isObject());
+ MOZ_ASSERT(args[2].isBoolean());
+
+ Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
+ dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
+
+ RootedObject resolved(cx, &args[1].toObject());
+
+ bool includeDateTimeFields = args[2].toBoolean();
+
+ mozilla::intl::DateTimeFormat* df =
+ GetOrCreateDateTimeFormat(cx, dateTimeFormat);
+ if (!df) {
+ return false;
+ }
+
+ auto result = df->ResolveComponents();
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ mozilla::intl::DateTimeFormat::ComponentsBag components = result.unwrap();
+
+ // Map the resolved mozilla::intl::DateTimeFormat::ComponentsBag to the
+ // options object as returned by DateTimeFormat.prototype.resolvedOptions.
+ //
+ // Resolved options must match the ordering as defined in:
+ // https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions
+
+ if (!SetResolvedProperty(cx, resolved, cx->names().hourCycle,
+ components.hourCycle)) {
+ return false;
+ }
+
+ if (components.hour12) {
+ RootedValue value(cx, BooleanValue(*components.hour12));
+ if (!DefineDataProperty(cx, resolved, cx->names().hour12, value)) {
+ return false;
+ }
+ }
+
+ if (!includeDateTimeFields) {
+ args.rval().setUndefined();
+ // Do not include date time fields.
+ return true;
+ }
+
+ if (!SetResolvedProperty(cx, resolved, cx->names().weekday,
+ components.weekday)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().era, components.era)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().year, components.year)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().month, components.month)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().day, components.day)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().dayPeriod,
+ components.dayPeriod)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().hour, components.hour)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().minute,
+ components.minute)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().second,
+ components.second)) {
+ return false;
+ }
+ if (!SetResolvedProperty(cx, resolved, cx->names().timeZoneName,
+ components.timeZoneName)) {
+ return false;
+ }
+
+ if (components.fractionalSecondDigits) {
+ RootedValue value(cx, Int32Value(*components.fractionalSecondDigits));
+ if (!DefineDataProperty(cx, resolved, cx->names().fractionalSecondDigits,
+ value)) {
+ return false;
+ }
+ }
+
+ args.rval().setUndefined();
+ return true;
+}
+
+static bool intl_FormatDateTime(JSContext* cx,
+ const mozilla::intl::DateTimeFormat* df,
+ ClippedTime x, MutableHandleValue result) {
+ MOZ_ASSERT(x.isValid());
+
+ FormatBuffer<char16_t, INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ auto dfResult = df->TryFormat(x.toDouble(), buffer);
+ if (dfResult.isErr()) {
+ intl::ReportInternalError(cx, dfResult.unwrapErr());
+ return false;
+ }
+
+ JSString* str = buffer.toString(cx);
+ if (!str) {
+ return false;
+ }
+
+ result.setString(str);
+ return true;
+}
+
+using FieldType = js::ImmutableTenuredPtr<PropertyName*> JSAtomState::*;
+
+static FieldType GetFieldTypeForPartType(mozilla::intl::DateTimePartType type) {
+ switch (type) {
+ case mozilla::intl::DateTimePartType::Literal:
+ return &JSAtomState::literal;
+ case mozilla::intl::DateTimePartType::Era:
+ return &JSAtomState::era;
+ case mozilla::intl::DateTimePartType::Year:
+ return &JSAtomState::year;
+ case mozilla::intl::DateTimePartType::YearName:
+ return &JSAtomState::yearName;
+ case mozilla::intl::DateTimePartType::RelatedYear:
+ return &JSAtomState::relatedYear;
+ case mozilla::intl::DateTimePartType::Month:
+ return &JSAtomState::month;
+ case mozilla::intl::DateTimePartType::Day:
+ return &JSAtomState::day;
+ case mozilla::intl::DateTimePartType::Hour:
+ return &JSAtomState::hour;
+ case mozilla::intl::DateTimePartType::Minute:
+ return &JSAtomState::minute;
+ case mozilla::intl::DateTimePartType::Second:
+ return &JSAtomState::second;
+ case mozilla::intl::DateTimePartType::Weekday:
+ return &JSAtomState::weekday;
+ case mozilla::intl::DateTimePartType::DayPeriod:
+ return &JSAtomState::dayPeriod;
+ case mozilla::intl::DateTimePartType::TimeZoneName:
+ return &JSAtomState::timeZoneName;
+ case mozilla::intl::DateTimePartType::FractionalSecondDigits:
+ return &JSAtomState::fractionalSecond;
+ case mozilla::intl::DateTimePartType::Unknown:
+ return &JSAtomState::unknown;
+ }
+
+ MOZ_CRASH(
+ "unenumerated, undocumented format field returned "
+ "by iterator");
+}
+
+static FieldType GetFieldTypeForPartSource(
+ mozilla::intl::DateTimePartSource source) {
+ switch (source) {
+ case mozilla::intl::DateTimePartSource::Shared:
+ return &JSAtomState::shared;
+ case mozilla::intl::DateTimePartSource::StartRange:
+ return &JSAtomState::startRange;
+ case mozilla::intl::DateTimePartSource::EndRange:
+ return &JSAtomState::endRange;
+ }
+
+ MOZ_CRASH(
+ "unenumerated, undocumented format field returned "
+ "by iterator");
+}
+
+// A helper function to create an ArrayObject from DateTimePart objects.
+// When hasNoSource is true, we don't need to create the ||Source|| property for
+// the DateTimePart object.
+static bool CreateDateTimePartArray(
+ JSContext* cx, mozilla::Span<const char16_t> formattedSpan,
+ bool hasNoSource, const mozilla::intl::DateTimePartVector& parts,
+ MutableHandleValue result) {
+ RootedString overallResult(cx, NewStringCopy<CanGC>(cx, formattedSpan));
+ if (!overallResult) {
+ return false;
+ }
+
+ Rooted<ArrayObject*> partsArray(
+ cx, NewDenseFullyAllocatedArray(cx, parts.length()));
+ if (!partsArray) {
+ return false;
+ }
+ partsArray->ensureDenseInitializedLength(0, parts.length());
+
+ if (overallResult->length() == 0) {
+ // An empty string contains no parts, so avoid extra work below.
+ result.setObject(*partsArray);
+ return true;
+ }
+
+ RootedObject singlePart(cx);
+ RootedValue val(cx);
+
+ size_t index = 0;
+ size_t beginIndex = 0;
+ for (const mozilla::intl::DateTimePart& part : parts) {
+ singlePart = NewPlainObject(cx);
+ if (!singlePart) {
+ return false;
+ }
+
+ FieldType type = GetFieldTypeForPartType(part.mType);
+ val = StringValue(cx->names().*type);
+ if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) {
+ return false;
+ }
+
+ MOZ_ASSERT(part.mEndIndex > beginIndex);
+ JSLinearString* partStr = NewDependentString(cx, overallResult, beginIndex,
+ part.mEndIndex - beginIndex);
+ if (!partStr) {
+ return false;
+ }
+ val = StringValue(partStr);
+ if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) {
+ return false;
+ }
+
+ if (!hasNoSource) {
+ FieldType source = GetFieldTypeForPartSource(part.mSource);
+ val = StringValue(cx->names().*source);
+ if (!DefineDataProperty(cx, singlePart, cx->names().source, val)) {
+ return false;
+ }
+ }
+
+ beginIndex = part.mEndIndex;
+ partsArray->initDenseElement(index++, ObjectValue(*singlePart));
+ }
+
+ MOZ_ASSERT(index == parts.length());
+ MOZ_ASSERT(beginIndex == formattedSpan.size());
+ result.setObject(*partsArray);
+ return true;
+}
+
+static bool intl_FormatToPartsDateTime(JSContext* cx,
+ const mozilla::intl::DateTimeFormat* df,
+ ClippedTime x, bool hasNoSource,
+ MutableHandleValue result) {
+ MOZ_ASSERT(x.isValid());
+
+ FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ mozilla::intl::DateTimePartVector parts;
+ auto r = df->TryFormatToParts(x.toDouble(), buffer, parts);
+ if (r.isErr()) {
+ intl::ReportInternalError(cx, r.unwrapErr());
+ return false;
+ }
+
+ return CreateDateTimePartArray(cx, buffer, hasNoSource, parts, result);
+}
+
+bool js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+ MOZ_ASSERT(args[0].isObject());
+ MOZ_ASSERT(args[1].isNumber());
+ MOZ_ASSERT(args[2].isBoolean());
+
+ Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
+ dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
+
+ bool formatToParts = args[2].toBoolean();
+
+ ClippedTime x = TimeClip(args[1].toNumber());
+ if (!x.isValid()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
+ formatToParts ? "formatToParts" : "format");
+ return false;
+ }
+
+ mozilla::intl::DateTimeFormat* df =
+ GetOrCreateDateTimeFormat(cx, dateTimeFormat);
+ if (!df) {
+ return false;
+ }
+
+ // Use the DateTimeFormat to actually format the time stamp.
+ return formatToParts ? intl_FormatToPartsDateTime(
+ cx, df, x, /* hasNoSource */ true, args.rval())
+ : intl_FormatDateTime(cx, df, x, args.rval());
+}
+
+/**
+ * Returns a new DateIntervalFormat with the locale and date-time formatting
+ * options of the given DateTimeFormat.
+ */
+static mozilla::intl::DateIntervalFormat* NewDateIntervalFormat(
+ JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
+ mozilla::intl::DateTimeFormat& mozDtf) {
+ RootedValue value(cx);
+ RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat));
+ if (!internals) {
+ return nullptr;
+ }
+
+ FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> pattern(cx);
+ auto result = mozDtf.GetPattern(pattern);
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+
+ // Determine the hour cycle used in the resolved pattern.
+ mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hcPattern =
+ mozilla::intl::DateTimeFormat::HourCycleFromPattern(pattern);
+
+ UniqueChars locale = DateTimeFormatLocale(cx, internals, hcPattern);
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) {
+ return nullptr;
+ }
+
+ AutoStableStringChars timeZone(cx);
+ if (!timeZone.initTwoByte(cx, value.toString())) {
+ return nullptr;
+ }
+ mozilla::Span<const char16_t> timeZoneChars = timeZone.twoByteRange();
+
+ FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> skeleton(cx);
+ auto skelResult = mozDtf.GetOriginalSkeleton(skeleton);
+ if (skelResult.isErr()) {
+ intl::ReportInternalError(cx, skelResult.unwrapErr());
+ return nullptr;
+ }
+
+ auto dif = mozilla::intl::DateIntervalFormat::TryCreate(
+ mozilla::MakeStringSpan(locale.get()), skeleton, timeZoneChars);
+
+ if (dif.isErr()) {
+ js::intl::ReportInternalError(cx, dif.unwrapErr());
+ return nullptr;
+ }
+
+ return dif.unwrap().release();
+}
+
+static mozilla::intl::DateIntervalFormat* GetOrCreateDateIntervalFormat(
+ JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
+ mozilla::intl::DateTimeFormat& mozDtf) {
+ // Obtain a cached DateIntervalFormat object.
+ mozilla::intl::DateIntervalFormat* dif =
+ dateTimeFormat->getDateIntervalFormat();
+ if (dif) {
+ return dif;
+ }
+
+ dif = NewDateIntervalFormat(cx, dateTimeFormat, mozDtf);
+ if (!dif) {
+ return nullptr;
+ }
+ dateTimeFormat->setDateIntervalFormat(dif);
+
+ intl::AddICUCellMemory(
+ dateTimeFormat,
+ DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
+ return dif;
+}
+
+/**
+ * PartitionDateTimeRangePattern ( dateTimeFormat, x, y )
+ */
+static bool PartitionDateTimeRangePattern(
+ JSContext* cx, const mozilla::intl::DateTimeFormat* df,
+ const mozilla::intl::DateIntervalFormat* dif,
+ mozilla::intl::AutoFormattedDateInterval& formatted, ClippedTime x,
+ ClippedTime y, bool* equal) {
+ MOZ_ASSERT(x.isValid());
+ MOZ_ASSERT(y.isValid());
+
+ // We can't access the calendar used by UDateIntervalFormat to change it to a
+ // proleptic Gregorian calendar. Instead we need to call a different formatter
+ // function which accepts UCalendar instead of UDate.
+ // But creating new UCalendar objects for each call is slow, so when we can
+ // ensure that the input dates are later than the Gregorian change date,
+ // directly call the formatter functions taking UDate.
+
+ // The Gregorian change date "1582-10-15T00:00:00.000Z".
+ constexpr double GregorianChangeDate = -12219292800000.0;
+
+ // Add a full day to account for time zone offsets.
+ constexpr double GregorianChangeDatePlusOneDay =
+ GregorianChangeDate + msPerDay;
+
+ mozilla::intl::ICUResult result = Ok();
+ if (x.toDouble() < GregorianChangeDatePlusOneDay ||
+ y.toDouble() < GregorianChangeDatePlusOneDay) {
+ // Create calendar objects for the start and end date by cloning the date
+ // formatter calendar. The date formatter calendar already has the correct
+ // time zone set and was changed to use a proleptic Gregorian calendar.
+ auto startCal = df->CloneCalendar(x.toDouble());
+ if (startCal.isErr()) {
+ intl::ReportInternalError(cx, startCal.unwrapErr());
+ return false;
+ }
+
+ auto endCal = df->CloneCalendar(y.toDouble());
+ if (endCal.isErr()) {
+ intl::ReportInternalError(cx, endCal.unwrapErr());
+ return false;
+ }
+
+ result = dif->TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(),
+ formatted, equal);
+ } else {
+ // The common fast path which doesn't require creating calendar objects.
+ result =
+ dif->TryFormatDateTime(x.toDouble(), y.toDouble(), formatted, equal);
+ }
+
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * FormatDateTimeRange( dateTimeFormat, x, y )
+ */
+static bool FormatDateTimeRange(JSContext* cx,
+ const mozilla::intl::DateTimeFormat* df,
+ const mozilla::intl::DateIntervalFormat* dif,
+ ClippedTime x, ClippedTime y,
+ MutableHandleValue result) {
+ mozilla::intl::AutoFormattedDateInterval formatted;
+ if (!formatted.IsValid()) {
+ intl::ReportInternalError(cx, formatted.GetError());
+ return false;
+ }
+
+ bool equal;
+ if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) {
+ return false;
+ }
+
+ // PartitionDateTimeRangePattern, step 12.
+ if (equal) {
+ return intl_FormatDateTime(cx, df, x, result);
+ }
+
+ auto spanResult = formatted.ToSpan();
+ if (spanResult.isErr()) {
+ intl::ReportInternalError(cx, spanResult.unwrapErr());
+ return false;
+ }
+ JSString* resultStr = NewStringCopy<CanGC>(cx, spanResult.unwrap());
+ if (!resultStr) {
+ return false;
+ }
+
+ result.setString(resultStr);
+ return true;
+}
+
+/**
+ * FormatDateTimeRangeToParts ( dateTimeFormat, x, y )
+ */
+static bool FormatDateTimeRangeToParts(
+ JSContext* cx, const mozilla::intl::DateTimeFormat* df,
+ const mozilla::intl::DateIntervalFormat* dif, ClippedTime x, ClippedTime y,
+ MutableHandleValue result) {
+ mozilla::intl::AutoFormattedDateInterval formatted;
+ if (!formatted.IsValid()) {
+ intl::ReportInternalError(cx, formatted.GetError());
+ return false;
+ }
+
+ bool equal;
+ if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) {
+ return false;
+ }
+
+ // PartitionDateTimeRangePattern, step 12.
+ if (equal) {
+ return intl_FormatToPartsDateTime(cx, df, x, /* hasNoSource */ false,
+ result);
+ }
+
+ mozilla::intl::DateTimePartVector parts;
+ auto r = dif->TryFormattedToParts(formatted, parts);
+ if (r.isErr()) {
+ intl::ReportInternalError(cx, r.unwrapErr());
+ return false;
+ }
+
+ auto spanResult = formatted.ToSpan();
+ if (spanResult.isErr()) {
+ intl::ReportInternalError(cx, spanResult.unwrapErr());
+ return false;
+ }
+ return CreateDateTimePartArray(cx, spanResult.unwrap(),
+ /* hasNoSource */ false, parts, result);
+}
+
+bool js::intl_FormatDateTimeRange(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 4);
+ MOZ_ASSERT(args[0].isObject());
+ MOZ_ASSERT(args[1].isNumber());
+ MOZ_ASSERT(args[2].isNumber());
+ MOZ_ASSERT(args[3].isBoolean());
+
+ Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
+ dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
+
+ bool formatToParts = args[3].toBoolean();
+
+ // PartitionDateTimeRangePattern, steps 1-2.
+ ClippedTime x = TimeClip(args[1].toNumber());
+ if (!x.isValid()) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
+ formatToParts ? "formatRangeToParts" : "formatRange");
+ return false;
+ }
+
+ // PartitionDateTimeRangePattern, steps 3-4.
+ ClippedTime y = TimeClip(args[2].toNumber());
+ if (!y.isValid()) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
+ formatToParts ? "formatRangeToParts" : "formatRange");
+ return false;
+ }
+
+ mozilla::intl::DateTimeFormat* df =
+ GetOrCreateDateTimeFormat(cx, dateTimeFormat);
+ if (!df) {
+ return false;
+ }
+
+ mozilla::intl::DateIntervalFormat* dif =
+ GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df);
+ if (!dif) {
+ return false;
+ }
+
+ // Use the DateIntervalFormat to actually format the time range.
+ return formatToParts
+ ? FormatDateTimeRangeToParts(cx, df, dif, x, y, args.rval())
+ : FormatDateTimeRange(cx, df, dif, x, y, args.rval());
+}
diff --git a/js/src/builtin/intl/DateTimeFormat.h b/js/src/builtin/intl/DateTimeFormat.h
new file mode 100644
index 0000000000..cd93af56c6
--- /dev/null
+++ b/js/src/builtin/intl/DateTimeFormat.h
@@ -0,0 +1,188 @@
+/* -*- 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_DateTimeFormat_h
+#define builtin_intl_DateTimeFormat_h
+
+#include "builtin/SelfHostingDefines.h"
+#include "js/Class.h"
+#include "vm/NativeObject.h"
+
+namespace mozilla::intl {
+class DateTimeFormat;
+class DateIntervalFormat;
+} // namespace mozilla::intl
+
+namespace js {
+
+class DateTimeFormatObject : public NativeObject {
+ public:
+ static const JSClass class_;
+ static const JSClass& protoClass_;
+
+ static constexpr uint32_t INTERNALS_SLOT = 0;
+ static constexpr uint32_t DATE_FORMAT_SLOT = 1;
+ static constexpr uint32_t DATE_INTERVAL_FORMAT_SLOT = 2;
+ static constexpr uint32_t SLOT_COUNT = 3;
+
+ static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT,
+ "INTERNALS_SLOT must match self-hosting define for internals "
+ "object slot");
+
+ // Estimated memory use for UDateFormat (see IcuMemoryUsage).
+ static constexpr size_t UDateFormatEstimatedMemoryUse = 105402;
+
+ // Estimated memory use for UDateIntervalFormat (see IcuMemoryUsage).
+ static constexpr size_t UDateIntervalFormatEstimatedMemoryUse = 133064;
+
+ mozilla::intl::DateTimeFormat* getDateFormat() const {
+ const auto& slot = getFixedSlot(DATE_FORMAT_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::DateTimeFormat*>(slot.toPrivate());
+ }
+
+ void setDateFormat(mozilla::intl::DateTimeFormat* dateFormat) {
+ setFixedSlot(DATE_FORMAT_SLOT, PrivateValue(dateFormat));
+ }
+
+ mozilla::intl::DateIntervalFormat* getDateIntervalFormat() const {
+ const auto& slot = getFixedSlot(DATE_INTERVAL_FORMAT_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::DateIntervalFormat*>(slot.toPrivate());
+ }
+
+ void setDateIntervalFormat(
+ mozilla::intl::DateIntervalFormat* dateIntervalFormat) {
+ setFixedSlot(DATE_INTERVAL_FORMAT_SLOT, PrivateValue(dateIntervalFormat));
+ }
+
+ private:
+ static const JSClassOps classOps_;
+ static const ClassSpec classSpec_;
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+};
+
+/**
+ * Returns a new instance of the standard built-in DateTimeFormat constructor.
+ * Self-hosted code cannot cache this constructor (as it does for others in
+ * Utilities.js) because it is initialized after self-hosted code is compiled.
+ *
+ * Usage: dateTimeFormat = intl_DateTimeFormat(locales, options)
+ */
+[[nodiscard]] extern bool intl_DateTimeFormat(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns an array with the calendar type identifiers per Unicode
+ * Technical Standard 35, Unicode Locale Data Markup Language, for the
+ * supported calendars for the given locale. The default calendar is
+ * element 0.
+ *
+ * Usage: calendars = intl_availableCalendars(locale)
+ */
+[[nodiscard]] extern bool intl_availableCalendars(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns the calendar type identifier per Unicode Technical Standard 35,
+ * Unicode Locale Data Markup Language, for the default calendar for the given
+ * locale.
+ *
+ * Usage: calendar = intl_defaultCalendar(locale)
+ */
+[[nodiscard]] extern bool intl_defaultCalendar(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * 6.4.1 IsValidTimeZoneName ( timeZone )
+ *
+ * Verifies that the given string is a valid time zone name. If it is a valid
+ * time zone name, its IANA time zone name is returned. Otherwise returns null.
+ *
+ * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3
+ *
+ * Usage: ianaTimeZone = intl_IsValidTimeZoneName(timeZone)
+ */
+[[nodiscard]] extern bool intl_IsValidTimeZoneName(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Return the canonicalized time zone name. Canonicalization resolves link
+ * names to their target time zones.
+ *
+ * Usage: ianaTimeZone = intl_canonicalizeTimeZone(timeZone)
+ */
+[[nodiscard]] extern bool intl_canonicalizeTimeZone(JSContext* cx,
+ unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Return the default time zone name. The time zone name is not canonicalized.
+ *
+ * Usage: icuDefaultTimeZone = intl_defaultTimeZone()
+ */
+[[nodiscard]] extern bool intl_defaultTimeZone(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Return the raw offset from GMT in milliseconds for the default time zone.
+ *
+ * Usage: defaultTimeZoneOffset = intl_defaultTimeZoneOffset()
+ */
+[[nodiscard]] extern bool intl_defaultTimeZoneOffset(JSContext* cx,
+ unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Return true if the given string is the default time zone as returned by
+ * intl_defaultTimeZone(). Otherwise return false.
+ *
+ * Usage: isIcuDefaultTimeZone = intl_isDefaultTimeZone(icuDefaultTimeZone)
+ */
+[[nodiscard]] extern bool intl_isDefaultTimeZone(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns a String value representing x (which must be a Number value)
+ * according to the effective locale and the formatting options of the
+ * given DateTimeFormat.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 12.3.2.
+ *
+ * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x, formatToParts)
+ */
+[[nodiscard]] extern bool intl_FormatDateTime(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns a String value representing the range between x and y (which both
+ * must be Number values) according to the effective locale and the formatting
+ * options of the given DateTimeFormat.
+ *
+ * Spec: Intl.DateTimeFormat.prototype.formatRange proposal
+ *
+ * Usage: formatted = intl_FormatDateTimeRange(dateTimeFmt, x, y, formatToParts)
+ */
+[[nodiscard]] extern bool intl_FormatDateTimeRange(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Extracts the resolved components from a DateTimeFormat and applies them to
+ * the object for resolved components.
+ *
+ * Usage: intl_resolveDateTimeFormatComponents(dateTimeFormat, resolved)
+ */
+[[nodiscard]] extern bool intl_resolveDateTimeFormatComponents(JSContext* cx,
+ unsigned argc,
+ JS::Value* vp);
+} // namespace js
+
+#endif /* builtin_intl_DateTimeFormat_h */
diff --git a/js/src/builtin/intl/DateTimeFormat.js b/js/src/builtin/intl/DateTimeFormat.js
new file mode 100644
index 0000000000..91a22fe5e4
--- /dev/null
+++ b/js/src/builtin/intl/DateTimeFormat.js
@@ -0,0 +1,1008 @@
+/* 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/. */
+
+/* Portions Copyright Norbert Lindenberg 2011-2012. */
+
+/**
+ * Compute an internal properties object from |lazyDateTimeFormatData|.
+ */
+function resolveDateTimeFormatInternals(lazyDateTimeFormatData) {
+ assert(IsObject(lazyDateTimeFormatData), "lazy data not an object?");
+
+ // Lazy DateTimeFormat data has the following structure:
+ //
+ // {
+ // requestedLocales: List of locales,
+ //
+ // localeOpt: // *first* opt computed in InitializeDateTimeFormat
+ // {
+ // localeMatcher: "lookup" / "best fit",
+ //
+ // ca: string matching a Unicode extension type, // optional
+ //
+ // nu: string matching a Unicode extension type, // optional
+ //
+ // hc: "h11" / "h12" / "h23" / "h24", // optional
+ // }
+ //
+ // timeZone: IANA time zone name,
+ //
+ // formatOpt: // *second* opt computed in InitializeDateTimeFormat
+ // {
+ // // all the properties/values listed in Table 3
+ // // (weekday, era, year, month, day, &c.)
+ //
+ // hour12: true / false, // optional
+ // }
+ //
+ // formatMatcher: "basic" / "best fit",
+ //
+ // dateStyle: "full" / "long" / "medium" / "short" / undefined,
+ //
+ // timeStyle: "full" / "long" / "medium" / "short" / undefined,
+ //
+ // patternOption:
+ // String representing LDML Date Format pattern or undefined
+ // }
+ //
+ // Note that lazy data is only installed as a final step of initialization,
+ // so every DateTimeFormat lazy data object has *all* these properties,
+ // never a subset of them.
+
+ var internalProps = std_Object_create(null);
+
+ var DateTimeFormat = dateTimeFormatInternalProperties;
+
+ // Compute effective locale.
+
+ // Step 10.
+ var localeData = DateTimeFormat.localeData;
+
+ // Step 11.
+ var r = ResolveLocale(
+ "DateTimeFormat",
+ lazyDateTimeFormatData.requestedLocales,
+ lazyDateTimeFormatData.localeOpt,
+ DateTimeFormat.relevantExtensionKeys,
+ localeData
+ );
+
+ // Steps 12-13, 15.
+ internalProps.locale = r.locale;
+ internalProps.calendar = r.ca;
+ internalProps.numberingSystem = r.nu;
+
+ // Step 20.
+ internalProps.timeZone = lazyDateTimeFormatData.timeZone;
+
+ // Step 21.
+ var formatOpt = lazyDateTimeFormatData.formatOpt;
+
+ // Step 14.
+ // Copy the hourCycle setting, if present, to the format options. But
+ // only do this if no hour12 option is present, because the latter takes
+ // precedence over hourCycle.
+ if (r.hc !== null && formatOpt.hour12 === undefined) {
+ formatOpt.hourCycle = r.hc;
+ }
+
+ // Steps 26-31, more or less - see comment after this function.
+ if (lazyDateTimeFormatData.patternOption !== undefined) {
+ internalProps.pattern = lazyDateTimeFormatData.patternOption;
+ } else if (
+ lazyDateTimeFormatData.dateStyle !== undefined ||
+ lazyDateTimeFormatData.timeStyle !== undefined
+ ) {
+ internalProps.hourCycle = formatOpt.hourCycle;
+ internalProps.hour12 = formatOpt.hour12;
+ internalProps.dateStyle = lazyDateTimeFormatData.dateStyle;
+ internalProps.timeStyle = lazyDateTimeFormatData.timeStyle;
+ } else {
+ internalProps.hourCycle = formatOpt.hourCycle;
+ internalProps.hour12 = formatOpt.hour12;
+ internalProps.weekday = formatOpt.weekday;
+ internalProps.era = formatOpt.era;
+ internalProps.year = formatOpt.year;
+ internalProps.month = formatOpt.month;
+ internalProps.day = formatOpt.day;
+ internalProps.dayPeriod = formatOpt.dayPeriod;
+ internalProps.hour = formatOpt.hour;
+ internalProps.minute = formatOpt.minute;
+ internalProps.second = formatOpt.second;
+ internalProps.fractionalSecondDigits = formatOpt.fractionalSecondDigits;
+ internalProps.timeZoneName = formatOpt.timeZoneName;
+ }
+
+ // The caller is responsible for associating |internalProps| with the right
+ // object using |setInternalProperties|.
+ return internalProps;
+}
+
+/**
+ * Returns an object containing the DateTimeFormat internal properties of |obj|.
+ */
+function getDateTimeFormatInternals(obj) {
+ assert(IsObject(obj), "getDateTimeFormatInternals called with non-object");
+ assert(
+ intl_GuardToDateTimeFormat(obj) !== null,
+ "getDateTimeFormatInternals called with non-DateTimeFormat"
+ );
+
+ var internals = getIntlObjectInternals(obj);
+ assert(
+ internals.type === "DateTimeFormat",
+ "bad type escaped getIntlObjectInternals"
+ );
+
+ // If internal properties have already been computed, use them.
+ var internalProps = maybeInternalProperties(internals);
+ if (internalProps) {
+ return internalProps;
+ }
+
+ // Otherwise it's time to fully create them.
+ internalProps = resolveDateTimeFormatInternals(internals.lazyData);
+ setInternalProperties(internals, internalProps);
+ return internalProps;
+}
+
+/**
+ * 12.1.10 UnwrapDateTimeFormat( dtf )
+ */
+function UnwrapDateTimeFormat(dtf) {
+ // Steps 2 and 4 (error handling moved to caller).
+ if (
+ IsObject(dtf) &&
+ intl_GuardToDateTimeFormat(dtf) === null &&
+ !intl_IsWrappedDateTimeFormat(dtf) &&
+ callFunction(
+ std_Object_isPrototypeOf,
+ GetBuiltinPrototype("DateTimeFormat"),
+ dtf
+ )
+ ) {
+ dtf = dtf[intlFallbackSymbol()];
+ }
+ return dtf;
+}
+
+/**
+ * 6.4.2 CanonicalizeTimeZoneName ( timeZone )
+ *
+ * Canonicalizes the given IANA time zone name.
+ *
+ * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3
+ */
+function CanonicalizeTimeZoneName(timeZone) {
+ assert(typeof timeZone === "string", "CanonicalizeTimeZoneName");
+
+ // Step 1. (Not applicable, the input is already a valid IANA time zone.)
+ assert(timeZone !== "Etc/Unknown", "Invalid time zone");
+ assert(
+ timeZone === intl_IsValidTimeZoneName(timeZone),
+ "Time zone name not normalized"
+ );
+
+ // Step 2.
+ var ianaTimeZone = intl_canonicalizeTimeZone(timeZone);
+ assert(ianaTimeZone !== "Etc/Unknown", "Invalid canonical time zone");
+ assert(
+ ianaTimeZone === intl_IsValidTimeZoneName(ianaTimeZone),
+ "Unsupported canonical time zone"
+ );
+
+ // Step 3.
+ if (ianaTimeZone === "Etc/UTC" || ianaTimeZone === "Etc/GMT") {
+ ianaTimeZone = "UTC";
+ }
+
+ // Step 4.
+ return ianaTimeZone;
+}
+
+var timeZoneCache = {
+ icuDefaultTimeZone: undefined,
+ defaultTimeZone: undefined,
+};
+
+/**
+ * 6.4.3 DefaultTimeZone ()
+ *
+ * Returns the IANA time zone name for the host environment's current time zone.
+ *
+ * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3
+ */
+function DefaultTimeZone() {
+ if (intl_isDefaultTimeZone(timeZoneCache.icuDefaultTimeZone)) {
+ return timeZoneCache.defaultTimeZone;
+ }
+
+ // Verify that the current ICU time zone is a valid ECMA-402 time zone.
+ var icuDefaultTimeZone = intl_defaultTimeZone();
+ var timeZone = intl_IsValidTimeZoneName(icuDefaultTimeZone);
+ if (timeZone === null) {
+ // Before defaulting to "UTC", try to represent the default time zone
+ // using the Etc/GMT + offset format. This format only accepts full
+ // hour offsets.
+ const msPerHour = 60 * 60 * 1000;
+ var offset = intl_defaultTimeZoneOffset();
+ assert(
+ offset === (offset | 0),
+ "milliseconds offset shouldn't be able to exceed int32_t range"
+ );
+ var offsetHours = offset / msPerHour;
+ var offsetHoursFraction = offset % msPerHour;
+ if (offsetHoursFraction === 0) {
+ // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset
+ // means a location west of GMT.
+ timeZone =
+ "Etc/GMT" + (offsetHours < 0 ? "+" : "-") + std_Math_abs(offsetHours);
+
+ // Check if the fallback is valid.
+ timeZone = intl_IsValidTimeZoneName(timeZone);
+ }
+
+ // Fallback to "UTC" if everything else fails.
+ if (timeZone === null) {
+ timeZone = "UTC";
+ }
+ }
+
+ // Canonicalize the ICU time zone, e.g. change Etc/UTC to UTC.
+ var defaultTimeZone = CanonicalizeTimeZoneName(timeZone);
+
+ timeZoneCache.defaultTimeZone = defaultTimeZone;
+ timeZoneCache.icuDefaultTimeZone = icuDefaultTimeZone;
+
+ return defaultTimeZone;
+}
+
+/**
+ * Initializes an object as a DateTimeFormat.
+ *
+ * This method is complicated a moderate bit by its implementing initialization
+ * as a *lazy* concept. Everything that must happen now, does -- but we defer
+ * all the work we can until the object is actually used as a DateTimeFormat.
+ * This later work occurs in |resolveDateTimeFormatInternals|; steps not noted
+ * here occur there.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 12.1.1.
+ */
+function InitializeDateTimeFormat(
+ dateTimeFormat,
+ thisValue,
+ locales,
+ options,
+ mozExtensions
+) {
+ assert(
+ IsObject(dateTimeFormat),
+ "InitializeDateTimeFormat called with non-Object"
+ );
+ assert(
+ intl_GuardToDateTimeFormat(dateTimeFormat) !== null,
+ "InitializeDateTimeFormat called with non-DateTimeFormat"
+ );
+
+ // Lazy DateTimeFormat data has the following structure:
+ //
+ // {
+ // requestedLocales: List of locales,
+ //
+ // localeOpt: // *first* opt computed in InitializeDateTimeFormat
+ // {
+ // localeMatcher: "lookup" / "best fit",
+ //
+ // ca: string matching a Unicode extension type, // optional
+ //
+ // nu: string matching a Unicode extension type, // optional
+ //
+ // hc: "h11" / "h12" / "h23" / "h24", // optional
+ // }
+ //
+ // timeZone: IANA time zone name,
+ //
+ // formatOpt: // *second* opt computed in InitializeDateTimeFormat
+ // {
+ // // all the properties/values listed in Table 3
+ // // (weekday, era, year, month, day, &c.)
+ //
+ // hour12: true / false, // optional
+ // }
+ //
+ // formatMatcher: "basic" / "best fit",
+ // }
+ //
+ // Note that lazy data is only installed as a final step of initialization,
+ // so every DateTimeFormat lazy data object has *all* these properties,
+ // never a subset of them.
+ var lazyDateTimeFormatData = std_Object_create(null);
+
+ // Step 1.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+ lazyDateTimeFormatData.requestedLocales = requestedLocales;
+
+ // Step 2.
+ options = ToDateTimeOptions(options, "any", "date");
+
+ // Compute options that impact interpretation of locale.
+ // Step 3.
+ var localeOpt = new_Record();
+ lazyDateTimeFormatData.localeOpt = localeOpt;
+
+ // Steps 4-5.
+ var localeMatcher = GetOption(
+ options,
+ "localeMatcher",
+ "string",
+ ["lookup", "best fit"],
+ "best fit"
+ );
+ localeOpt.localeMatcher = localeMatcher;
+
+ var calendar = GetOption(options, "calendar", "string", undefined, undefined);
+
+ if (calendar !== undefined) {
+ calendar = intl_ValidateAndCanonicalizeUnicodeExtensionType(
+ calendar,
+ "calendar",
+ "ca"
+ );
+ }
+
+ localeOpt.ca = calendar;
+
+ var numberingSystem = GetOption(
+ options,
+ "numberingSystem",
+ "string",
+ undefined,
+ undefined
+ );
+
+ if (numberingSystem !== undefined) {
+ numberingSystem = intl_ValidateAndCanonicalizeUnicodeExtensionType(
+ numberingSystem,
+ "numberingSystem",
+ "nu"
+ );
+ }
+
+ localeOpt.nu = numberingSystem;
+
+ // Step 6.
+ var hr12 = GetOption(options, "hour12", "boolean", undefined, undefined);
+
+ // Step 7.
+ var hc = GetOption(
+ options,
+ "hourCycle",
+ "string",
+ ["h11", "h12", "h23", "h24"],
+ undefined
+ );
+
+ // Step 8.
+ if (hr12 !== undefined) {
+ // The "hourCycle" option is ignored if "hr12" is also present.
+ hc = null;
+ }
+
+ // Step 9.
+ localeOpt.hc = hc;
+
+ // Steps 10-16 (see resolveDateTimeFormatInternals).
+
+ // Steps 17-20.
+ var tz = options.timeZone;
+ if (tz !== undefined) {
+ // Step 18.a.
+ tz = ToString(tz);
+
+ // Step 18.b.
+ var timeZone = intl_IsValidTimeZoneName(tz);
+ if (timeZone === null) {
+ ThrowRangeError(JSMSG_INVALID_TIME_ZONE, tz);
+ }
+
+ // Step 18.c.
+ tz = CanonicalizeTimeZoneName(timeZone);
+ } else {
+ // Step 19.
+ tz = DefaultTimeZone();
+ }
+ lazyDateTimeFormatData.timeZone = tz;
+
+ // Step 21.
+ var formatOpt = new_Record();
+ lazyDateTimeFormatData.formatOpt = formatOpt;
+
+ if (mozExtensions) {
+ let pattern = GetOption(options, "pattern", "string", undefined, undefined);
+ lazyDateTimeFormatData.patternOption = pattern;
+ }
+
+ // Step 22.
+ // 12.1, Table 5: Components of date and time formats.
+ formatOpt.weekday = GetOption(
+ options,
+ "weekday",
+ "string",
+ ["narrow", "short", "long"],
+ undefined
+ );
+ formatOpt.era = GetOption(
+ options,
+ "era",
+ "string",
+ ["narrow", "short", "long"],
+ undefined
+ );
+ formatOpt.year = GetOption(
+ options,
+ "year",
+ "string",
+ ["2-digit", "numeric"],
+ undefined
+ );
+ formatOpt.month = GetOption(
+ options,
+ "month",
+ "string",
+ ["2-digit", "numeric", "narrow", "short", "long"],
+ undefined
+ );
+ formatOpt.day = GetOption(
+ options,
+ "day",
+ "string",
+ ["2-digit", "numeric"],
+ undefined
+ );
+ formatOpt.dayPeriod = GetOption(
+ options,
+ "dayPeriod",
+ "string",
+ ["narrow", "short", "long"],
+ undefined
+ );
+ formatOpt.hour = GetOption(
+ options,
+ "hour",
+ "string",
+ ["2-digit", "numeric"],
+ undefined
+ );
+ formatOpt.minute = GetOption(
+ options,
+ "minute",
+ "string",
+ ["2-digit", "numeric"],
+ undefined
+ );
+ formatOpt.second = GetOption(
+ options,
+ "second",
+ "string",
+ ["2-digit", "numeric"],
+ undefined
+ );
+ formatOpt.fractionalSecondDigits = GetNumberOption(
+ options,
+ "fractionalSecondDigits",
+ 1,
+ 3,
+ undefined
+ );
+ formatOpt.timeZoneName = GetOption(
+ options,
+ "timeZoneName",
+ "string",
+ [
+ "short",
+ "long",
+ "shortOffset",
+ "longOffset",
+ "shortGeneric",
+ "longGeneric",
+ ],
+ undefined
+ );
+
+ // Steps 23-24 provided by ICU - see comment after this function.
+
+ // Step 25.
+ //
+ // For some reason (ICU not exposing enough interface?) we drop the
+ // requested format matcher on the floor after this. In any case, even if
+ // doing so is justified, we have to do this work here in case it triggers
+ // getters or similar. (bug 852837)
+ var formatMatcher = GetOption(
+ options,
+ "formatMatcher",
+ "string",
+ ["basic", "best fit"],
+ "best fit"
+ );
+ void formatMatcher;
+
+ // "DateTimeFormat dateStyle & timeStyle" propsal
+ // https://github.com/tc39/proposal-intl-datetime-style
+ var dateStyle = GetOption(
+ options,
+ "dateStyle",
+ "string",
+ ["full", "long", "medium", "short"],
+ undefined
+ );
+ lazyDateTimeFormatData.dateStyle = dateStyle;
+
+ var timeStyle = GetOption(
+ options,
+ "timeStyle",
+ "string",
+ ["full", "long", "medium", "short"],
+ undefined
+ );
+ lazyDateTimeFormatData.timeStyle = timeStyle;
+
+ if (dateStyle !== undefined || timeStyle !== undefined) {
+ var optionsList = [
+ "weekday",
+ "era",
+ "year",
+ "month",
+ "day",
+ "dayPeriod",
+ "hour",
+ "minute",
+ "second",
+ "fractionalSecondDigits",
+ "timeZoneName",
+ ];
+
+ for (var i = 0; i < optionsList.length; i++) {
+ var option = optionsList[i];
+ if (formatOpt[option] !== undefined) {
+ ThrowTypeError(
+ JSMSG_INVALID_DATETIME_OPTION,
+ option,
+ dateStyle !== undefined ? "dateStyle" : "timeStyle"
+ );
+ }
+ }
+ }
+
+ // Steps 26-28 provided by ICU, more or less - see comment after this function.
+
+ // Steps 29-30.
+ // Pass hr12 on to ICU.
+ if (hr12 !== undefined) {
+ formatOpt.hour12 = hr12;
+ }
+
+ // Step 32.
+ //
+ // We've done everything that must be done now: mark the lazy data as fully
+ // computed and install it.
+ initializeIntlObject(
+ dateTimeFormat,
+ "DateTimeFormat",
+ lazyDateTimeFormatData
+ );
+
+ // 12.2.1, steps 4-5.
+ if (
+ dateTimeFormat !== thisValue &&
+ callFunction(
+ std_Object_isPrototypeOf,
+ GetBuiltinPrototype("DateTimeFormat"),
+ thisValue
+ )
+ ) {
+ DefineDataProperty(
+ thisValue,
+ intlFallbackSymbol(),
+ dateTimeFormat,
+ ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE
+ );
+
+ return thisValue;
+ }
+
+ // 12.2.1, step 6.
+ return dateTimeFormat;
+}
+
+/**
+ * Returns a new options object that includes the provided options (if any)
+ * and fills in default components if required components are not defined.
+ * Required can be "date", "time", or "any".
+ * Defaults can be "date", "time", or "all".
+ *
+ * Spec: ECMAScript Internationalization API Specification, 12.1.1.
+ */
+function ToDateTimeOptions(options, required, defaults) {
+ assert(typeof required === "string", "ToDateTimeOptions");
+ assert(typeof defaults === "string", "ToDateTimeOptions");
+
+ // Steps 1-2.
+ if (options === undefined) {
+ options = null;
+ } else {
+ options = ToObject(options);
+ }
+ options = std_Object_create(options);
+
+ // Step 3.
+ var needDefaults = true;
+
+ // Step 4.
+ if (required === "date" || required === "any") {
+ if (options.weekday !== undefined) {
+ needDefaults = false;
+ }
+ if (options.year !== undefined) {
+ needDefaults = false;
+ }
+ if (options.month !== undefined) {
+ needDefaults = false;
+ }
+ if (options.day !== undefined) {
+ needDefaults = false;
+ }
+ }
+
+ // Step 5.
+ if (required === "time" || required === "any") {
+ if (options.dayPeriod !== undefined) {
+ needDefaults = false;
+ }
+ if (options.hour !== undefined) {
+ needDefaults = false;
+ }
+ if (options.minute !== undefined) {
+ needDefaults = false;
+ }
+ if (options.second !== undefined) {
+ needDefaults = false;
+ }
+ if (options.fractionalSecondDigits !== undefined) {
+ needDefaults = false;
+ }
+ }
+
+ // "DateTimeFormat dateStyle & timeStyle" propsal
+ // https://github.com/tc39/proposal-intl-datetime-style
+ var dateStyle = options.dateStyle;
+ var timeStyle = options.timeStyle;
+
+ if (dateStyle !== undefined || timeStyle !== undefined) {
+ needDefaults = false;
+ }
+
+ if (required === "date" && timeStyle !== undefined) {
+ ThrowTypeError(
+ JSMSG_INVALID_DATETIME_STYLE,
+ "timeStyle",
+ "toLocaleDateString"
+ );
+ }
+
+ if (required === "time" && dateStyle !== undefined) {
+ ThrowTypeError(
+ JSMSG_INVALID_DATETIME_STYLE,
+ "dateStyle",
+ "toLocaleTimeString"
+ );
+ }
+
+ // Step 6.
+ if (needDefaults && (defaults === "date" || defaults === "all")) {
+ // The specification says to call [[DefineOwnProperty]] with false for
+ // the Throw parameter, while Object.defineProperty uses true. For the
+ // calls here, the difference doesn't matter because we're adding
+ // properties to a new object.
+ DefineDataProperty(options, "year", "numeric");
+ DefineDataProperty(options, "month", "numeric");
+ DefineDataProperty(options, "day", "numeric");
+ }
+
+ // Step 7.
+ if (needDefaults && (defaults === "time" || defaults === "all")) {
+ // See comment for step 7.
+ DefineDataProperty(options, "hour", "numeric");
+ DefineDataProperty(options, "minute", "numeric");
+ DefineDataProperty(options, "second", "numeric");
+ }
+
+ // Step 8.
+ return options;
+}
+
+/**
+ * Returns the subset of the given locale list for which this locale list has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 12.3.2.
+ */
+function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) {
+ var options = arguments.length > 1 ? arguments[1] : undefined;
+
+ // Step 1.
+ var availableLocales = "DateTimeFormat";
+
+ // Step 2.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+
+ // Step 3.
+ return SupportedLocales(availableLocales, requestedLocales, options);
+}
+
+/**
+ * DateTimeFormat internal properties.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.3.3.
+ */
+var dateTimeFormatInternalProperties = {
+ localeData: dateTimeFormatLocaleData,
+ relevantExtensionKeys: ["ca", "hc", "nu"],
+};
+
+function dateTimeFormatLocaleData() {
+ return {
+ ca: intl_availableCalendars,
+ nu: getNumberingSystems,
+ hc: () => {
+ return [null, "h11", "h12", "h23", "h24"];
+ },
+ default: {
+ ca: intl_defaultCalendar,
+ nu: intl_numberingSystem,
+ hc: () => {
+ return null;
+ },
+ },
+ };
+}
+
+/**
+ * Create function to be cached and returned by Intl.DateTimeFormat.prototype.format.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 12.1.5.
+ */
+function createDateTimeFormatFormat(dtf) {
+ // This function is not inlined in $Intl_DateTimeFormat_format_get to avoid
+ // creating a call-object on each call to $Intl_DateTimeFormat_format_get.
+ return function(date) {
+ // Step 1 (implicit).
+
+ // Step 2.
+ assert(IsObject(dtf), "dateTimeFormatFormatToBind called with non-Object");
+ assert(
+ intl_GuardToDateTimeFormat(dtf) !== null,
+ "dateTimeFormatFormatToBind called with non-DateTimeFormat"
+ );
+
+ // Steps 3-4.
+ var x = date === undefined ? std_Date_now() : ToNumber(date);
+
+ // Step 5.
+ return intl_FormatDateTime(dtf, x, /* formatToParts = */ false);
+ };
+}
+
+/**
+ * Returns a function bound to this DateTimeFormat that returns a String value
+ * representing the result of calling ToNumber(date) according to the
+ * effective locale and the formatting options of this DateTimeFormat.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 12.4.3.
+ */
+// Uncloned functions with `$` prefix are allocated as extended function
+// to store the original name in `SetCanonicalName`.
+function $Intl_DateTimeFormat_format_get() {
+ // Steps 1-3.
+ var thisArg = UnwrapDateTimeFormat(this);
+ var dtf = thisArg;
+ if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
+ return callFunction(
+ intl_CallDateTimeFormatMethodIfWrapped,
+ thisArg,
+ "$Intl_DateTimeFormat_format_get"
+ );
+ }
+
+ var internals = getDateTimeFormatInternals(dtf);
+
+ // Step 4.
+ if (internals.boundFormat === undefined) {
+ // Steps 4.a-c.
+ internals.boundFormat = createDateTimeFormatFormat(dtf);
+ }
+
+ // Step 5.
+ return internals.boundFormat;
+}
+SetCanonicalName($Intl_DateTimeFormat_format_get, "get format");
+
+/**
+ * Intl.DateTimeFormat.prototype.formatToParts ( date )
+ *
+ * Spec: ECMAScript Internationalization API Specification, 12.4.4.
+ */
+function Intl_DateTimeFormat_formatToParts(date) {
+ // Step 1.
+ var dtf = this;
+
+ // Steps 2-3.
+ if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
+ return callFunction(
+ intl_CallDateTimeFormatMethodIfWrapped,
+ this,
+ date,
+ "Intl_DateTimeFormat_formatToParts"
+ );
+ }
+
+ // Steps 4-5.
+ var x = date === undefined ? std_Date_now() : ToNumber(date);
+
+ // Ensure the DateTimeFormat internals are resolved.
+ getDateTimeFormatInternals(dtf);
+
+ // Step 6.
+ return intl_FormatDateTime(dtf, x, /* formatToParts = */ true);
+}
+
+/**
+ * Intl.DateTimeFormat.prototype.formatRange ( startDate , endDate )
+ *
+ * Spec: Intl.DateTimeFormat.prototype.formatRange proposal
+ */
+function Intl_DateTimeFormat_formatRange(startDate, endDate) {
+ // Step 1.
+ var dtf = this;
+
+ // Step 2.
+ if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
+ return callFunction(
+ intl_CallDateTimeFormatMethodIfWrapped,
+ this,
+ startDate,
+ endDate,
+ "Intl_DateTimeFormat_formatRange"
+ );
+ }
+
+ // Step 3.
+ if (startDate === undefined || endDate === undefined) {
+ ThrowTypeError(
+ JSMSG_UNDEFINED_DATE,
+ startDate === undefined ? "start" : "end",
+ "formatRange"
+ );
+ }
+
+ // Step 4.
+ var x = ToNumber(startDate);
+
+ // Step 5.
+ var y = ToNumber(endDate);
+
+ // Ensure the DateTimeFormat internals are resolved.
+ getDateTimeFormatInternals(dtf);
+
+ // Step 6.
+ return intl_FormatDateTimeRange(dtf, x, y, /* formatToParts = */ false);
+}
+
+/**
+ * Intl.DateTimeFormat.prototype.formatRangeToParts ( startDate , endDate )
+ *
+ * Spec: Intl.DateTimeFormat.prototype.formatRange proposal
+ */
+function Intl_DateTimeFormat_formatRangeToParts(startDate, endDate) {
+ // Step 1.
+ var dtf = this;
+
+ // Step 2.
+ if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
+ return callFunction(
+ intl_CallDateTimeFormatMethodIfWrapped,
+ this,
+ startDate,
+ endDate,
+ "Intl_DateTimeFormat_formatRangeToParts"
+ );
+ }
+
+ // Step 3.
+ if (startDate === undefined || endDate === undefined) {
+ ThrowTypeError(
+ JSMSG_UNDEFINED_DATE,
+ startDate === undefined ? "start" : "end",
+ "formatRangeToParts"
+ );
+ }
+
+ // Step 4.
+ var x = ToNumber(startDate);
+
+ // Step 5.
+ var y = ToNumber(endDate);
+
+ // Ensure the DateTimeFormat internals are resolved.
+ getDateTimeFormatInternals(dtf);
+
+ // Step 6.
+ return intl_FormatDateTimeRange(dtf, x, y, /* formatToParts = */ true);
+}
+
+/**
+ * Returns the resolved options for a DateTimeFormat object.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 12.4.5.
+ */
+function Intl_DateTimeFormat_resolvedOptions() {
+ // Steps 1-3.
+ var thisArg = UnwrapDateTimeFormat(this);
+ var dtf = thisArg;
+ if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
+ return callFunction(
+ intl_CallDateTimeFormatMethodIfWrapped,
+ thisArg,
+ "Intl_DateTimeFormat_resolvedOptions"
+ );
+ }
+
+ // Ensure the internals are resolved.
+ var internals = getDateTimeFormatInternals(dtf);
+
+ // Steps 4-5.
+ var result = {
+ locale: internals.locale,
+ calendar: internals.calendar,
+ numberingSystem: internals.numberingSystem,
+ timeZone: internals.timeZone,
+ };
+
+ if (internals.pattern !== undefined) {
+ // The raw pattern option is only internal to Mozilla, and not part of the
+ // ECMA-402 API.
+ DefineDataProperty(result, "pattern", internals.pattern);
+ }
+
+ var hasDateStyle = internals.dateStyle !== undefined;
+ var hasTimeStyle = internals.timeStyle !== undefined;
+
+ if (hasDateStyle || hasTimeStyle) {
+ if (hasTimeStyle) {
+ // timeStyle (unlike dateStyle) requires resolving the pattern to
+ // ensure "hourCycle" and "hour12" properties are added to |result|.
+ intl_resolveDateTimeFormatComponents(
+ dtf,
+ result,
+ /* includeDateTimeFields = */ false
+ );
+ }
+ if (hasDateStyle) {
+ DefineDataProperty(result, "dateStyle", internals.dateStyle);
+ }
+ if (hasTimeStyle) {
+ DefineDataProperty(result, "timeStyle", internals.timeStyle);
+ }
+ } else {
+ // Components bag or a (Mozilla-only) raw pattern.
+ intl_resolveDateTimeFormatComponents(
+ dtf,
+ result,
+ /* includeDateTimeFields = */ true
+ );
+ }
+
+ // Step 6.
+ return result;
+}
diff --git a/js/src/builtin/intl/DecimalNumber.cpp b/js/src/builtin/intl/DecimalNumber.cpp
new file mode 100644
index 0000000000..ef83edbb87
--- /dev/null
+++ b/js/src/builtin/intl/DecimalNumber.cpp
@@ -0,0 +1,263 @@
+/* -*- 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/. */
+
+#include "builtin/intl/DecimalNumber.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/TextUtils.h"
+
+#include "js/GCAPI.h"
+#include "util/Text.h"
+#include "util/Unicode.h"
+#include "vm/StringType.h"
+
+int32_t js::intl::DecimalNumber::compareTo(const DecimalNumber& other) const {
+ // Can't compare if the exponent is too large.
+ MOZ_ASSERT(!exponentTooLarge());
+ MOZ_ASSERT(!other.exponentTooLarge());
+
+ // If the signs don't match, the negative number is smaller.
+ if (isNegative() != other.isNegative()) {
+ return isNegative() ? -1 : 1;
+ }
+
+ // Next handle the case when one of the numbers is zero.
+ if (isZero()) {
+ return other.isZero() ? 0 : other.isNegative() ? 1 : -1;
+ }
+ if (other.isZero()) {
+ return isNegative() ? -1 : 1;
+ }
+
+ // If the exponent is different, the number with the smaller exponent is
+ // smaller in total, unless the numbers are negative.
+ if (exponent() != other.exponent()) {
+ return (exponent() < other.exponent() ? -1 : 1) * (isNegative() ? -1 : 1);
+ }
+
+ class Significand {
+ const DecimalNumber& decimal_;
+ size_t index_;
+
+ public:
+ explicit Significand(const DecimalNumber& decimal) : decimal_(decimal) {
+ index_ = decimal.significandStart_;
+ }
+
+ int32_t next() {
+ // Any remaining digits in the significand are implicit zeros.
+ if (index_ >= decimal_.significandEnd_) {
+ return 0;
+ }
+
+ char ch = decimal_.charAt(index_++);
+
+ // Skip over the decimal point.
+ if (ch == '.') {
+ if (index_ >= decimal_.significandEnd_) {
+ return 0;
+ }
+ ch = decimal_.charAt(index_++);
+ }
+
+ MOZ_ASSERT(mozilla::IsAsciiDigit(ch));
+ return AsciiDigitToNumber(ch);
+ }
+ };
+
+ // Both numbers have the same sign, neither of them is zero, and they have the
+ // same exponent. Next compare the significand digit by digit until we find
+ // the first difference.
+
+ Significand s1(*this);
+ Significand s2(other);
+ for (int32_t e = std::abs(exponent()); e >= 0; e--) {
+ int32_t x = s1.next();
+ int32_t y = s2.next();
+ if (int32_t r = x - y) {
+ return r * (isNegative() ? -1 : 1);
+ }
+ }
+
+ // No different significand digit was found, so the numbers are equal.
+ return 0;
+}
+
+mozilla::Maybe<js::intl::DecimalNumber> js::intl::DecimalNumber::from(
+ JSLinearString* str, JS::AutoCheckCannotGC& nogc) {
+ return str->hasLatin1Chars() ? from<Latin1Char>(str->latin1Range(nogc))
+ : from<char16_t>(str->twoByteRange(nogc));
+}
+
+template <typename CharT>
+mozilla::Maybe<js::intl::DecimalNumber> js::intl::DecimalNumber::from(
+ mozilla::Span<const CharT> chars) {
+ // This algorithm matches a subset of the `StringNumericLiteral` grammar
+ // production of ECMAScript. In particular, we do *not* allow:
+ // - NonDecimalIntegerLiteral (eg. "0x10")
+ // - NumericLiteralSeparator (eg. "123_456")
+ // - Infinity (eg. "-Infinity")
+
+ DecimalNumber number(chars);
+
+ // Skip over leading whitespace.
+ size_t i = 0;
+ while (i < chars.size() && unicode::IsSpace(chars[i])) {
+ i++;
+ }
+
+ // The number is only whitespace, treat as zero.
+ if (i == chars.size()) {
+ number.zero_ = true;
+ return mozilla::Some(number);
+ }
+
+ // Read the optional sign.
+ if (auto ch = chars[i]; ch == '-' || ch == '+') {
+ i++;
+ number.negative_ = ch == '-';
+
+ if (i == chars.size()) {
+ return mozilla::Nothing();
+ }
+ }
+
+ // Must start with either a digit or the decimal point.
+ size_t startInteger = i;
+ size_t endInteger = i;
+ if (auto ch = chars[i]; mozilla::IsAsciiDigit(ch)) {
+ // Skip over leading zeros.
+ while (i < chars.size() && chars[i] == '0') {
+ i++;
+ }
+
+ // Read the integer part.
+ startInteger = i;
+ while (i < chars.size() && mozilla::IsAsciiDigit(chars[i])) {
+ i++;
+ }
+ endInteger = i;
+ } else if (ch == '.') {
+ // There must be a digit when the number starts with the decimal point.
+ if (i + 1 == chars.size() || !mozilla::IsAsciiDigit(chars[i + 1])) {
+ return mozilla::Nothing();
+ }
+ } else {
+ return mozilla::Nothing();
+ }
+
+ // Read the fractional part.
+ size_t startFraction = i;
+ size_t endFraction = i;
+ if (i < chars.size() && chars[i] == '.') {
+ i++;
+
+ startFraction = i;
+ while (i < chars.size() && mozilla::IsAsciiDigit(chars[i])) {
+ i++;
+ }
+ endFraction = i;
+
+ // Ignore trailing zeros in the fractional part.
+ while (startFraction <= endFraction && chars[endFraction - 1] == '0') {
+ endFraction--;
+ }
+ }
+
+ // Read the exponent.
+ if (i < chars.size() && (chars[i] == 'e' || chars[i] == 'E')) {
+ i++;
+
+ if (i == chars.size()) {
+ return mozilla::Nothing();
+ }
+
+ int32_t exponentSign = 1;
+ if (auto ch = chars[i]; ch == '-' || ch == '+') {
+ i++;
+ exponentSign = ch == '-' ? -1 : +1;
+
+ if (i == chars.size()) {
+ return mozilla::Nothing();
+ }
+ }
+
+ if (!mozilla::IsAsciiDigit(chars[i])) {
+ return mozilla::Nothing();
+ }
+
+ mozilla::CheckedInt32 exp = 0;
+ while (i < chars.size() && mozilla::IsAsciiDigit(chars[i])) {
+ exp *= 10;
+ exp += AsciiDigitToNumber(chars[i]);
+
+ i++;
+ }
+
+ // Check for exponent overflow.
+ if (exp.isValid()) {
+ number.exponent_ = exp.value() * exponentSign;
+ } else {
+ number.exponentTooLarge_ = true;
+ }
+ }
+
+ // Skip over trailing whitespace.
+ while (i < chars.size() && unicode::IsSpace(chars[i])) {
+ i++;
+ }
+
+ // The complete string must have been parsed.
+ if (i != chars.size()) {
+ return mozilla::Nothing();
+ }
+
+ if (startInteger < endInteger) {
+ // We have a non-zero integer part.
+
+ mozilla::CheckedInt32 integerExponent = number.exponent_;
+ integerExponent += size_t(endInteger - startInteger);
+
+ if (integerExponent.isValid()) {
+ number.exponent_ = integerExponent.value();
+ } else {
+ number.exponent_ = 0;
+ number.exponentTooLarge_ = true;
+ }
+
+ number.significandStart_ = startInteger;
+ number.significandEnd_ = endFraction;
+ } else if (startFraction < endFraction) {
+ // We have a non-zero fractional part.
+
+ // Skip over leading zeros
+ size_t i = startFraction;
+ while (i < endFraction && chars[i] == '0') {
+ i++;
+ }
+
+ mozilla::CheckedInt32 fractionExponent = number.exponent_;
+ fractionExponent -= size_t(i - startFraction);
+
+ if (fractionExponent.isValid() && fractionExponent.value() != INT32_MIN) {
+ number.exponent_ = fractionExponent.value();
+ } else {
+ number.exponent_ = 0;
+ number.exponentTooLarge_ = true;
+ }
+
+ number.significandStart_ = i;
+ number.significandEnd_ = endFraction;
+ } else {
+ // The number is zero, clear the error flag if it was set.
+ number.zero_ = true;
+ number.exponent_ = 0;
+ number.exponentTooLarge_ = false;
+ }
+
+ return mozilla::Some(number);
+}
diff --git a/js/src/builtin/intl/DecimalNumber.h b/js/src/builtin/intl/DecimalNumber.h
new file mode 100644
index 0000000000..2373ae0f7f
--- /dev/null
+++ b/js/src/builtin/intl/DecimalNumber.h
@@ -0,0 +1,117 @@
+/* -*- 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_DecimalNumber_h
+#define builtin_intl_DecimalNumber_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Span.h"
+#include "mozilla/Variant.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "jstypes.h"
+
+#include "js/TypeDecls.h"
+
+class JSLinearString;
+
+namespace JS {
+class JS_PUBLIC_API AutoCheckCannotGC;
+}
+
+namespace js::intl {
+
+/**
+ * Representation of a decimal number in normalized form.
+ *
+ * Examples of normalized forms:
+ * - "123" is normalized to "0.123e3".
+ * - "0.01e-4" is normalized to "0.1e-5".
+ * - "12.3" is normalized to "0.123e2".
+ *
+ * Note: Internally we leave the decimal point where it lies to avoid copying
+ * the string, but otherwise ignore it once we calculate the normalized
+ * exponent.
+ *
+ * TODO: Remove unused capabilities once there's a concrete PR for
+ * <https://github.com/tc39/proposal-intl-numberformat-v3/issues/98>.
+ */
+class MOZ_STACK_CLASS DecimalNumber final {
+ using Latin1String = mozilla::Span<const JS::Latin1Char>;
+ using TwoByteString = mozilla::Span<const char16_t>;
+
+ mozilla::Variant<Latin1String, TwoByteString> string_;
+
+ char charAt(size_t i) const {
+ if (string_.is<Latin1String>()) {
+ return static_cast<char>(string_.as<Latin1String>()[i]);
+ }
+ return static_cast<char>(string_.as<TwoByteString>()[i]);
+ }
+
+ // Decimal exponent. Valid range is (INT32_MIN, INT_MAX32].
+ int32_t exponent_ = 0;
+
+ // Start and end position of the significand.
+ size_t significandStart_ = 0;
+ size_t significandEnd_ = 0;
+
+ // Flag if the number is zero.
+ bool zero_ = false;
+
+ // Flag for negative numbers.
+ bool negative_ = false;
+
+ // Error flag when the exponent is too large.
+ bool exponentTooLarge_ = false;
+
+ template <typename CharT>
+ explicit DecimalNumber(mozilla::Span<const CharT> string) : string_(string) {}
+
+ public:
+ /** Return true if this decimal is zero. */
+ bool isZero() const { return zero_; }
+
+ /** Return true if this decimal is negative. */
+ bool isNegative() const { return negative_; }
+
+ /** Return true if the exponent is too large. */
+ bool exponentTooLarge() const { return exponentTooLarge_; }
+
+ /** Return the exponent of this decimal. */
+ int32_t exponent() const { return exponent_; }
+
+ // Exposed for testing.
+ size_t significandStart() const { return significandStart_; }
+ size_t significandEnd() const { return significandEnd_; }
+
+ /**
+ * Compare this decimal to another decimal. Returns a negative value if this
+ * decimal is smaller; zero if this decimal is equal; or a positive value if
+ * this decimal is larger than the input.
+ */
+ int32_t compareTo(const DecimalNumber& other) const;
+
+ /**
+ * Create a decimal number from the input. Returns |mozilla::Nothing| if the
+ * input can't be parsed.
+ */
+ template <typename CharT>
+ static mozilla::Maybe<DecimalNumber> from(mozilla::Span<const CharT> chars);
+
+ /**
+ * Create a decimal number from the input. Returns |mozilla::Nothing| if the
+ * input can't be parsed.
+ */
+ static mozilla::Maybe<DecimalNumber> from(JSLinearString* str,
+ JS::AutoCheckCannotGC& nogc);
+};
+} // namespace js::intl
+
+#endif /* builtin_intl_DecimalNumber_h */
diff --git a/js/src/builtin/intl/DisplayNames.cpp b/js/src/builtin/intl/DisplayNames.cpp
new file mode 100644
index 0000000000..2ddffe0365
--- /dev/null
+++ b/js/src/builtin/intl/DisplayNames.cpp
@@ -0,0 +1,551 @@
+/* -*- 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.DisplayNames implementation. */
+
+#include "builtin/intl/DisplayNames.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/DisplayNames.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/Span.h"
+
+#include <algorithm>
+
+#include "jsnum.h"
+#include "jspubtd.h"
+
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/FormatBuffer.h"
+#include "gc/AllocKind.h"
+#include "gc/GCContext.h"
+#include "js/CallArgs.h"
+#include "js/Class.h"
+#include "js/experimental/Intl.h" // JS::AddMozDisplayNamesConstructor
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties
+#include "js/PropertyDescriptor.h"
+#include "js/PropertySpec.h"
+#include "js/RootingAPI.h"
+#include "js/TypeDecls.h"
+#include "js/Utility.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/JSObject.h"
+#include "vm/Printer.h"
+#include "vm/Runtime.h"
+#include "vm/SelfHosting.h"
+#include "vm/Stack.h"
+#include "vm/StringType.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+const JSClassOps DisplayNamesObject::classOps_ = {nullptr, /* addProperty */
+ nullptr, /* delProperty */
+ nullptr, /* enumerate */
+ nullptr, /* newEnumerate */
+ nullptr, /* resolve */
+ nullptr, /* mayResolve */
+ DisplayNamesObject::finalize};
+
+const JSClass DisplayNamesObject::class_ = {
+ "Intl.DisplayNames",
+ JSCLASS_HAS_RESERVED_SLOTS(DisplayNamesObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_DisplayNames) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &DisplayNamesObject::classOps_, &DisplayNamesObject::classSpec_};
+
+const JSClass& DisplayNamesObject::protoClass_ = PlainObject::class_;
+
+static bool displayNames_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().DisplayNames);
+ return true;
+}
+
+static const JSFunctionSpec displayNames_static_methods[] = {
+ JS_SELF_HOSTED_FN("supportedLocalesOf",
+ "Intl_DisplayNames_supportedLocalesOf", 1, 0),
+ JS_FS_END};
+
+static const JSFunctionSpec displayNames_methods[] = {
+ JS_SELF_HOSTED_FN("of", "Intl_DisplayNames_of", 1, 0),
+ JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DisplayNames_resolvedOptions", 0,
+ 0),
+ JS_FN(js_toSource_str, displayNames_toSource, 0, 0), JS_FS_END};
+
+static const JSPropertySpec displayNames_properties[] = {
+ JS_STRING_SYM_PS(toStringTag, "Intl.DisplayNames", JSPROP_READONLY),
+ JS_PS_END};
+
+static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp);
+
+const ClassSpec DisplayNamesObject::classSpec_ = {
+ GenericCreateConstructor<DisplayNames, 2, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<DisplayNamesObject>,
+ displayNames_static_methods,
+ nullptr,
+ displayNames_methods,
+ displayNames_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+enum class DisplayNamesOptions {
+ Standard,
+
+ // Calendar display names are no longer available with the current spec
+ // proposal text, but may be re-enabled in the future. For our internal use
+ // we still need to have them present, so use a feature guard for now.
+ EnableMozExtensions,
+};
+
+/**
+ * Initialize a new Intl.DisplayNames object using the named self-hosted
+ * function.
+ */
+static bool InitializeDisplayNamesObject(JSContext* cx, HandleObject obj,
+ Handle<PropertyName*> initializer,
+ HandleValue locales,
+ HandleValue options,
+ DisplayNamesOptions dnoptions) {
+ FixedInvokeArgs<4> args(cx);
+
+ args[0].setObject(*obj);
+ args[1].set(locales);
+ args[2].set(options);
+ args[3].setBoolean(dnoptions == DisplayNamesOptions::EnableMozExtensions);
+
+ RootedValue ignored(cx);
+ if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args,
+ &ignored)) {
+ return false;
+ }
+
+ MOZ_ASSERT(ignored.isUndefined(),
+ "Unexpected return value from non-legacy Intl object initializer");
+ return true;
+}
+
+/**
+ * Intl.DisplayNames ([ locales [ , options ]])
+ */
+static bool DisplayNames(JSContext* cx, const CallArgs& args,
+ DisplayNamesOptions dnoptions) {
+ // Step 1.
+ if (!ThrowIfNotConstructing(cx, args, "Intl.DisplayNames")) {
+ return false;
+ }
+
+ // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+ RootedObject proto(cx);
+ if (dnoptions == DisplayNamesOptions::Standard) {
+ if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DisplayNames,
+ &proto)) {
+ return false;
+ }
+ } else {
+ RootedObject newTarget(cx, &args.newTarget().toObject());
+ if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Null, &proto)) {
+ return false;
+ }
+ }
+
+ Rooted<DisplayNamesObject*> displayNames(cx);
+ displayNames = NewObjectWithClassProto<DisplayNamesObject>(cx, proto);
+ if (!displayNames) {
+ return false;
+ }
+
+ HandleValue locales = args.get(0);
+ HandleValue options = args.get(1);
+
+ // Steps 3-26.
+ if (!InitializeDisplayNamesObject(cx, displayNames,
+ cx->names().InitializeDisplayNames, locales,
+ options, dnoptions)) {
+ return false;
+ }
+
+ // Step 27.
+ args.rval().setObject(*displayNames);
+ return true;
+}
+
+static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ return DisplayNames(cx, args, DisplayNamesOptions::Standard);
+}
+
+static bool MozDisplayNames(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ return DisplayNames(cx, args, DisplayNamesOptions::EnableMozExtensions);
+}
+
+void js::DisplayNamesObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ MOZ_ASSERT(gcx->onMainThread());
+
+ if (mozilla::intl::DisplayNames* displayNames =
+ obj->as<DisplayNamesObject>().getDisplayNames()) {
+ intl::RemoveICUCellMemory(gcx, obj, DisplayNamesObject::EstimatedMemoryUse);
+ delete displayNames;
+ }
+}
+
+bool JS::AddMozDisplayNamesConstructor(JSContext* cx, HandleObject intl) {
+ RootedObject ctor(cx, GlobalObject::createConstructor(
+ cx, MozDisplayNames, cx->names().DisplayNames, 2));
+ if (!ctor) {
+ return false;
+ }
+
+ RootedObject proto(
+ cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global()));
+ if (!proto) {
+ return false;
+ }
+
+ if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
+ return false;
+ }
+
+ if (!JS_DefineFunctions(cx, ctor, displayNames_static_methods)) {
+ return false;
+ }
+
+ if (!JS_DefineFunctions(cx, proto, displayNames_methods)) {
+ return false;
+ }
+
+ if (!JS_DefineProperties(cx, proto, displayNames_properties)) {
+ return false;
+ }
+
+ RootedValue ctorValue(cx, ObjectValue(*ctor));
+ return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0);
+}
+
+static mozilla::intl::DisplayNames* NewDisplayNames(
+ JSContext* cx, const char* locale,
+ mozilla::intl::DisplayNames::Options& options) {
+ auto result = mozilla::intl::DisplayNames::TryCreate(locale, options);
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+ return result.unwrap().release();
+}
+
+static mozilla::intl::DisplayNames* GetOrCreateDisplayNames(
+ JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale,
+ mozilla::intl::DisplayNames::Options& options) {
+ // Obtain a cached mozilla::intl::DisplayNames object.
+ mozilla::intl::DisplayNames* dn = displayNames->getDisplayNames();
+ if (!dn) {
+ dn = NewDisplayNames(cx, locale, options);
+ if (!dn) {
+ return nullptr;
+ }
+ displayNames->setDisplayNames(dn);
+
+ intl::AddICUCellMemory(displayNames,
+ DisplayNamesObject::EstimatedMemoryUse);
+ }
+ return dn;
+}
+
+static void ReportInvalidOptionError(JSContext* cx, HandleString type,
+ HandleString option) {
+ if (UniqueChars optionStr = QuoteString(cx, option, '"')) {
+ if (UniqueChars typeStr = QuoteString(cx, type)) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE, typeStr.get(),
+ optionStr.get());
+ }
+ }
+}
+
+static void ReportInvalidOptionError(JSContext* cx, const char* type,
+ HandleString option) {
+ if (UniqueChars str = QuoteString(cx, option, '"')) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE, type, str.get());
+ }
+}
+
+static void ReportInvalidOptionError(JSContext* cx, const char* type,
+ double option) {
+ ToCStringBuf cbuf;
+ const char* str = NumberToCString(&cbuf, option);
+ MOZ_ASSERT(str);
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INVALID_DIGITS_VALUE, str);
+}
+
+/**
+ * intl_ComputeDisplayName(displayNames, locale, calendar, style,
+ * languageDisplay, fallback, type, code)
+ */
+bool js::intl_ComputeDisplayName(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 8);
+
+ Rooted<DisplayNamesObject*> displayNames(
+ cx, &args[0].toObject().as<DisplayNamesObject>());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[1].toString());
+ if (!locale) {
+ return false;
+ }
+
+ Rooted<JSLinearString*> calendar(cx, args[2].toString()->ensureLinear(cx));
+ if (!calendar) {
+ return false;
+ }
+
+ Rooted<JSLinearString*> code(cx, args[7].toString()->ensureLinear(cx));
+ if (!code) {
+ return false;
+ }
+
+ mozilla::intl::DisplayNames::Style style;
+ {
+ JSLinearString* styleStr = args[3].toString()->ensureLinear(cx);
+ if (!styleStr) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(styleStr, "long")) {
+ style = mozilla::intl::DisplayNames::Style::Long;
+ } else if (StringEqualsLiteral(styleStr, "short")) {
+ style = mozilla::intl::DisplayNames::Style::Short;
+ } else if (StringEqualsLiteral(styleStr, "narrow")) {
+ style = mozilla::intl::DisplayNames::Style::Narrow;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(styleStr, "abbreviated"));
+ style = mozilla::intl::DisplayNames::Style::Abbreviated;
+ }
+ }
+
+ mozilla::intl::DisplayNames::LanguageDisplay languageDisplay;
+ {
+ JSLinearString* language = args[4].toString()->ensureLinear(cx);
+ if (!language) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(language, "dialect")) {
+ languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Dialect;
+ } else {
+ MOZ_ASSERT(language->empty() ||
+ StringEqualsLiteral(language, "standard"));
+ languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Standard;
+ }
+ }
+
+ mozilla::intl::DisplayNames::Fallback fallback;
+ {
+ JSLinearString* fallbackStr = args[5].toString()->ensureLinear(cx);
+ if (!fallbackStr) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(fallbackStr, "none")) {
+ fallback = mozilla::intl::DisplayNames::Fallback::None;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(fallbackStr, "code"));
+ fallback = mozilla::intl::DisplayNames::Fallback::Code;
+ }
+ }
+
+ Rooted<JSLinearString*> type(cx, args[6].toString()->ensureLinear(cx));
+ if (!type) {
+ return false;
+ }
+
+ mozilla::intl::DisplayNames::Options options{
+ style,
+ languageDisplay,
+ };
+
+ // If a calendar exists, set it as an option.
+ JS::UniqueChars calendarChars = nullptr;
+ if (!calendar->empty()) {
+ calendarChars = JS_EncodeStringToUTF8(cx, calendar);
+ if (!calendarChars) {
+ return false;
+ }
+ }
+
+ mozilla::intl::DisplayNames* dn =
+ GetOrCreateDisplayNames(cx, displayNames, locale.get(), options);
+ if (!dn) {
+ return false;
+ }
+
+ // The "code" is usually a small ASCII string, so try to avoid an allocation
+ // by copying it to the stack. Unfortunately we can't pass a string span of
+ // the JSString directly to the unified DisplayNames API, as the
+ // intl::FormatBuffer will be written to. This writing can trigger a GC and
+ // invalidate the span, creating a nogc rooting hazard.
+ JS::UniqueChars utf8 = nullptr;
+ unsigned char ascii[32];
+ mozilla::Span<const char> codeSpan = nullptr;
+ if (code->length() < 32 && code->hasLatin1Chars() && StringIsAscii(code)) {
+ JS::AutoCheckCannotGC nogc;
+ mozilla::PodCopy(ascii, code->latin1Chars(nogc), code->length());
+ codeSpan =
+ mozilla::Span(reinterpret_cast<const char*>(ascii), code->length());
+ } else {
+ utf8 = JS_EncodeStringToUTF8(cx, code);
+ if (!utf8) {
+ return false;
+ }
+ codeSpan = mozilla::MakeStringSpan(utf8.get());
+ }
+
+ intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ mozilla::Result<mozilla::Ok, mozilla::intl::DisplayNamesError> result =
+ mozilla::Ok{};
+
+ if (StringEqualsLiteral(type, "language")) {
+ result = dn->GetLanguage(buffer, codeSpan, fallback);
+ } else if (StringEqualsLiteral(type, "script")) {
+ result = dn->GetScript(buffer, codeSpan, fallback);
+ } else if (StringEqualsLiteral(type, "region")) {
+ result = dn->GetRegion(buffer, codeSpan, fallback);
+ } else if (StringEqualsLiteral(type, "currency")) {
+ result = dn->GetCurrency(buffer, codeSpan, fallback);
+ } else if (StringEqualsLiteral(type, "calendar")) {
+ result = dn->GetCalendar(buffer, codeSpan, fallback);
+ } else if (StringEqualsLiteral(type, "weekday")) {
+ double d = LinearStringToNumber(code);
+ if (!IsInteger(d) || d < 1 || d > 7) {
+ ReportInvalidOptionError(cx, "weekday", d);
+ return false;
+ }
+ result =
+ dn->GetWeekday(buffer, static_cast<mozilla::intl::Weekday>(d),
+ mozilla::MakeStringSpan(calendarChars.get()), fallback);
+ } else if (StringEqualsLiteral(type, "month")) {
+ double d = LinearStringToNumber(code);
+ if (!IsInteger(d) || d < 1 || d > 13) {
+ ReportInvalidOptionError(cx, "month", d);
+ return false;
+ }
+
+ result =
+ dn->GetMonth(buffer, static_cast<mozilla::intl::Month>(d),
+ mozilla::MakeStringSpan(calendarChars.get()), fallback);
+
+ } else if (StringEqualsLiteral(type, "quarter")) {
+ double d = LinearStringToNumber(code);
+
+ // Inlined implementation of `IsValidQuarterCode ( quarter )`.
+ if (!IsInteger(d) || d < 1 || d > 4) {
+ ReportInvalidOptionError(cx, "quarter", d);
+ return false;
+ }
+
+ result =
+ dn->GetQuarter(buffer, static_cast<mozilla::intl::Quarter>(d),
+ mozilla::MakeStringSpan(calendarChars.get()), fallback);
+
+ } else if (StringEqualsLiteral(type, "dayPeriod")) {
+ mozilla::intl::DayPeriod dayPeriod;
+ if (StringEqualsLiteral(code, "am")) {
+ dayPeriod = mozilla::intl::DayPeriod::AM;
+ } else if (StringEqualsLiteral(code, "pm")) {
+ dayPeriod = mozilla::intl::DayPeriod::PM;
+ } else {
+ ReportInvalidOptionError(cx, "dayPeriod", code);
+ return false;
+ }
+ result = dn->GetDayPeriod(buffer, dayPeriod,
+ mozilla::MakeStringSpan(calendarChars.get()),
+ fallback);
+
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(type, "dateTimeField"));
+ mozilla::intl::DateTimeField field;
+ if (StringEqualsLiteral(code, "era")) {
+ field = mozilla::intl::DateTimeField::Era;
+ } else if (StringEqualsLiteral(code, "year")) {
+ field = mozilla::intl::DateTimeField::Year;
+ } else if (StringEqualsLiteral(code, "quarter")) {
+ field = mozilla::intl::DateTimeField::Quarter;
+ } else if (StringEqualsLiteral(code, "month")) {
+ field = mozilla::intl::DateTimeField::Month;
+ } else if (StringEqualsLiteral(code, "weekOfYear")) {
+ field = mozilla::intl::DateTimeField::WeekOfYear;
+ } else if (StringEqualsLiteral(code, "weekday")) {
+ field = mozilla::intl::DateTimeField::Weekday;
+ } else if (StringEqualsLiteral(code, "day")) {
+ field = mozilla::intl::DateTimeField::Day;
+ } else if (StringEqualsLiteral(code, "dayPeriod")) {
+ field = mozilla::intl::DateTimeField::DayPeriod;
+ } else if (StringEqualsLiteral(code, "hour")) {
+ field = mozilla::intl::DateTimeField::Hour;
+ } else if (StringEqualsLiteral(code, "minute")) {
+ field = mozilla::intl::DateTimeField::Minute;
+ } else if (StringEqualsLiteral(code, "second")) {
+ field = mozilla::intl::DateTimeField::Second;
+ } else if (StringEqualsLiteral(code, "timeZoneName")) {
+ field = mozilla::intl::DateTimeField::TimeZoneName;
+ } else {
+ ReportInvalidOptionError(cx, "dateTimeField", code);
+ return false;
+ }
+
+ intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+ mozilla::intl::DateTimePatternGenerator* dtpgen =
+ sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
+ if (!dtpgen) {
+ return false;
+ }
+
+ result = dn->GetDateTimeField(buffer, field, *dtpgen, fallback);
+ }
+
+ if (result.isErr()) {
+ switch (result.unwrapErr()) {
+ case mozilla::intl::DisplayNamesError::InternalError:
+ intl::ReportInternalError(cx);
+ break;
+ case mozilla::intl::DisplayNamesError::OutOfMemory:
+ ReportOutOfMemory(cx);
+ break;
+ case mozilla::intl::DisplayNamesError::InvalidOption:
+ ReportInvalidOptionError(cx, type, code);
+ break;
+ case mozilla::intl::DisplayNamesError::DuplicateVariantSubtag:
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DUPLICATE_VARIANT_SUBTAG);
+ break;
+ case mozilla::intl::DisplayNamesError::InvalidLanguageTag:
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INVALID_LANGUAGE_TAG);
+ break;
+ }
+ return false;
+ }
+
+ JSString* str = buffer.toString(cx);
+ if (!str) {
+ return false;
+ }
+
+ if (str->empty()) {
+ args.rval().setUndefined();
+ } else {
+ args.rval().setString(str);
+ }
+
+ return true;
+}
diff --git a/js/src/builtin/intl/DisplayNames.h b/js/src/builtin/intl/DisplayNames.h
new file mode 100644
index 0000000000..9fd6c63a62
--- /dev/null
+++ b/js/src/builtin/intl/DisplayNames.h
@@ -0,0 +1,79 @@
+/* -*- 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_DisplayNames_h
+#define builtin_intl_DisplayNames_h
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "jstypes.h"
+#include "NamespaceImports.h"
+
+#include "builtin/SelfHostingDefines.h"
+#include "js/Class.h" // JSClass, JSClassOps, js::ClassSpec
+#include "js/TypeDecls.h"
+#include "js/Value.h"
+#include "vm/NativeObject.h"
+
+struct JS_PUBLIC_API JSContext;
+
+namespace mozilla::intl {
+class DisplayNames;
+}
+
+namespace js {
+struct ClassSpec;
+
+class DisplayNamesObject : public NativeObject {
+ public:
+ static const JSClass class_;
+ static const JSClass& protoClass_;
+
+ static constexpr uint32_t INTERNALS_SLOT = 0;
+ static constexpr uint32_t LOCALE_DISPLAY_NAMES_SLOT = 1;
+ static constexpr uint32_t SLOT_COUNT = 3;
+
+ static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT,
+ "INTERNALS_SLOT must match self-hosting define for internals "
+ "object slot");
+
+ // Estimated memory use for ULocaleDisplayNames (see IcuMemoryUsage).
+ static constexpr size_t EstimatedMemoryUse = 1238;
+
+ mozilla::intl::DisplayNames* getDisplayNames() const {
+ const auto& slot = getFixedSlot(LOCALE_DISPLAY_NAMES_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::DisplayNames*>(slot.toPrivate());
+ }
+
+ void setDisplayNames(mozilla::intl::DisplayNames* displayNames) {
+ setFixedSlot(LOCALE_DISPLAY_NAMES_SLOT, PrivateValue(displayNames));
+ }
+
+ private:
+ static const JSClassOps classOps_;
+ static const ClassSpec classSpec_;
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+};
+
+/**
+ * Return the display name for the requested code or undefined if no applicable
+ * display name was found.
+ *
+ * Usage: result = intl_ComputeDisplayName(displayNames, locale, calendar,
+ * style, languageDisplay, fallback,
+ * type, code)
+ */
+[[nodiscard]] extern bool intl_ComputeDisplayName(JSContext* cx, unsigned argc,
+ Value* vp);
+
+} // namespace js
+
+#endif /* builtin_intl_DisplayNames_h */
diff --git a/js/src/builtin/intl/DisplayNames.js b/js/src/builtin/intl/DisplayNames.js
new file mode 100644
index 0000000000..5eb699fc89
--- /dev/null
+++ b/js/src/builtin/intl/DisplayNames.js
@@ -0,0 +1,418 @@
+/* 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.DisplayNames internal properties.
+ */
+function displayNamesLocaleData() {
+ // Intl.DisplayNames doesn't support any extension keys.
+ return {};
+}
+var displayNamesInternalProperties = {
+ localeData: displayNamesLocaleData,
+ relevantExtensionKeys: [],
+};
+
+function mozDisplayNamesLocaleData() {
+ return {
+ ca: intl_availableCalendars,
+ default: {
+ ca: intl_defaultCalendar,
+ },
+ };
+}
+var mozDisplayNamesInternalProperties = {
+ localeData: mozDisplayNamesLocaleData,
+ relevantExtensionKeys: ["ca"],
+};
+
+/**
+ * Intl.DisplayNames ( [ locales [ , options ] ] )
+ *
+ * Compute an internal properties object from |lazyDisplayNamesData|.
+ */
+function resolveDisplayNamesInternals(lazyDisplayNamesData) {
+ assert(IsObject(lazyDisplayNamesData), "lazy data not an object?");
+
+ var internalProps = std_Object_create(null);
+
+ var mozExtensions = lazyDisplayNamesData.mozExtensions;
+
+ var DisplayNames = mozExtensions
+ ? mozDisplayNamesInternalProperties
+ : displayNamesInternalProperties;
+
+ // Compute effective locale.
+
+ // Step 7.
+ var localeData = DisplayNames.localeData;
+
+ // Step 10.
+ var r = ResolveLocale(
+ "DisplayNames",
+ lazyDisplayNamesData.requestedLocales,
+ lazyDisplayNamesData.opt,
+ DisplayNames.relevantExtensionKeys,
+ localeData
+ );
+
+ // Step 12.
+ internalProps.style = lazyDisplayNamesData.style;
+
+ // Step 14.
+ var type = lazyDisplayNamesData.type;
+ internalProps.type = type;
+
+ // Step 16.
+ internalProps.fallback = lazyDisplayNamesData.fallback;
+
+ // Step 17.
+ internalProps.locale = r.locale;
+
+ // Step 25.
+ if (type === "language") {
+ internalProps.languageDisplay = lazyDisplayNamesData.languageDisplay;
+ }
+
+ if (mozExtensions) {
+ internalProps.calendar = r.ca;
+ }
+
+ // The caller is responsible for associating |internalProps| with the right
+ // object using |setInternalProperties|.
+ return internalProps;
+}
+
+/**
+ * Returns an object containing the DisplayNames internal properties of |obj|.
+ */
+function getDisplayNamesInternals(obj) {
+ assert(IsObject(obj), "getDisplayNamesInternals called with non-object");
+ assert(
+ intl_GuardToDisplayNames(obj) !== null,
+ "getDisplayNamesInternals called with non-DisplayNames"
+ );
+
+ var internals = getIntlObjectInternals(obj);
+ assert(
+ internals.type === "DisplayNames",
+ "bad type escaped getIntlObjectInternals"
+ );
+
+ // If internal properties have already been computed, use them.
+ var internalProps = maybeInternalProperties(internals);
+ if (internalProps) {
+ return internalProps;
+ }
+
+ // Otherwise it's time to fully create them.
+ internalProps = resolveDisplayNamesInternals(internals.lazyData);
+ setInternalProperties(internals, internalProps);
+ return internalProps;
+}
+
+/**
+ * Intl.DisplayNames ( [ locales [ , options ] ] )
+ *
+ * Initializes an object as a DisplayNames.
+ *
+ * This method is complicated a moderate bit by its implementing initialization
+ * as a *lazy* concept. Everything that must happen now, does -- but we defer
+ * all the work we can until the object is actually used as a DisplayNames.
+ * This later work occurs in |resolveDisplayNamesInternals|; steps not noted
+ * here occur there.
+ */
+function InitializeDisplayNames(displayNames, locales, options, mozExtensions) {
+ assert(
+ IsObject(displayNames),
+ "InitializeDisplayNames called with non-object"
+ );
+ assert(
+ intl_GuardToDisplayNames(displayNames) !== null,
+ "InitializeDisplayNames called with non-DisplayNames"
+ );
+
+ // Lazy DisplayNames data has the following structure:
+ //
+ // {
+ // requestedLocales: List of locales,
+ //
+ // opt: // opt object computed in InitializeDisplayNames
+ // {
+ // localeMatcher: "lookup" / "best fit",
+ //
+ // ca: string matching a Unicode extension type, // optional
+ // }
+ //
+ // localeMatcher: "lookup" / "best fit",
+ //
+ // style: "narrow" / "short" / "abbreviated" / "long",
+ //
+ // type: "language" / "region" / "script" / "currency" / "weekday" /
+ // "month" / "quarter" / "dayPeriod" / "dateTimeField"
+ //
+ // fallback: "code" / "none",
+ //
+ // // field present only if type === "language":
+ // languageDisplay: "dialect" / "standard",
+ //
+ // mozExtensions: true / false,
+ // }
+ //
+ // Note that lazy data is only installed as a final step of initialization,
+ // so every DisplayNames lazy data object has *all* these properties, never a
+ // subset of them.
+ var lazyDisplayNamesData = std_Object_create(null);
+
+ // Step 3.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+ lazyDisplayNamesData.requestedLocales = requestedLocales;
+
+ // Step 4.
+ if (!IsObject(options)) {
+ ThrowTypeError(
+ JSMSG_OBJECT_REQUIRED,
+ options === null ? "null" : typeof options
+ );
+ }
+
+ // Step 5.
+ var opt = new_Record();
+ lazyDisplayNamesData.opt = opt;
+ lazyDisplayNamesData.mozExtensions = mozExtensions;
+
+ // Steps 7-8.
+ var matcher = GetOption(
+ options,
+ "localeMatcher",
+ "string",
+ ["lookup", "best fit"],
+ "best fit"
+ );
+ opt.localeMatcher = matcher;
+
+ if (mozExtensions) {
+ var calendar = GetOption(
+ options,
+ "calendar",
+ "string",
+ undefined,
+ undefined
+ );
+
+ if (calendar !== undefined) {
+ calendar = intl_ValidateAndCanonicalizeUnicodeExtensionType(
+ calendar,
+ "calendar",
+ "ca"
+ );
+ }
+
+ opt.ca = calendar;
+ }
+
+ // Step 10.
+ var style;
+ if (mozExtensions) {
+ style = GetOption(
+ options,
+ "style",
+ "string",
+ ["narrow", "short", "abbreviated", "long"],
+ "long"
+ );
+ } else {
+ style = GetOption(
+ options,
+ "style",
+ "string",
+ ["narrow", "short", "long"],
+ "long"
+ );
+ }
+
+ // Step 11.
+ lazyDisplayNamesData.style = style;
+
+ // Step 12.
+ var type;
+ if (mozExtensions) {
+ type = GetOption(
+ options,
+ "type",
+ "string",
+ [
+ "language",
+ "region",
+ "script",
+ "currency",
+ "calendar",
+ "dateTimeField",
+ "weekday",
+ "month",
+ "quarter",
+ "dayPeriod",
+ ],
+ undefined
+ );
+ } else {
+ type = GetOption(
+ options,
+ "type",
+ "string",
+ ["language", "region", "script", "currency", "calendar", "dateTimeField"],
+ undefined
+ );
+ }
+
+ // Step 13.
+ if (type === undefined) {
+ ThrowTypeError(JSMSG_UNDEFINED_TYPE);
+ }
+
+ // Step 14.
+ lazyDisplayNamesData.type = type;
+
+ // Step 15.
+ var fallback = GetOption(
+ options,
+ "fallback",
+ "string",
+ ["code", "none"],
+ "code"
+ );
+
+ // Step 16.
+ lazyDisplayNamesData.fallback = fallback;
+
+ // Step 24.
+ var languageDisplay = GetOption(
+ options,
+ "languageDisplay",
+ "string",
+ ["dialect", "standard"],
+ "dialect"
+ );
+
+ // Step 25.
+ if (type === "language") {
+ lazyDisplayNamesData.languageDisplay = languageDisplay;
+ }
+
+ // We've done everything that must be done now: mark the lazy data as fully
+ // computed and install it.
+ initializeIntlObject(displayNames, "DisplayNames", lazyDisplayNamesData);
+}
+
+/**
+ * Returns the subset of the given locale list for which this locale list has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ */
+function Intl_DisplayNames_supportedLocalesOf(locales /*, options*/) {
+ var options = arguments.length > 1 ? arguments[1] : undefined;
+
+ // Step 1.
+ var availableLocales = "DisplayNames";
+
+ // Step 2.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+
+ // Step 3.
+ return SupportedLocales(availableLocales, requestedLocales, options);
+}
+
+/**
+ * Returns the resolved options for a DisplayNames object.
+ */
+function Intl_DisplayNames_of(code) {
+ // Step 1.
+ var displayNames = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(displayNames) ||
+ (displayNames = intl_GuardToDisplayNames(displayNames)) === null
+ ) {
+ return callFunction(
+ intl_CallDisplayNamesMethodIfWrapped,
+ this,
+ "Intl_DisplayNames_of"
+ );
+ }
+
+ code = ToString(code);
+
+ var internals = getDisplayNamesInternals(displayNames);
+
+ // Unpack the internals object to avoid a slow runtime to selfhosted JS call
+ // in |intl_ComputeDisplayName()|.
+ var {
+ locale,
+ calendar = "",
+ style,
+ type,
+ languageDisplay = "",
+ fallback,
+ } = internals;
+
+ // Steps 5-10.
+ return intl_ComputeDisplayName(
+ displayNames,
+ locale,
+ calendar,
+ style,
+ languageDisplay,
+ fallback,
+ type,
+ code
+ );
+}
+
+/**
+ * Returns the resolved options for a DisplayNames object.
+ */
+function Intl_DisplayNames_resolvedOptions() {
+ // Step 1.
+ var displayNames = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(displayNames) ||
+ (displayNames = intl_GuardToDisplayNames(displayNames)) === null
+ ) {
+ return callFunction(
+ intl_CallDisplayNamesMethodIfWrapped,
+ this,
+ "Intl_DisplayNames_resolvedOptions"
+ );
+ }
+
+ var internals = getDisplayNamesInternals(displayNames);
+
+ // Steps 4-5.
+ var options = {
+ locale: internals.locale,
+ style: internals.style,
+ type: internals.type,
+ fallback: internals.fallback,
+ };
+
+ // languageDisplay is only present for language display names.
+ assert(
+ hasOwn("languageDisplay", internals) === (internals.type === "language"),
+ "languageDisplay is present iff type is 'language'"
+ );
+
+ if (hasOwn("languageDisplay", internals)) {
+ DefineDataProperty(options, "languageDisplay", internals.languageDisplay);
+ }
+
+ if (hasOwn("calendar", internals)) {
+ DefineDataProperty(options, "calendar", internals.calendar);
+ }
+
+ // Step 6.
+ return options;
+}
diff --git a/js/src/builtin/intl/FormatBuffer.h b/js/src/builtin/intl/FormatBuffer.h
new file mode 100644
index 0000000000..73d450a546
--- /dev/null
+++ b/js/src/builtin/intl/FormatBuffer.h
@@ -0,0 +1,155 @@
+/* -*- 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_FormatBuffer_h
+#define builtin_intl_FormatBuffer_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Span.h"
+#include "mozilla/TextUtils.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "gc/Allocator.h"
+#include "js/AllocPolicy.h"
+#include "js/CharacterEncoding.h"
+#include "js/TypeDecls.h"
+#include "js/UniquePtr.h"
+#include "js/Vector.h"
+#include "vm/StringType.h"
+
+namespace js::intl {
+
+/**
+ * A buffer for formatting unified intl data.
+ */
+template <typename CharT, size_t MinInlineCapacity = 0,
+ class AllocPolicy = TempAllocPolicy>
+class FormatBuffer {
+ public:
+ using CharType = CharT;
+
+ // Allow move constructors, but not copy constructors, as this class owns a
+ // js::Vector.
+ FormatBuffer(FormatBuffer&& other) noexcept = default;
+ FormatBuffer& operator=(FormatBuffer&& other) noexcept = default;
+
+ explicit FormatBuffer(AllocPolicy aP = AllocPolicy())
+ : buffer_(std::move(aP)) {
+ // The initial capacity matches the requested minimum inline capacity, as
+ // long as it doesn't exceed |Vector::kMaxInlineBytes / sizeof(CharT)|. If
+ // this assertion should ever fail, either reduce |MinInlineCapacity| or
+ // make the FormatBuffer initialization fallible.
+ MOZ_ASSERT(buffer_.capacity() == MinInlineCapacity);
+ if constexpr (MinInlineCapacity > 0) {
+ // Ensure the full capacity is marked as reserved.
+ //
+ // Reserving the minimum inline capacity can never fail, even when
+ // simulating OOM.
+ MOZ_ALWAYS_TRUE(buffer_.reserve(MinInlineCapacity));
+ }
+ }
+
+ // Implicitly convert to a Span.
+ operator mozilla::Span<CharType>() { return buffer_; }
+ operator mozilla::Span<const CharType>() const { return buffer_; }
+
+ /**
+ * Ensures the buffer has enough space to accommodate |size| elements.
+ */
+ [[nodiscard]] bool reserve(size_t size) {
+ // Call |reserve| a second time to ensure its full capacity is marked as
+ // reserved.
+ return buffer_.reserve(size) && buffer_.reserve(buffer_.capacity());
+ }
+
+ /**
+ * Returns the raw data inside the buffer.
+ */
+ CharType* data() { return buffer_.begin(); }
+
+ /**
+ * Returns the count of elements written into the buffer.
+ */
+ size_t length() const { return buffer_.length(); }
+
+ /**
+ * Returns the buffer's overall capacity.
+ */
+ size_t capacity() const { return buffer_.capacity(); }
+
+ /**
+ * Resizes the buffer to the given amount of written elements.
+ */
+ void written(size_t amount) {
+ MOZ_ASSERT(amount <= buffer_.capacity());
+ // This sets |buffer_|'s internal size so that it matches how much was
+ // written. This is necessary because the write happens across FFI
+ // boundaries.
+ size_t curLength = length();
+ if (amount > curLength) {
+ buffer_.infallibleGrowByUninitialized(amount - curLength);
+ } else {
+ buffer_.shrinkBy(curLength - amount);
+ }
+ }
+
+ /**
+ * Copies the buffer's data to a JSString.
+ *
+ * TODO(#1715842) - This should be more explicit on needing to handle OOM
+ * errors. In this case it returns a nullptr that must be checked, but it may
+ * not be obvious.
+ */
+ JSLinearString* toString(JSContext* cx) const {
+ if constexpr (std::is_same_v<CharT, uint8_t> ||
+ std::is_same_v<CharT, unsigned char> ||
+ std::is_same_v<CharT, char>) {
+ // Handle the UTF-8 encoding case.
+ return NewStringCopyUTF8N(
+ cx, JS::UTF8Chars(buffer_.begin(), buffer_.length()));
+ } else {
+ // Handle the UTF-16 encoding case.
+ static_assert(std::is_same_v<CharT, char16_t>);
+ return NewStringCopyN<CanGC>(cx, buffer_.begin(), buffer_.length());
+ }
+ }
+
+ /**
+ * Copies the buffer's data to a JSString. The buffer must contain only
+ * ASCII characters.
+ */
+ JSLinearString* toAsciiString(JSContext* cx) const {
+ static_assert(std::is_same_v<CharT, char>);
+
+ MOZ_ASSERT(mozilla::IsAscii(buffer_));
+ return NewStringCopyN<CanGC>(cx, buffer_.begin(), buffer_.length());
+ }
+
+ /**
+ * Extract this buffer's content as a null-terminated string.
+ */
+ UniquePtr<CharType[], JS::FreePolicy> extractStringZ() {
+ // Adding the NUL character on an already null-terminated string is likely
+ // an error. If there's ever a valid use case which triggers this assertion,
+ // we should change the below code to only conditionally add '\0'.
+ MOZ_ASSERT_IF(!buffer_.empty(), buffer_.end()[-1] != '\0');
+
+ if (!buffer_.append('\0')) {
+ return nullptr;
+ }
+ return UniquePtr<CharType[], JS::FreePolicy>(
+ buffer_.extractOrCopyRawBuffer());
+ }
+
+ private:
+ js::Vector<CharT, MinInlineCapacity, AllocPolicy> buffer_;
+};
+
+} // namespace js::intl
+
+#endif /* builtin_intl_FormatBuffer_h */
diff --git a/js/src/builtin/intl/IcuMemoryUsage.java b/js/src/builtin/intl/IcuMemoryUsage.java
new file mode 100644
index 0000000000..15e4f1fd38
--- /dev/null
+++ b/js/src/builtin/intl/IcuMemoryUsage.java
@@ -0,0 +1,260 @@
+/* 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/. */
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.regex.*;
+import java.util.stream.Collectors;
+
+/**
+ * Java program to estimate the memory usage of ICU objects (bug 1585536).
+ *
+ * It computes for each Intl constructor the amount of allocated memory. We're
+ * currently using the maximum memory ("max" in the output) to estimate the
+ * memory consumption of ICU objects.
+ *
+ * Insert before {@code JS_InitWithFailureDiagnostic} in "js.cpp":
+ *
+ * <pre>
+ * <code>
+ * JS_SetICUMemoryFunctions(
+ * [](const void*, size_t size) {
+ * void* ptr = malloc(size);
+ * if (ptr) {
+ * printf(" alloc: %p -> %zu\n", ptr, size);
+ * }
+ * return ptr;
+ * },
+ * [](const void*, void* p, size_t size) {
+ * void* ptr = realloc(p, size);
+ * if (p) {
+ * printf(" realloc: %p -> %p -> %zu\n", p, ptr, size);
+ * } else {
+ * printf(" alloc: %p -> %zu\n", ptr, size);
+ * }
+ * return ptr;
+ * },
+ * [](const void*, void* p) {
+ * if (p) {
+ * printf(" free: %p\n", p);
+ * }
+ * free(p);
+ * });
+ * </code>
+ * </pre>
+ *
+ * Run this script with:
+ * {@code java --enable-preview --source=14 IcuMemoryUsage.java $MOZ_JS_SHELL}.
+ */
+@SuppressWarnings("preview")
+public class IcuMemoryUsage {
+ private enum Phase {
+ None, Create, Init, Destroy, Collect, Quit
+ }
+
+ private static final class Memory {
+ private Phase phase = Phase.None;
+ private HashMap<Long, Map.Entry<Phase, Long>> allocations = new HashMap<>();
+ private HashSet<Long> freed = new HashSet<>();
+ private HashMap<Long, Map.Entry<Phase, Long>> completeAllocations = new HashMap<>();
+ private int allocCount = 0;
+ private ArrayList<Long> allocSizes = new ArrayList<>();
+
+ void transition(Phase nextPhase) {
+ assert phase.ordinal() + 1 == nextPhase.ordinal() || (phase == Phase.Collect && nextPhase == Phase.Create);
+ phase = nextPhase;
+
+ // Create a clean slate when starting a new create cycle or before termination.
+ if (phase == Phase.Create || phase == Phase.Quit) {
+ transferAllocations();
+ }
+
+ // Only measure the allocation size when creating the second object with the
+ // same locale.
+ if (phase == Phase.Collect && ++allocCount % 2 == 0) {
+ long size = allocations.values().stream().map(Map.Entry::getValue).reduce(0L, (a, c) -> a + c);
+ allocSizes.add(size);
+ }
+ }
+
+ void transferAllocations() {
+ completeAllocations.putAll(allocations);
+ completeAllocations.keySet().removeAll(freed);
+ allocations.clear();
+ freed.clear();
+ }
+
+ void alloc(long ptr, long size) {
+ allocations.put(ptr, Map.entry(phase, size));
+ }
+
+ void realloc(long oldPtr, long newPtr, long size) {
+ free(oldPtr);
+ allocations.put(newPtr, Map.entry(phase, size));
+ }
+
+ void free(long ptr) {
+ if (allocations.remove(ptr) == null) {
+ freed.add(ptr);
+ }
+ }
+
+ LongSummaryStatistics statistics() {
+ return allocSizes.stream().collect(Collectors.summarizingLong(Long::valueOf));
+ }
+
+ double percentile(double p) {
+ var size = allocSizes.size();
+ return allocSizes.stream().sorted().skip((long) ((size - 1) * p)).limit(2 - size % 2)
+ .mapToDouble(Long::doubleValue).average().getAsDouble();
+ }
+
+ long persistent() {
+ return completeAllocations.values().stream().map(Map.Entry::getValue).reduce(0L, (a, c) -> a + c);
+ }
+ }
+
+ private static long parseSize(Matcher m, int group) {
+ return Long.parseLong(m.group(group), 10);
+ }
+
+ private static long parsePointer(Matcher m, int group) {
+ return Long.parseLong(m.group(group), 16);
+ }
+
+ private static void measure(String exec, String constructor, String description, String initializer) throws IOException {
+ var locales = Arrays.stream(Locale.getAvailableLocales()).map(Locale::toLanguageTag).sorted()
+ .collect(Collectors.toUnmodifiableList());
+
+ var pb = new ProcessBuilder(exec, "--file=-", "--", constructor, initializer,
+ locales.stream().collect(Collectors.joining(",")));
+ var process = pb.start();
+
+ try (var writer = new BufferedWriter(
+ new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
+ writer.write(sourceCode);
+ writer.flush();
+ }
+
+ var memory = new Memory();
+
+ try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ var reAlloc = Pattern.compile("\\s+alloc: 0x(\\p{XDigit}+) -> (\\p{Digit}+)");
+ var reRealloc = Pattern.compile("\\s+realloc: 0x(\\p{XDigit}+) -> 0x(\\p{XDigit}+) -> (\\p{Digit}+)");
+ var reFree = Pattern.compile("\\s+free: 0x(\\p{XDigit}+)");
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Matcher m;
+ if ((m = reAlloc.matcher(line)).matches()) {
+ var ptr = parsePointer(m, 1);
+ var size = parseSize(m, 2);
+ memory.alloc(ptr, size);
+ } else if ((m = reRealloc.matcher(line)).matches()) {
+ var oldPtr = parsePointer(m, 1);
+ var newPtr = parsePointer(m, 2);
+ var size = parseSize(m, 3);
+ memory.realloc(oldPtr, newPtr, size);
+ } else if ((m = reFree.matcher(line)).matches()) {
+ var ptr = parsePointer(m, 1);
+ memory.free(ptr);
+ } else {
+ memory.transition(Phase.valueOf(line));
+ }
+ }
+ }
+
+ try (var errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+ String line;
+ while ((line = errorReader.readLine()) != null) {
+ System.err.println(line);
+ }
+ }
+
+ var stats = memory.statistics();
+
+ System.out.printf("%s%n", description);
+ System.out.printf(" max: %d%n", stats.getMax());
+ System.out.printf(" min: %d%n", stats.getMin());
+ System.out.printf(" avg: %.0f%n", stats.getAverage());
+ System.out.printf(" 50p: %.0f%n", memory.percentile(0.50));
+ System.out.printf(" 75p: %.0f%n", memory.percentile(0.75));
+ System.out.printf(" 85p: %.0f%n", memory.percentile(0.85));
+ System.out.printf(" 95p: %.0f%n", memory.percentile(0.95));
+ System.out.printf(" 99p: %.0f%n", memory.percentile(0.99));
+ System.out.printf(" mem: %d%n", memory.persistent());
+
+ memory.transferAllocations();
+ assert memory.persistent() == 0 : String.format("Leaked %d bytes", memory.persistent());
+ }
+
+ public static void main(String[] args) throws IOException {
+ if (args.length == 0) {
+ throw new RuntimeException("The first argument must point to the SpiderMonkey shell executable");
+ }
+
+ record Entry (String constructor, String description, String initializer) {
+ public static Entry of(String constructor, String description, String initializer) {
+ return new Entry(constructor, description, initializer);
+ }
+
+ public static Entry of(String constructor, String initializer) {
+ return new Entry(constructor, constructor, initializer);
+ }
+ }
+
+ var objects = new ArrayList<Entry>();
+ objects.add(Entry.of("Collator", "o.compare('a', 'b')"));
+ objects.add(Entry.of("DateTimeFormat", "DateTimeFormat (UDateFormat)", "o.format(0)"));
+ objects.add(Entry.of("DateTimeFormat", "DateTimeFormat (UDateFormat+UDateIntervalFormat)",
+ "o.formatRange(0, 24*60*60*1000)"));
+ objects.add(Entry.of("DisplayNames", "o.of('en')"));
+ objects.add(Entry.of("ListFormat", "o.format(['a', 'b'])"));
+ objects.add(Entry.of("NumberFormat", "o.format(0)"));
+ objects.add(Entry.of("NumberFormat", "NumberFormat (UNumberRangeFormatter)",
+ "o.formatRange(0, 1000)"));
+ objects.add(Entry.of("PluralRules", "o.select(0)"));
+ objects.add(Entry.of("RelativeTimeFormat", "o.format(0, 'hour')"));
+
+ for (var entry : objects) {
+ measure(args[0], entry.constructor, entry.description, entry.initializer);
+ }
+ }
+
+ private static final String sourceCode = """
+const constructorName = scriptArgs[0];
+const initializer = Function("o", scriptArgs[1]);
+const locales = scriptArgs[2].split(",");
+
+const extras = {};
+addIntlExtras(extras);
+
+for (let i = 0; i < locales.length; ++i) {
+ // Loop twice in case the first time we create an object with a new locale
+ // allocates additional memory when loading the locale data.
+ for (let j = 0; j < 2; ++j) {
+ let constructor = Intl[constructorName];
+ let options = undefined;
+ if (constructor === Intl.DisplayNames) {
+ options = {type: "language"};
+ }
+
+ print("Create");
+ let obj = new constructor(locales[i], options);
+
+ print("Init");
+ initializer(obj);
+
+ print("Destroy");
+ gc();
+ gc();
+ print("Collect");
+ }
+}
+
+print("Quit");
+quit();
+""";
+}
diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp
new file mode 100644
index 0000000000..299964d07b
--- /dev/null
+++ b/js/src/builtin/intl/IntlObject.cpp
@@ -0,0 +1,910 @@
+/* -*- 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/. */
+
+/* Implementation of the Intl object and its non-constructor properties. */
+
+#include "builtin/intl/IntlObject.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/Calendar.h"
+#include "mozilla/intl/Collator.h"
+#include "mozilla/intl/Currency.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/intl/MeasureUnitGenerated.h"
+#include "mozilla/intl/TimeZone.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <iterator>
+#include <string_view>
+
+#include "builtin/Array.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/FormatBuffer.h"
+#include "builtin/intl/NumberingSystemsGenerated.h"
+#include "builtin/intl/SharedIntlData.h"
+#include "builtin/intl/StringAsciiChars.h"
+#include "ds/Sort.h"
+#include "js/Class.h"
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "js/GCAPI.h"
+#include "js/GCVector.h"
+#include "js/PropertySpec.h"
+#include "js/Result.h"
+#include "js/StableStringChars.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSAtom.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/StringType.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+/******************** Intl ********************/
+
+bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+
+ auto result = mozilla::intl::Calendar::TryCreate(locale.get());
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+ auto calendar = result.unwrap();
+
+ RootedObject info(cx, NewPlainObject(cx));
+ if (!info) {
+ return false;
+ }
+
+ RootedValue v(cx);
+
+ v.setInt32(static_cast<int32_t>(calendar->GetFirstDayOfWeek()));
+ if (!DefineDataProperty(cx, info, cx->names().firstDayOfWeek, v)) {
+ return false;
+ }
+
+ v.setInt32(calendar->GetMinimalDaysInFirstWeek());
+ if (!DefineDataProperty(cx, info, cx->names().minDays, v)) {
+ return false;
+ }
+
+ Rooted<ArrayObject*> weekendArray(cx, NewDenseEmptyArray(cx));
+ if (!weekendArray) {
+ return false;
+ }
+
+ auto weekend = calendar->GetWeekend();
+ if (weekend.isErr()) {
+ intl::ReportInternalError(cx, weekend.unwrapErr());
+ return false;
+ }
+
+ for (auto day : weekend.unwrap()) {
+ if (!NewbornArrayPush(cx, weekendArray,
+ Int32Value(static_cast<int32_t>(day)))) {
+ return false;
+ }
+ }
+
+ v.setObject(*weekendArray);
+ if (!DefineDataProperty(cx, info, cx->names().weekend, v)) {
+ return false;
+ }
+
+ args.rval().setObject(*info);
+ return true;
+}
+
+static void ReportBadKey(JSContext* cx, JSString* key) {
+ if (UniqueChars chars = QuoteString(cx, key, '"')) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY,
+ chars.get());
+ }
+}
+
+static bool SameOrParentLocale(JSLinearString* locale,
+ JSLinearString* otherLocale) {
+ // Return true if |locale| is the same locale as |otherLocale|.
+ if (locale->length() == otherLocale->length()) {
+ return EqualStrings(locale, otherLocale);
+ }
+
+ // Also return true if |locale| is the parent locale of |otherLocale|.
+ if (locale->length() < otherLocale->length()) {
+ return HasSubstringAt(otherLocale, locale, 0) &&
+ otherLocale->latin1OrTwoByteChar(locale->length()) == '-';
+ }
+
+ return false;
+}
+
+using SupportedLocaleKind = js::intl::SharedIntlData::SupportedLocaleKind;
+
+// 9.2.2 BestAvailableLocale ( availableLocales, locale )
+static JS::Result<JSLinearString*> BestAvailableLocale(
+ JSContext* cx, SupportedLocaleKind kind, Handle<JSLinearString*> locale,
+ Handle<JSLinearString*> defaultLocale) {
+ // In the spec, [[availableLocales]] is formally a list of all available
+ // locales. But in our implementation, it's an *incomplete* list, not
+ // necessarily including the default locale (and all locales implied by it,
+ // e.g. "de" implied by "de-CH"), if that locale isn't in every
+ // [[availableLocales]] list (because that locale is supported through
+ // fallback, e.g. "de-CH" supported through "de").
+ //
+ // If we're considering the default locale, augment the spec loop with
+ // additional checks to also test whether the current prefix is a prefix of
+ // the default locale.
+
+ intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+
+ auto findLast = [](const auto* chars, size_t length) {
+ auto rbegin = std::make_reverse_iterator(chars + length);
+ auto rend = std::make_reverse_iterator(chars);
+ auto p = std::find(rbegin, rend, '-');
+
+ // |dist(chars, p.base())| is equal to |dist(p, rend)|, pick whichever you
+ // find easier to reason about when using reserve iterators.
+ ptrdiff_t r = std::distance(chars, p.base());
+ MOZ_ASSERT(r == std::distance(p, rend));
+
+ // But always subtract one to convert from the reverse iterator result to
+ // the correspoding forward iterator value, because reserve iterators point
+ // to one element past the forward iterator value.
+ return r - 1;
+ };
+
+ // Step 1.
+ Rooted<JSLinearString*> candidate(cx, locale);
+
+ // Step 2.
+ while (true) {
+ // Step 2.a.
+ bool supported = false;
+ if (!sharedIntlData.isSupportedLocale(cx, kind, candidate, &supported)) {
+ return cx->alreadyReportedError();
+ }
+ if (supported) {
+ return candidate.get();
+ }
+
+ if (defaultLocale && SameOrParentLocale(candidate, defaultLocale)) {
+ return candidate.get();
+ }
+
+ // Step 2.b.
+ ptrdiff_t pos;
+ if (candidate->hasLatin1Chars()) {
+ JS::AutoCheckCannotGC nogc;
+ pos = findLast(candidate->latin1Chars(nogc), candidate->length());
+ } else {
+ JS::AutoCheckCannotGC nogc;
+ pos = findLast(candidate->twoByteChars(nogc), candidate->length());
+ }
+
+ if (pos < 0) {
+ return nullptr;
+ }
+
+ // Step 2.c.
+ size_t length = size_t(pos);
+ if (length >= 2 && candidate->latin1OrTwoByteChar(length - 2) == '-') {
+ length -= 2;
+ }
+
+ // Step 2.d.
+ candidate = NewDependentString(cx, candidate, 0, length);
+ if (!candidate) {
+ return cx->alreadyReportedError();
+ }
+ }
+}
+
+// 9.2.2 BestAvailableLocale ( availableLocales, locale )
+//
+// Carries an additional third argument in our implementation to provide the
+// default locale. See the doc-comment in the header file.
+bool js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+
+ SupportedLocaleKind kind;
+ {
+ JSLinearString* typeStr = args[0].toString()->ensureLinear(cx);
+ if (!typeStr) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(typeStr, "Collator")) {
+ kind = SupportedLocaleKind::Collator;
+ } else if (StringEqualsLiteral(typeStr, "DateTimeFormat")) {
+ kind = SupportedLocaleKind::DateTimeFormat;
+ } else if (StringEqualsLiteral(typeStr, "DisplayNames")) {
+ kind = SupportedLocaleKind::DisplayNames;
+ } else if (StringEqualsLiteral(typeStr, "ListFormat")) {
+ kind = SupportedLocaleKind::ListFormat;
+ } else if (StringEqualsLiteral(typeStr, "NumberFormat")) {
+ kind = SupportedLocaleKind::NumberFormat;
+ } else if (StringEqualsLiteral(typeStr, "PluralRules")) {
+ kind = SupportedLocaleKind::PluralRules;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(typeStr, "RelativeTimeFormat"));
+ kind = SupportedLocaleKind::RelativeTimeFormat;
+ }
+ }
+
+ Rooted<JSLinearString*> locale(cx, args[1].toString()->ensureLinear(cx));
+ if (!locale) {
+ return false;
+ }
+
+#ifdef DEBUG
+ {
+ MOZ_ASSERT(StringIsAscii(locale), "language tags are ASCII-only");
+
+ // |locale| is a structurally valid language tag.
+ mozilla::intl::Locale tag;
+
+ using ParserError = mozilla::intl::LocaleParser::ParserError;
+ mozilla::Result<mozilla::Ok, ParserError> parse_result = Ok();
+ {
+ intl::StringAsciiChars chars(locale);
+ if (!chars.init(cx)) {
+ return false;
+ }
+
+ parse_result = mozilla::intl::LocaleParser::TryParse(chars, tag);
+ }
+
+ if (parse_result.isErr()) {
+ MOZ_ASSERT(parse_result.unwrapErr() == ParserError::OutOfMemory,
+ "locale is a structurally valid language tag");
+
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ MOZ_ASSERT(!tag.GetUnicodeExtension(),
+ "locale must contain no Unicode extensions");
+
+ if (auto result = tag.Canonicalize(); result.isErr()) {
+ MOZ_ASSERT(
+ result.unwrapErr() !=
+ mozilla::intl::Locale::CanonicalizationError::DuplicateVariant);
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ JSLinearString* tagStr = buffer.toString(cx);
+ if (!tagStr) {
+ return false;
+ }
+
+ MOZ_ASSERT(EqualStrings(locale, tagStr),
+ "locale is a canonicalized language tag");
+ }
+#endif
+
+ MOZ_ASSERT(args[2].isNull() || args[2].isString());
+
+ Rooted<JSLinearString*> defaultLocale(cx);
+ if (args[2].isString()) {
+ defaultLocale = args[2].toString()->ensureLinear(cx);
+ if (!defaultLocale) {
+ return false;
+ }
+ }
+
+ JSString* result;
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, result, BestAvailableLocale(cx, kind, locale, defaultLocale));
+
+ if (result) {
+ args.rval().setString(result);
+ } else {
+ args.rval().setUndefined();
+ }
+ return true;
+}
+
+bool js::intl_supportedLocaleOrFallback(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+
+ Rooted<JSLinearString*> locale(cx, args[0].toString()->ensureLinear(cx));
+ if (!locale) {
+ return false;
+ }
+
+ mozilla::intl::Locale tag;
+ bool canParseLocale = false;
+ if (StringIsAscii(locale)) {
+ intl::StringAsciiChars chars(locale);
+ if (!chars.init(cx)) {
+ return false;
+ }
+
+ // Tell the analysis the |tag.canonicalize()| method can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ canParseLocale = mozilla::intl::LocaleParser::TryParse(chars, tag).isOk() &&
+ tag.Canonicalize().isOk();
+ }
+
+ Rooted<JSLinearString*> candidate(cx);
+ if (!canParseLocale) {
+ candidate = NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale());
+ if (!candidate) {
+ return false;
+ }
+ } else {
+ // The default locale must be in [[AvailableLocales]], and that list must
+ // not contain any locales with Unicode extension sequences, so remove any
+ // present in the candidate.
+ tag.ClearUnicodeExtension();
+
+ intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ candidate = buffer.toAsciiString(cx);
+ if (!candidate) {
+ return false;
+ }
+
+ // Certain old-style language tags lack a script code, but in current
+ // usage they *would* include a script code. Map these over to modern
+ // forms.
+ for (const auto& mapping : js::intl::oldStyleLanguageTagMappings) {
+ const char* oldStyle = mapping.oldStyle;
+ const char* modernStyle = mapping.modernStyle;
+
+ if (StringEqualsAscii(candidate, oldStyle)) {
+ candidate = NewStringCopyZ<CanGC>(cx, modernStyle);
+ if (!candidate) {
+ return false;
+ }
+ break;
+ }
+ }
+ }
+
+ // 9.1 Internal slots of Service Constructors
+ //
+ // - [[AvailableLocales]] is a List [...]. The list must include the value
+ // returned by the DefaultLocale abstract operation (6.2.4), [...].
+ //
+ // That implies we must ignore any candidate which isn't supported by all
+ // Intl service constructors.
+
+ Rooted<JSLinearString*> supportedCollator(cx);
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, supportedCollator,
+ BestAvailableLocale(cx, SupportedLocaleKind::Collator, candidate,
+ nullptr));
+
+ Rooted<JSLinearString*> supportedDateTimeFormat(cx);
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, supportedDateTimeFormat,
+ BestAvailableLocale(cx, SupportedLocaleKind::DateTimeFormat, candidate,
+ nullptr));
+
+#ifdef DEBUG
+ // Note: We don't test the supported locales of the remaining Intl service
+ // constructors, because the set of supported locales is exactly equal to
+ // the set of supported locales of Intl.DateTimeFormat.
+ for (auto kind :
+ {SupportedLocaleKind::DisplayNames, SupportedLocaleKind::ListFormat,
+ SupportedLocaleKind::NumberFormat, SupportedLocaleKind::PluralRules,
+ SupportedLocaleKind::RelativeTimeFormat}) {
+ JSLinearString* supported;
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, supported, BestAvailableLocale(cx, kind, candidate, nullptr));
+
+ MOZ_ASSERT(!!supported == !!supportedDateTimeFormat);
+ MOZ_ASSERT_IF(supported, EqualStrings(supported, supportedDateTimeFormat));
+ }
+#endif
+
+ // Accept the candidate locale if it is supported by all Intl service
+ // constructors.
+ if (supportedCollator && supportedDateTimeFormat) {
+ // Use the actually supported locale instead of the candidate locale. For
+ // example when the candidate locale "en-US-posix" is supported through
+ // "en-US", use "en-US" as the default locale.
+ //
+ // Also prefer the supported locale with more subtags. For example when
+ // requesting "de-CH" and Intl.DateTimeFormat supports "de-CH", but
+ // Intl.Collator only "de", still return "de-CH" as the result.
+ if (SameOrParentLocale(supportedCollator, supportedDateTimeFormat)) {
+ candidate = supportedDateTimeFormat;
+ } else {
+ candidate = supportedCollator;
+ }
+ } else {
+ candidate = NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale());
+ if (!candidate) {
+ return false;
+ }
+ }
+
+ args.rval().setString(candidate);
+ return true;
+}
+
+using StringList = GCVector<JSLinearString*>;
+
+/**
+ * Create a sorted array from a list of strings.
+ */
+static ArrayObject* CreateArrayFromList(JSContext* cx,
+ MutableHandle<StringList> list) {
+ // Reserve scratch space for MergeSort().
+ size_t initialLength = list.length();
+ if (!list.growBy(initialLength)) {
+ return nullptr;
+ }
+
+ // Sort all strings in alphabetical order.
+ MOZ_ALWAYS_TRUE(
+ MergeSort(list.begin(), initialLength, list.begin() + initialLength,
+ [](const auto* a, const auto* b, bool* lessOrEqual) {
+ *lessOrEqual = CompareStrings(a, b) <= 0;
+ return true;
+ }));
+
+ // Ensure we don't add duplicate entries to the array.
+ auto* end = std::unique(
+ list.begin(), list.begin() + initialLength,
+ [](const auto* a, const auto* b) { return EqualStrings(a, b); });
+
+ // std::unique leaves the elements after |end| with an unspecified value, so
+ // remove them first. And also delete the elements in the scratch space.
+ list.shrinkBy(std::distance(end, list.end()));
+
+ // And finally copy the strings into the result array.
+ auto* array = NewDenseFullyAllocatedArray(cx, list.length());
+ if (!array) {
+ return nullptr;
+ }
+ array->setDenseInitializedLength(list.length());
+
+ for (size_t i = 0; i < list.length(); ++i) {
+ array->initDenseElement(i, StringValue(list[i]));
+ }
+
+ return array;
+}
+
+/**
+ * Create an array from a sorted list of strings.
+ */
+template <size_t N>
+static ArrayObject* CreateArrayFromSortedList(
+ JSContext* cx, const std::array<const char*, N>& list) {
+ // Ensure the list is sorted and doesn't contain duplicates.
+#ifdef DEBUG
+ // See bug 1583449 for why the lambda can't be in the MOZ_ASSERT.
+ auto isLargerThanOrEqual = [](const auto& a, const auto& b) {
+ return std::strcmp(a, b) >= 0;
+ };
+#endif
+ MOZ_ASSERT(std::adjacent_find(std::begin(list), std::end(list),
+ isLargerThanOrEqual) == std::end(list));
+
+ size_t length = std::size(list);
+
+ Rooted<ArrayObject*> array(cx, NewDenseFullyAllocatedArray(cx, length));
+ if (!array) {
+ return nullptr;
+ }
+ array->ensureDenseInitializedLength(0, length);
+
+ for (size_t i = 0; i < length; ++i) {
+ auto* str = NewStringCopyZ<CanGC>(cx, list[i]);
+ if (!str) {
+ return nullptr;
+ }
+ array->initDenseElement(i, StringValue(str));
+ }
+ return array;
+}
+
+/**
+ * Create an array from an intl::Enumeration.
+ */
+template <const auto& unsupported, class Enumeration>
+static bool EnumerationIntoList(JSContext* cx, Enumeration values,
+ MutableHandle<StringList> list) {
+ for (auto value : values) {
+ if (value.isErr()) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ auto span = value.unwrap();
+
+ // Skip over known, unsupported values.
+ std::string_view sv(span.data(), span.size());
+ if (std::any_of(std::begin(unsupported), std::end(unsupported),
+ [sv](const auto& e) { return sv == e; })) {
+ continue;
+ }
+
+ auto* string = NewStringCopy<CanGC>(cx, span);
+ if (!string) {
+ return false;
+ }
+ if (!list.append(string)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Returns the list of calendar types which mustn't be returned by
+ * |Intl.supportedValuesOf()|.
+ */
+static constexpr auto UnsupportedCalendars() {
+ // No calendar values are currently unsupported.
+ return std::array<const char*, 0>{};
+}
+
+// Defined outside of the function to workaround bugs in GCC<9.
+// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>.
+static constexpr auto UnsupportedCalendarsArray = UnsupportedCalendars();
+
+/**
+ * AvailableCalendars ( )
+ */
+static ArrayObject* AvailableCalendars(JSContext* cx) {
+ Rooted<StringList> list(cx, StringList(cx));
+
+ {
+ // Hazard analysis complains that the mozilla::Result destructor calls a
+ // GC function, which is unsound when returning an unrooted value. Work
+ // around this issue by restricting the lifetime of |keywords| to a
+ // separate block.
+ auto keywords = mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale("");
+ if (keywords.isErr()) {
+ intl::ReportInternalError(cx, keywords.unwrapErr());
+ return nullptr;
+ }
+
+ static constexpr auto& unsupported = UnsupportedCalendarsArray;
+
+ if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) {
+ return nullptr;
+ }
+ }
+
+ return CreateArrayFromList(cx, &list);
+}
+
+/**
+ * Returns the list of collation types which mustn't be returned by
+ * |Intl.supportedValuesOf()|.
+ */
+static constexpr auto UnsupportedCollations() {
+ return std::array{
+ "search",
+ "standard",
+ };
+}
+
+// Defined outside of the function to workaround bugs in GCC<9.
+// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>.
+static constexpr auto UnsupportedCollationsArray = UnsupportedCollations();
+
+/**
+ * AvailableCollations ( )
+ */
+static ArrayObject* AvailableCollations(JSContext* cx) {
+ Rooted<StringList> list(cx, StringList(cx));
+
+ {
+ // Hazard analysis complains that the mozilla::Result destructor calls a
+ // GC function, which is unsound when returning an unrooted value. Work
+ // around this issue by restricting the lifetime of |keywords| to a
+ // separate block.
+ auto keywords = mozilla::intl::Collator::GetBcp47KeywordValues();
+ if (keywords.isErr()) {
+ intl::ReportInternalError(cx, keywords.unwrapErr());
+ return nullptr;
+ }
+
+ static constexpr auto& unsupported = UnsupportedCollationsArray;
+
+ if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) {
+ return nullptr;
+ }
+ }
+
+ return CreateArrayFromList(cx, &list);
+}
+
+/**
+ * Returns a list of known, unsupported currencies which are returned by
+ * |Currency::GetISOCurrencies()|.
+ */
+static constexpr auto UnsupportedCurrencies() {
+ // "MVP" is also marked with "questionable, remove?" in ucurr.cpp, but only
+ // this single currency code isn't supported by |Intl.DisplayNames| and
+ // therefore must be excluded by |Intl.supportedValuesOf|.
+ return std::array{
+ "LSM", // https://unicode-org.atlassian.net/browse/ICU-21687
+ };
+}
+
+/**
+ * Return a list of known, missing currencies which aren't returned by
+ * |Currency::GetISOCurrencies()|.
+ */
+static constexpr auto MissingCurrencies() {
+ return std::array{
+ "SLE", // https://unicode-org.atlassian.net/browse/ICU-21989
+ "VED", // https://unicode-org.atlassian.net/browse/ICU-21989
+ };
+}
+
+// Defined outside of the function to workaround bugs in GCC<9.
+// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>.
+static constexpr auto UnsupportedCurrenciesArray = UnsupportedCurrencies();
+static constexpr auto MissingCurrenciesArray = MissingCurrencies();
+
+/**
+ * AvailableCurrencies ( )
+ */
+static ArrayObject* AvailableCurrencies(JSContext* cx) {
+ Rooted<StringList> list(cx, StringList(cx));
+
+ {
+ // Hazard analysis complains that the mozilla::Result destructor calls a
+ // GC function, which is unsound when returning an unrooted value. Work
+ // around this issue by restricting the lifetime of |currencies| to a
+ // separate block.
+ auto currencies = mozilla::intl::Currency::GetISOCurrencies();
+ if (currencies.isErr()) {
+ intl::ReportInternalError(cx, currencies.unwrapErr());
+ return nullptr;
+ }
+
+ static constexpr auto& unsupported = UnsupportedCurrenciesArray;
+
+ if (!EnumerationIntoList<unsupported>(cx, currencies.unwrap(), &list)) {
+ return nullptr;
+ }
+ }
+
+ // Add known missing values.
+ for (const char* value : MissingCurrenciesArray) {
+ auto* string = NewStringCopyZ<CanGC>(cx, value);
+ if (!string) {
+ return nullptr;
+ }
+ if (!list.append(string)) {
+ return nullptr;
+ }
+ }
+
+ return CreateArrayFromList(cx, &list);
+}
+
+/**
+ * AvailableNumberingSystems ( )
+ */
+static ArrayObject* AvailableNumberingSystems(JSContext* cx) {
+ static constexpr std::array numberingSystems = {
+ NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS};
+
+ return CreateArrayFromSortedList(cx, numberingSystems);
+}
+
+/**
+ * AvailableTimeZones ( )
+ */
+static ArrayObject* AvailableTimeZones(JSContext* cx) {
+ // Unsorted list of canonical time zone names, possibly containing
+ // duplicates.
+ Rooted<StringList> timeZones(cx, StringList(cx));
+
+ intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+ auto iterResult = sharedIntlData.availableTimeZonesIteration(cx);
+ if (iterResult.isErr()) {
+ return nullptr;
+ }
+ auto iter = iterResult.unwrap();
+
+ Rooted<JSAtom*> validatedTimeZone(cx);
+ Rooted<JSAtom*> ianaTimeZone(cx);
+ for (; !iter.done(); iter.next()) {
+ validatedTimeZone = iter.get();
+
+ // Canonicalize the time zone before adding it to the result array.
+
+ // Some time zone names are canonicalized differently by ICU -- handle
+ // those first.
+ ianaTimeZone.set(nullptr);
+ if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(
+ cx, validatedTimeZone, &ianaTimeZone)) {
+ return nullptr;
+ }
+
+ JSLinearString* timeZone;
+ if (ianaTimeZone) {
+ cx->markAtom(ianaTimeZone);
+
+ timeZone = ianaTimeZone;
+ } else {
+ // Call into ICU to canonicalize the time zone.
+
+ JS::AutoStableStringChars stableChars(cx);
+ if (!stableChars.initTwoByte(cx, validatedTimeZone)) {
+ return nullptr;
+ }
+
+ intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE>
+ canonicalTimeZone(cx);
+ auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID(
+ stableChars.twoByteRange(), canonicalTimeZone);
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+
+ timeZone = canonicalTimeZone.toString(cx);
+ if (!timeZone) {
+ return nullptr;
+ }
+
+ // Canonicalize both to "UTC" per CanonicalizeTimeZoneName().
+ if (StringEqualsLiteral(timeZone, "Etc/UTC") ||
+ StringEqualsLiteral(timeZone, "Etc/GMT")) {
+ timeZone = cx->names().UTC;
+ }
+ }
+
+ if (!timeZones.append(timeZone)) {
+ return nullptr;
+ }
+ }
+
+ return CreateArrayFromList(cx, &timeZones);
+}
+
+template <size_t N>
+constexpr auto MeasurementUnitNames(
+ const mozilla::intl::SimpleMeasureUnit (&units)[N]) {
+ std::array<const char*, N> array = {};
+ for (size_t i = 0; i < N; ++i) {
+ array[i] = units[i].name;
+ }
+ return array;
+}
+
+/**
+ * AvailableUnits ( )
+ */
+static ArrayObject* AvailableUnits(JSContext* cx) {
+ static constexpr auto simpleMeasureUnitNames =
+ MeasurementUnitNames(mozilla::intl::simpleMeasureUnits);
+
+ return CreateArrayFromSortedList(cx, simpleMeasureUnitNames);
+}
+
+bool js::intl_SupportedValuesOf(JSContext* cx, unsigned argc, JS::Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ JSLinearString* key = args[0].toString()->ensureLinear(cx);
+ if (!key) {
+ return false;
+ }
+
+ ArrayObject* list;
+ if (StringEqualsLiteral(key, "calendar")) {
+ list = AvailableCalendars(cx);
+ } else if (StringEqualsLiteral(key, "collation")) {
+ list = AvailableCollations(cx);
+ } else if (StringEqualsLiteral(key, "currency")) {
+ list = AvailableCurrencies(cx);
+ } else if (StringEqualsLiteral(key, "numberingSystem")) {
+ list = AvailableNumberingSystems(cx);
+ } else if (StringEqualsLiteral(key, "timeZone")) {
+ list = AvailableTimeZones(cx);
+ } else if (StringEqualsLiteral(key, "unit")) {
+ list = AvailableUnits(cx);
+ } else {
+ ReportBadKey(cx, key);
+ return false;
+ }
+ if (!list) {
+ return false;
+ }
+
+ args.rval().setObject(*list);
+ return true;
+}
+
+static bool intl_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().Intl);
+ return true;
+}
+
+static const JSFunctionSpec intl_static_methods[] = {
+ JS_FN(js_toSource_str, intl_toSource, 0, 0),
+ JS_SELF_HOSTED_FN("getCanonicalLocales", "Intl_getCanonicalLocales", 1, 0),
+ JS_SELF_HOSTED_FN("supportedValuesOf", "Intl_supportedValuesOf", 1, 0),
+ JS_FS_END};
+
+static const JSPropertySpec intl_static_properties[] = {
+ JS_STRING_SYM_PS(toStringTag, "Intl", JSPROP_READONLY), JS_PS_END};
+
+static JSObject* CreateIntlObject(JSContext* cx, JSProtoKey key) {
+ RootedObject proto(cx, &cx->global()->getObjectPrototype());
+
+ // The |Intl| object is just a plain object with some "static" function
+ // properties and some constructor properties.
+ return NewTenuredObjectWithGivenProto(cx, &IntlClass, proto);
+}
+
+/**
+ * Initializes the Intl Object and its standard built-in properties.
+ * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1
+ */
+static bool IntlClassFinish(JSContext* cx, HandleObject intl,
+ HandleObject proto) {
+ // Add the constructor properties.
+ RootedId ctorId(cx);
+ RootedValue ctorValue(cx);
+ for (const auto& protoKey :
+ {JSProto_Collator, JSProto_DateTimeFormat, JSProto_DisplayNames,
+ JSProto_ListFormat, JSProto_Locale, JSProto_NumberFormat,
+ JSProto_PluralRules, JSProto_RelativeTimeFormat}) {
+ JSObject* ctor = GlobalObject::getOrCreateConstructor(cx, protoKey);
+ if (!ctor) {
+ return false;
+ }
+
+ ctorId = NameToId(ClassName(protoKey, cx));
+ ctorValue.setObject(*ctor);
+ if (!DefineDataProperty(cx, intl, ctorId, ctorValue, 0)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static const ClassSpec IntlClassSpec = {
+ CreateIntlObject, nullptr, intl_static_methods, intl_static_properties,
+ nullptr, nullptr, IntlClassFinish};
+
+const JSClass js::IntlClass = {"Intl", JSCLASS_HAS_CACHED_PROTO(JSProto_Intl),
+ JS_NULL_CLASS_OPS, &IntlClassSpec};
diff --git a/js/src/builtin/intl/IntlObject.h b/js/src/builtin/intl/IntlObject.h
new file mode 100644
index 0000000000..5b79f74e92
--- /dev/null
+++ b/js/src/builtin/intl/IntlObject.h
@@ -0,0 +1,82 @@
+/* -*- 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_IntlObject_h
+#define builtin_intl_IntlObject_h
+
+#include "js/TypeDecls.h"
+
+namespace js {
+
+extern const JSClass IntlClass;
+
+/**
+ * Returns a plain object with calendar information for a single valid locale
+ * (callers must perform this validation). The object will have these
+ * properties:
+ *
+ * firstDayOfWeek
+ * an integer in the range 1=Monday to 7=Sunday indicating the day
+ * considered the first day of the week in calendars, e.g. 7 for en-US,
+ * 1 for en-GB, 7 for bn-IN
+ * minDays
+ * an integer in the range of 1 to 7 indicating the minimum number
+ * of days required in the first week of the year, e.g. 1 for en-US,
+ * 4 for de
+ * weekend
+ * an array with values in the range 1=Monday to 7=Sunday indicating the
+ * days of the week considered as part of the weekend, e.g. [6, 7] for en-US
+ * and en-GB, [7] for bn-IN (note that "weekend" is *not* necessarily two
+ * days)
+ *
+ * NOTE: "calendar" and "locale" properties are *not* added to the object.
+ */
+[[nodiscard]] extern bool intl_GetCalendarInfo(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Compares a BCP 47 language tag against the locales in availableLocales and
+ * returns the best available match -- or |undefined| if no match was found.
+ * Uses the fallback mechanism of RFC 4647, section 3.4.
+ *
+ * The set of available locales consulted doesn't necessarily include the
+ * default locale or any generalized forms of it (e.g. "de" is a more-general
+ * form of "de-CH"). If you want to be sure to consider the default local and
+ * its generalized forms (you usually will), pass the default locale as the
+ * value of |defaultOrNull|; otherwise pass null.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.2.
+ * Spec: RFC 4647, section 3.4.
+ *
+ * Usage: result = intl_BestAvailableLocale("Collator", locale, defaultOrNull)
+ */
+[[nodiscard]] extern bool intl_BestAvailableLocale(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Return the supported locale for the input locale if ICU supports that locale
+ * (perhaps via fallback, e.g. supporting "de-CH" through "de" support implied
+ * by a "de-DE" locale). Otherwise uses the last-ditch locale.
+ *
+ * Usage: result = intl_supportedLocaleOrFallback(locale)
+ */
+[[nodiscard]] extern bool intl_supportedLocaleOrFallback(JSContext* cx,
+ unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns the list of supported values for the given key. Throws a RangeError
+ * if the key isn't one of {"calendar", "collation", "currency",
+ * "numberingSystem", "timeZone", "unit"}.
+ *
+ * Usage: list = intl_SupportedValuesOf(key)
+ */
+[[nodiscard]] extern bool intl_SupportedValuesOf(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+} // namespace js
+
+#endif /* builtin_intl_IntlObject_h */
diff --git a/js/src/builtin/intl/IntlObject.js b/js/src/builtin/intl/IntlObject.js
new file mode 100644
index 0000000000..95b158bb27
--- /dev/null
+++ b/js/src/builtin/intl/IntlObject.js
@@ -0,0 +1,81 @@
+/* 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/. */
+
+/**
+ * 8.2.1 Intl.getCanonicalLocales ( locales )
+ *
+ * ES2017 Intl draft rev 947aa9a0c853422824a0c9510d8f09be3eb416b9
+ */
+function Intl_getCanonicalLocales(locales) {
+ // Steps 1-2.
+ return CanonicalizeLocaleList(locales);
+}
+
+/**
+ * Intl.supportedValuesOf ( key )
+ */
+function Intl_supportedValuesOf(key) {
+ // Step 1.
+ key = ToString(key);
+
+ // Steps 2-9.
+ return intl_SupportedValuesOf(key);
+}
+
+/**
+ * This function is a custom function in the style of the standard Intl.*
+ * functions, that isn't part of any spec or proposal yet.
+ *
+ * Returns an object with the following properties:
+ * locale:
+ * The actual resolved locale.
+ *
+ * calendar:
+ * The default calendar of the resolved locale.
+ *
+ * firstDayOfWeek:
+ * The first day of the week for the resolved locale.
+ *
+ * minDays:
+ * The minimum number of days in a week for the resolved locale.
+ *
+ * weekend:
+ * The days of the week considered as the weekend for the resolved locale.
+ *
+ * Days are encoded as integers in the range 1=Monday to 7=Sunday.
+ */
+function Intl_getCalendarInfo(locales) {
+ // 1. Let requestLocales be ? CanonicalizeLocaleList(locales).
+ const requestedLocales = CanonicalizeLocaleList(locales);
+
+ const DateTimeFormat = dateTimeFormatInternalProperties;
+
+ // 2. Let localeData be %DateTimeFormat%.[[localeData]].
+ const localeData = DateTimeFormat.localeData;
+
+ // 3. Let localeOpt be a new Record.
+ const localeOpt = new_Record();
+
+ // 4. Set localeOpt.[[localeMatcher]] to "best fit".
+ localeOpt.localeMatcher = "best fit";
+
+ // 5. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]],
+ // requestedLocales, localeOpt,
+ // %DateTimeFormat%.[[relevantExtensionKeys]], localeData).
+ const r = ResolveLocale(
+ "DateTimeFormat",
+ requestedLocales,
+ localeOpt,
+ DateTimeFormat.relevantExtensionKeys,
+ localeData
+ );
+
+ // 6. Let result be GetCalendarInfo(r.[[locale]]).
+ const result = intl_GetCalendarInfo(r.locale);
+ DefineDataProperty(result, "calendar", r.ca);
+ DefineDataProperty(result, "locale", r.locale);
+
+ // 7. Return result.
+ return result;
+}
diff --git a/js/src/builtin/intl/LanguageTag.cpp b/js/src/builtin/intl/LanguageTag.cpp
new file mode 100644
index 0000000000..3372f5d99a
--- /dev/null
+++ b/js/src/builtin/intl/LanguageTag.cpp
@@ -0,0 +1,193 @@
+/* -*- 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/. */
+
+#include "builtin/intl/LanguageTag.h"
+
+#include "mozilla/intl/Locale.h"
+#include "mozilla/Span.h"
+
+#include "builtin/intl/StringAsciiChars.h"
+#include "gc/Tracer.h"
+#include "vm/JSContext.h"
+
+namespace js {
+namespace intl {
+
+[[nodiscard]] bool ParseLocale(JSContext* cx, Handle<JSLinearString*> str,
+ mozilla::intl::Locale& result) {
+ if (StringIsAscii(str)) {
+ intl::StringAsciiChars chars(str);
+ if (!chars.init(cx)) {
+ return false;
+ }
+
+ if (mozilla::intl::LocaleParser::TryParse(chars, result).isOk()) {
+ return true;
+ }
+ }
+
+ if (UniqueChars localeChars = QuoteString(cx, str, '"')) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INVALID_LANGUAGE_TAG, localeChars.get());
+ }
+ return false;
+}
+
+bool ParseStandaloneLanguageTag(Handle<JSLinearString*> str,
+ mozilla::intl::LanguageSubtag& result) {
+ // Tell the analysis the |IsStructurallyValidLanguageTag| function can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ if (str->hasLatin1Chars()) {
+ if (!mozilla::intl::IsStructurallyValidLanguageTag<Latin1Char>(
+ str->latin1Range(nogc))) {
+ return false;
+ }
+ result.Set<Latin1Char>(str->latin1Range(nogc));
+ } else {
+ if (!mozilla::intl::IsStructurallyValidLanguageTag<char16_t>(
+ str->twoByteRange(nogc))) {
+ return false;
+ }
+ result.Set<char16_t>(str->twoByteRange(nogc));
+ }
+ return true;
+}
+
+bool ParseStandaloneScriptTag(Handle<JSLinearString*> str,
+ mozilla::intl::ScriptSubtag& result) {
+ // Tell the analysis the |IsStructurallyValidScriptTag| function can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ if (str->hasLatin1Chars()) {
+ if (!mozilla::intl::IsStructurallyValidScriptTag<Latin1Char>(
+ str->latin1Range(nogc))) {
+ return false;
+ }
+ result.Set<Latin1Char>(str->latin1Range(nogc));
+ } else {
+ if (!mozilla::intl::IsStructurallyValidScriptTag<char16_t>(
+ str->twoByteRange(nogc))) {
+ return false;
+ }
+ result.Set<char16_t>(str->twoByteRange(nogc));
+ }
+ return true;
+}
+
+bool ParseStandaloneRegionTag(Handle<JSLinearString*> str,
+ mozilla::intl::RegionSubtag& result) {
+ // Tell the analysis the |IsStructurallyValidRegionTag| function can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ if (str->hasLatin1Chars()) {
+ if (!mozilla::intl::IsStructurallyValidRegionTag<Latin1Char>(
+ str->latin1Range(nogc))) {
+ return false;
+ }
+ result.Set<Latin1Char>(str->latin1Range(nogc));
+ } else {
+ if (!mozilla::intl::IsStructurallyValidRegionTag<char16_t>(
+ str->twoByteRange(nogc))) {
+ return false;
+ }
+ result.Set<char16_t>(str->twoByteRange(nogc));
+ }
+ return true;
+}
+
+template <typename CharT>
+static bool IsAsciiLowercaseAlpha(mozilla::Span<const CharT> span) {
+ // Tell the analysis the |std::all_of| function can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ const CharT* ptr = span.data();
+ size_t length = span.size();
+ return std::all_of(ptr, ptr + length, mozilla::IsAsciiLowercaseAlpha<CharT>);
+}
+
+static bool IsAsciiLowercaseAlpha(JSLinearString* str) {
+ JS::AutoCheckCannotGC nogc;
+ if (str->hasLatin1Chars()) {
+ return IsAsciiLowercaseAlpha<Latin1Char>(str->latin1Range(nogc));
+ }
+ return IsAsciiLowercaseAlpha<char16_t>(str->twoByteRange(nogc));
+}
+
+template <typename CharT>
+static bool IsAsciiAlpha(mozilla::Span<const CharT> span) {
+ // Tell the analysis the |std::all_of| function can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ const CharT* ptr = span.data();
+ size_t length = span.size();
+ return std::all_of(ptr, ptr + length, mozilla::IsAsciiAlpha<CharT>);
+}
+
+static bool IsAsciiAlpha(JSLinearString* str) {
+ JS::AutoCheckCannotGC nogc;
+ if (str->hasLatin1Chars()) {
+ return IsAsciiAlpha<Latin1Char>(str->latin1Range(nogc));
+ }
+ return IsAsciiAlpha<char16_t>(str->twoByteRange(nogc));
+}
+
+JS::Result<JSString*> ParseStandaloneISO639LanguageTag(
+ JSContext* cx, Handle<JSLinearString*> str) {
+ // ISO-639 language codes contain either two or three characters.
+ size_t length = str->length();
+ if (length != 2 && length != 3) {
+ return nullptr;
+ }
+
+ // We can directly the return the input below if it's in the correct case.
+ bool isLowerCase = IsAsciiLowercaseAlpha(str);
+ if (!isLowerCase) {
+ // Must be an ASCII alpha string.
+ if (!IsAsciiAlpha(str)) {
+ return nullptr;
+ }
+ }
+
+ mozilla::intl::LanguageSubtag languageTag;
+ if (str->hasLatin1Chars()) {
+ JS::AutoCheckCannotGC nogc;
+ languageTag.Set<Latin1Char>(str->latin1Range(nogc));
+ } else {
+ JS::AutoCheckCannotGC nogc;
+ languageTag.Set<char16_t>(str->twoByteRange(nogc));
+ }
+
+ if (!isLowerCase) {
+ // The language subtag is canonicalized to lower case.
+ languageTag.ToLowerCase();
+ }
+
+ // Reject the input if the canonical tag contains more than just a single
+ // language subtag.
+ if (mozilla::intl::Locale::ComplexLanguageMapping(languageTag)) {
+ return nullptr;
+ }
+
+ // Take care to replace deprecated subtags with their preferred values.
+ JSString* result;
+ if (mozilla::intl::Locale::LanguageMapping(languageTag) || !isLowerCase) {
+ result = NewStringCopy<CanGC>(cx, languageTag.Span());
+ } else {
+ result = str;
+ }
+ if (!result) {
+ return cx->alreadyReportedOOM();
+ }
+ return result;
+}
+
+void js::intl::UnicodeExtensionKeyword::trace(JSTracer* trc) {
+ TraceRoot(trc, &type_, "UnicodeExtensionKeyword::type");
+}
+
+} // namespace intl
+} // namespace js
diff --git a/js/src/builtin/intl/LanguageTag.h b/js/src/builtin/intl/LanguageTag.h
new file mode 100644
index 0000000000..e896411e19
--- /dev/null
+++ b/js/src/builtin/intl/LanguageTag.h
@@ -0,0 +1,91 @@
+/* -*- 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/. */
+
+/* Structured representation of Unicode locale IDs used with Intl functions. */
+
+#ifndef builtin_intl_LanguageTag_h
+#define builtin_intl_LanguageTag_h
+
+#include "mozilla/intl/Locale.h"
+#include "mozilla/Span.h"
+
+#include "js/Result.h"
+#include "js/RootingAPI.h"
+
+struct JS_PUBLIC_API JSContext;
+class JSLinearString;
+class JS_PUBLIC_API JSString;
+class JS_PUBLIC_API JSTracer;
+
+namespace js {
+
+namespace intl {
+
+/**
+ * Parse a string Unicode BCP 47 locale identifier. If successful, store in
+ * |result| and return true. Otherwise return false.
+ */
+[[nodiscard]] bool ParseLocale(JSContext* cx, JS::Handle<JSLinearString*> str,
+ mozilla::intl::Locale& result);
+
+/**
+ * Parse a string as a standalone |language| tag. If |str| is a standalone
+ * language tag, store it in |result| and return true. Otherwise return false.
+ */
+[[nodiscard]] bool ParseStandaloneLanguageTag(
+ JS::Handle<JSLinearString*> str, mozilla::intl::LanguageSubtag& result);
+
+/**
+ * Parse a string as a standalone |script| tag. If |str| is a standalone script
+ * tag, store it in |result| and return true. Otherwise return false.
+ */
+[[nodiscard]] bool ParseStandaloneScriptTag(
+ JS::Handle<JSLinearString*> str, mozilla::intl::ScriptSubtag& result);
+
+/**
+ * Parse a string as a standalone |region| tag. If |str| is a standalone region
+ * tag, store it in |result| and return true. Otherwise return false.
+ */
+[[nodiscard]] bool ParseStandaloneRegionTag(
+ JS::Handle<JSLinearString*> str, mozilla::intl::RegionSubtag& result);
+
+/**
+ * Parse a string as an ISO-639 language code. Return |nullptr| in the result if
+ * the input could not be parsed or the canonical form of the resulting language
+ * tag contains more than a single language subtag.
+ */
+JS::Result<JSString*> ParseStandaloneISO639LanguageTag(
+ JSContext* cx, JS::Handle<JSLinearString*> str);
+
+class UnicodeExtensionKeyword final {
+ char key_[mozilla::intl::LanguageTagLimits::UnicodeKeyLength];
+ JSLinearString* type_;
+
+ public:
+ using UnicodeKey =
+ const char (&)[mozilla::intl::LanguageTagLimits::UnicodeKeyLength + 1];
+ using UnicodeKeySpan =
+ mozilla::Span<const char,
+ mozilla::intl::LanguageTagLimits::UnicodeKeyLength>;
+
+ UnicodeExtensionKeyword(UnicodeKey key, JSLinearString* type)
+ : key_{key[0], key[1]}, type_(type) {}
+
+ UnicodeKeySpan key() const { return {key_, sizeof(key_)}; }
+ JSLinearString* type() const { return type_; }
+
+ void trace(JSTracer* trc);
+};
+
+[[nodiscard]] extern bool ApplyUnicodeExtensionToTag(
+ JSContext* cx, mozilla::intl::Locale& tag,
+ JS::HandleVector<UnicodeExtensionKeyword> keywords);
+
+} // namespace intl
+
+} // namespace js
+
+#endif /* builtin_intl_LanguageTag_h */
diff --git a/js/src/builtin/intl/ListFormat.cpp b/js/src/builtin/intl/ListFormat.cpp
new file mode 100644
index 0000000000..7d11d5acd6
--- /dev/null
+++ b/js/src/builtin/intl/ListFormat.cpp
@@ -0,0 +1,373 @@
+/* -*- 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/. */
+
+#include "builtin/intl/ListFormat.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/ListFormat.h"
+
+#include <stddef.h>
+
+#include "builtin/Array.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/FormatBuffer.h"
+#include "gc/GCContext.h"
+#include "js/Utility.h"
+#include "js/Vector.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/StringType.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+#include "vm/ObjectOperations-inl.h"
+
+using namespace js;
+
+const JSClassOps ListFormatObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ ListFormatObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ nullptr, // trace
+};
+const JSClass ListFormatObject::class_ = {
+ "Intl.ListFormat",
+ JSCLASS_HAS_RESERVED_SLOTS(ListFormatObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_ListFormat) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &ListFormatObject::classOps_, &ListFormatObject::classSpec_};
+
+const JSClass& ListFormatObject::protoClass_ = PlainObject::class_;
+
+static bool listFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().ListFormat);
+ return true;
+}
+
+static const JSFunctionSpec listFormat_static_methods[] = {
+ JS_SELF_HOSTED_FN("supportedLocalesOf",
+ "Intl_ListFormat_supportedLocalesOf", 1, 0),
+ JS_FS_END};
+
+static const JSFunctionSpec listFormat_methods[] = {
+ JS_SELF_HOSTED_FN("resolvedOptions", "Intl_ListFormat_resolvedOptions", 0,
+ 0),
+ JS_SELF_HOSTED_FN("format", "Intl_ListFormat_format", 1, 0),
+ JS_SELF_HOSTED_FN("formatToParts", "Intl_ListFormat_formatToParts", 1, 0),
+ JS_FN(js_toSource_str, listFormat_toSource, 0, 0), JS_FS_END};
+
+static const JSPropertySpec listFormat_properties[] = {
+ JS_STRING_SYM_PS(toStringTag, "Intl.ListFormat", JSPROP_READONLY),
+ JS_PS_END};
+
+static bool ListFormat(JSContext* cx, unsigned argc, Value* vp);
+
+const ClassSpec ListFormatObject::classSpec_ = {
+ GenericCreateConstructor<ListFormat, 0, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<ListFormatObject>,
+ listFormat_static_methods,
+ nullptr,
+ listFormat_methods,
+ listFormat_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+/**
+ * Intl.ListFormat([ locales [, options]])
+ */
+static bool ListFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // Step 1.
+ if (!ThrowIfNotConstructing(cx, args, "Intl.ListFormat")) {
+ return false;
+ }
+
+ // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+ RootedObject proto(cx);
+ if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ListFormat,
+ &proto)) {
+ return false;
+ }
+
+ Rooted<ListFormatObject*> listFormat(
+ cx, NewObjectWithClassProto<ListFormatObject>(cx, proto));
+ if (!listFormat) {
+ return false;
+ }
+
+ HandleValue locales = args.get(0);
+ HandleValue options = args.get(1);
+
+ // Step 3.
+ if (!intl::InitializeObject(cx, listFormat, cx->names().InitializeListFormat,
+ locales, options)) {
+ return false;
+ }
+
+ args.rval().setObject(*listFormat);
+ return true;
+}
+
+void js::ListFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ MOZ_ASSERT(gcx->onMainThread());
+
+ mozilla::intl::ListFormat* lf =
+ obj->as<ListFormatObject>().getListFormatSlot();
+ if (lf) {
+ intl::RemoveICUCellMemory(gcx, obj, ListFormatObject::EstimatedMemoryUse);
+ delete lf;
+ }
+}
+
+/**
+ * Returns a new ListFormat with the locale and list formatting options
+ * of the given ListFormat.
+ */
+static mozilla::intl::ListFormat* NewListFormat(
+ JSContext* cx, Handle<ListFormatObject*> listFormat) {
+ RootedObject internals(cx, intl::GetInternalsObject(cx, listFormat));
+ if (!internals) {
+ return nullptr;
+ }
+
+ RootedValue value(cx);
+
+ if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
+ return nullptr;
+ }
+ UniqueChars locale = intl::EncodeLocale(cx, value.toString());
+ if (!locale) {
+ return nullptr;
+ }
+
+ mozilla::intl::ListFormat::Options options;
+
+ using ListFormatType = mozilla::intl::ListFormat::Type;
+ if (!GetProperty(cx, internals, internals, cx->names().type, &value)) {
+ return nullptr;
+ }
+ {
+ JSLinearString* strType = value.toString()->ensureLinear(cx);
+ if (!strType) {
+ return nullptr;
+ }
+
+ if (StringEqualsLiteral(strType, "conjunction")) {
+ options.mType = ListFormatType::Conjunction;
+ } else if (StringEqualsLiteral(strType, "disjunction")) {
+ options.mType = ListFormatType::Disjunction;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(strType, "unit"));
+ options.mType = ListFormatType::Unit;
+ }
+ }
+
+ using ListFormatStyle = mozilla::intl::ListFormat::Style;
+ if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
+ return nullptr;
+ }
+ {
+ JSLinearString* strStyle = value.toString()->ensureLinear(cx);
+ if (!strStyle) {
+ return nullptr;
+ }
+
+ if (StringEqualsLiteral(strStyle, "long")) {
+ options.mStyle = ListFormatStyle::Long;
+ } else if (StringEqualsLiteral(strStyle, "short")) {
+ options.mStyle = ListFormatStyle::Short;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(strStyle, "narrow"));
+ options.mStyle = ListFormatStyle::Narrow;
+ }
+ }
+
+ auto result = mozilla::intl::ListFormat::TryCreate(
+ mozilla::MakeStringSpan(locale.get()), options);
+
+ if (result.isOk()) {
+ return result.unwrap().release();
+ }
+
+ js::intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+}
+
+static mozilla::intl::ListFormat* GetOrCreateListFormat(
+ JSContext* cx, Handle<ListFormatObject*> listFormat) {
+ // Obtain a cached mozilla::intl::ListFormat object.
+ mozilla::intl::ListFormat* lf = listFormat->getListFormatSlot();
+ if (lf) {
+ return lf;
+ }
+
+ lf = NewListFormat(cx, listFormat);
+ if (!lf) {
+ return nullptr;
+ }
+ listFormat->setListFormatSlot(lf);
+
+ intl::AddICUCellMemory(listFormat, ListFormatObject::EstimatedMemoryUse);
+ return lf;
+}
+
+/**
+ * FormatList ( listFormat, list )
+ */
+static bool FormatList(JSContext* cx, mozilla::intl::ListFormat* lf,
+ const mozilla::intl::ListFormat::StringList& list,
+ MutableHandleValue result) {
+ intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> formatBuffer(cx);
+ auto formatResult = lf->Format(list, formatBuffer);
+ if (formatResult.isErr()) {
+ js::intl::ReportInternalError(cx, formatResult.unwrapErr());
+ return false;
+ }
+
+ JSString* str = formatBuffer.toString(cx);
+ if (!str) {
+ return false;
+ }
+ result.setString(str);
+ return true;
+}
+
+/**
+ * FormatListToParts ( listFormat, list )
+ */
+static bool FormatListToParts(JSContext* cx, mozilla::intl::ListFormat* lf,
+ const mozilla::intl::ListFormat::StringList& list,
+ MutableHandleValue result) {
+ intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ mozilla::intl::ListFormat::PartVector parts;
+ auto formatResult = lf->FormatToParts(list, buffer, parts);
+ if (formatResult.isErr()) {
+ intl::ReportInternalError(cx, formatResult.unwrapErr());
+ return false;
+ }
+
+ RootedString overallResult(cx, buffer.toString(cx));
+ if (!overallResult) {
+ return false;
+ }
+
+ Rooted<ArrayObject*> partsArray(
+ cx, NewDenseFullyAllocatedArray(cx, parts.length()));
+ if (!partsArray) {
+ return false;
+ }
+ partsArray->ensureDenseInitializedLength(0, parts.length());
+
+ RootedObject singlePart(cx);
+ RootedValue val(cx);
+
+ size_t index = 0;
+ size_t beginIndex = 0;
+ for (const mozilla::intl::ListFormat::Part& part : parts) {
+ singlePart = NewPlainObject(cx);
+ if (!singlePart) {
+ return false;
+ }
+
+ if (part.first == mozilla::intl::ListFormat::PartType::Element) {
+ val = StringValue(cx->names().element);
+ } else {
+ val = StringValue(cx->names().literal);
+ }
+
+ if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) {
+ return false;
+ }
+
+ // There could be an empty string so the endIndex coule be equal to
+ // beginIndex.
+ MOZ_ASSERT(part.second >= beginIndex);
+ JSLinearString* partStr = NewDependentString(cx, overallResult, beginIndex,
+ part.second - beginIndex);
+ if (!partStr) {
+ return false;
+ }
+ val = StringValue(partStr);
+ if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) {
+ return false;
+ }
+
+ beginIndex = part.second;
+ partsArray->initDenseElement(index++, ObjectValue(*singlePart));
+ }
+
+ MOZ_ASSERT(index == parts.length());
+ MOZ_ASSERT(beginIndex == buffer.length());
+ result.setObject(*partsArray);
+
+ return true;
+}
+
+bool js::intl_FormatList(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+
+ Rooted<ListFormatObject*> listFormat(
+ cx, &args[0].toObject().as<ListFormatObject>());
+
+ bool formatToParts = args[2].toBoolean();
+
+ mozilla::intl::ListFormat* lf = GetOrCreateListFormat(cx, listFormat);
+ if (!lf) {
+ return false;
+ }
+
+ // Collect all strings and their lengths.
+ //
+ // 'strings' takes the ownership of those strings, and 'list' will be passed
+ // to mozilla::intl::ListFormat as a Span.
+ Vector<UniqueTwoByteChars, mozilla::intl::DEFAULT_LIST_LENGTH> strings(cx);
+ mozilla::intl::ListFormat::StringList list;
+
+ Rooted<ArrayObject*> listObj(cx, &args[1].toObject().as<ArrayObject>());
+ RootedValue value(cx);
+ uint32_t listLen = listObj->length();
+ for (uint32_t i = 0; i < listLen; i++) {
+ if (!GetElement(cx, listObj, listObj, i, &value)) {
+ return false;
+ }
+
+ JSLinearString* linear = value.toString()->ensureLinear(cx);
+ if (!linear) {
+ return false;
+ }
+
+ size_t linearLength = linear->length();
+
+ UniqueTwoByteChars chars = cx->make_pod_array<char16_t>(linearLength);
+ if (!chars) {
+ return false;
+ }
+ CopyChars(chars.get(), *linear);
+
+ if (!strings.append(std::move(chars))) {
+ return false;
+ }
+
+ if (!list.emplaceBack(strings[i].get(), linearLength)) {
+ return false;
+ }
+ }
+
+ if (formatToParts) {
+ return FormatListToParts(cx, lf, list, args.rval());
+ }
+ return FormatList(cx, lf, list, args.rval());
+}
diff --git a/js/src/builtin/intl/ListFormat.h b/js/src/builtin/intl/ListFormat.h
new file mode 100644
index 0000000000..da0daa711b
--- /dev/null
+++ b/js/src/builtin/intl/ListFormat.h
@@ -0,0 +1,69 @@
+/* -*- 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_ListFormat_h
+#define builtin_intl_ListFormat_h
+
+#include <stdint.h>
+
+#include "builtin/SelfHostingDefines.h"
+#include "js/Class.h"
+#include "js/TypeDecls.h"
+#include "vm/NativeObject.h"
+
+namespace mozilla::intl {
+class ListFormat;
+} // namespace mozilla::intl
+
+namespace js {
+
+class ListFormatObject : public NativeObject {
+ public:
+ static const JSClass class_;
+ static const JSClass& protoClass_;
+
+ static constexpr uint32_t INTERNALS_SLOT = 0;
+ static constexpr uint32_t LIST_FORMAT_SLOT = 1;
+ static constexpr uint32_t SLOT_COUNT = 2;
+
+ static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT,
+ "INTERNALS_SLOT must match self-hosting define for internals "
+ "object slot");
+
+ // Estimated memory use for UListFormatter (see IcuMemoryUsage).
+ static constexpr size_t EstimatedMemoryUse = 24;
+
+ mozilla::intl::ListFormat* getListFormatSlot() const {
+ const auto& slot = getFixedSlot(LIST_FORMAT_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::ListFormat*>(slot.toPrivate());
+ }
+
+ void setListFormatSlot(mozilla::intl::ListFormat* format) {
+ setFixedSlot(LIST_FORMAT_SLOT, PrivateValue(format));
+ }
+
+ private:
+ static const JSClassOps classOps_;
+ static const ClassSpec classSpec_;
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+};
+
+/**
+ * Returns a string representing the array of string values |list| according to
+ * the effective locale and the formatting options of the given ListFormat.
+ *
+ * Usage: formatted = intl_FormatList(listFormat, list, formatToParts)
+ */
+[[nodiscard]] extern bool intl_FormatList(JSContext* cx, unsigned argc,
+ Value* vp);
+
+} // namespace js
+
+#endif /* builtin_intl_ListFormat_h */
diff --git a/js/src/builtin/intl/ListFormat.js b/js/src/builtin/intl/ListFormat.js
new file mode 100644
index 0000000000..59ac29b944
--- /dev/null
+++ b/js/src/builtin/intl/ListFormat.js
@@ -0,0 +1,330 @@
+/* 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/. */
+
+/**
+ * ListFormat internal properties.
+ */
+function listFormatLocaleData() {
+ // ListFormat don't support any extension keys.
+ return {};
+}
+var listFormatInternalProperties = {
+ localeData: listFormatLocaleData,
+ relevantExtensionKeys: [],
+};
+
+/**
+ * Intl.ListFormat ( [ locales [ , options ] ] )
+ *
+ * Compute an internal properties object from |lazyListFormatData|.
+ */
+function resolveListFormatInternals(lazyListFormatData) {
+ assert(IsObject(lazyListFormatData), "lazy data not an object?");
+
+ var internalProps = std_Object_create(null);
+
+ var ListFormat = listFormatInternalProperties;
+
+ // Compute effective locale.
+
+ // Step 9.
+ var localeData = ListFormat.localeData;
+
+ // Step 10.
+ var r = ResolveLocale(
+ "ListFormat",
+ lazyListFormatData.requestedLocales,
+ lazyListFormatData.opt,
+ ListFormat.relevantExtensionKeys,
+ localeData
+ );
+
+ // Step 11.
+ internalProps.locale = r.locale;
+
+ // Step 13.
+ internalProps.type = lazyListFormatData.type;
+
+ // Step 15.
+ internalProps.style = lazyListFormatData.style;
+
+ // Steps 16-23 (not applicable in our implementation).
+
+ // The caller is responsible for associating |internalProps| with the right
+ // object using |setInternalProperties|.
+ return internalProps;
+}
+
+/**
+ * Returns an object containing the ListFormat internal properties of |obj|.
+ */
+function getListFormatInternals(obj) {
+ assert(IsObject(obj), "getListFormatInternals called with non-object");
+ assert(
+ intl_GuardToListFormat(obj) !== null,
+ "getListFormatInternals called with non-ListFormat"
+ );
+
+ var internals = getIntlObjectInternals(obj);
+ assert(
+ internals.type === "ListFormat",
+ "bad type escaped getIntlObjectInternals"
+ );
+
+ // If internal properties have already been computed, use them.
+ var internalProps = maybeInternalProperties(internals);
+ if (internalProps) {
+ return internalProps;
+ }
+
+ // Otherwise it's time to fully create them.
+ internalProps = resolveListFormatInternals(internals.lazyData);
+ setInternalProperties(internals, internalProps);
+ return internalProps;
+}
+
+/**
+ * Intl.ListFormat ( [ locales [ , options ] ] )
+ *
+ * Initializes an object as a ListFormat.
+ *
+ * This method is complicated a moderate bit by its implementing initialization
+ * as a *lazy* concept. Everything that must happen now, does -- but we defer
+ * all the work we can until the object is actually used as a ListFormat.
+ * This later work occurs in |resolveListFormatInternals|; steps not noted
+ * here occur there.
+ */
+function InitializeListFormat(listFormat, locales, options) {
+ assert(IsObject(listFormat), "InitializeListFormat called with non-object");
+ assert(
+ intl_GuardToListFormat(listFormat) !== null,
+ "InitializeListFormat called with non-ListFormat"
+ );
+
+ // Lazy ListFormat data has the following structure:
+ //
+ // {
+ // requestedLocales: List of locales,
+ // type: "conjunction" / "disjunction" / "unit",
+ // style: "long" / "short" / "narrow",
+ //
+ // opt: // opt object computed in InitializeListFormat
+ // {
+ // localeMatcher: "lookup" / "best fit",
+ // }
+ // }
+ //
+ // Note that lazy data is only installed as a final step of initialization,
+ // so every ListFormat lazy data object has *all* these properties, never a
+ // subset of them.
+ var lazyListFormatData = std_Object_create(null);
+
+ // Step 3.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+ lazyListFormatData.requestedLocales = requestedLocales;
+
+ // Steps 4-5.
+ if (options === undefined) {
+ options = std_Object_create(null);
+ } else if (!IsObject(options)) {
+ ThrowTypeError(
+ JSMSG_OBJECT_REQUIRED,
+ options === null ? "null" : typeof options
+ );
+ }
+
+ // Step 6.
+ var opt = new_Record();
+ lazyListFormatData.opt = opt;
+
+ // Steps 7-8.
+ let matcher = GetOption(
+ options,
+ "localeMatcher",
+ "string",
+ ["lookup", "best fit"],
+ "best fit"
+ );
+ opt.localeMatcher = matcher;
+
+ // Compute formatting options.
+
+ // Steps 12-13.
+ var type = GetOption(
+ options,
+ "type",
+ "string",
+ ["conjunction", "disjunction", "unit"],
+ "conjunction"
+ );
+ lazyListFormatData.type = type;
+
+ // Steps 14-15.
+ var style = GetOption(
+ options,
+ "style",
+ "string",
+ ["long", "short", "narrow"],
+ "long"
+ );
+ lazyListFormatData.style = style;
+
+ // We've done everything that must be done now: mark the lazy data as fully
+ // computed and install it.
+ initializeIntlObject(listFormat, "ListFormat", lazyListFormatData);
+}
+
+/**
+ * Returns the subset of the given locale list for which this locale list has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ */
+function Intl_ListFormat_supportedLocalesOf(locales /*, options*/) {
+ var options = arguments.length > 1 ? arguments[1] : undefined;
+
+ // Step 1.
+ var availableLocales = "ListFormat";
+
+ // Step 2.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+
+ // Step 3.
+ return SupportedLocales(availableLocales, requestedLocales, options);
+}
+
+/**
+ * StringListFromIterable ( iterable )
+ */
+function StringListFromIterable(iterable, methodName) {
+ // Step 1.
+ if (iterable === undefined) {
+ return [];
+ }
+
+ // Step 3.
+ var list = [];
+
+ // Steps 2, 4-5.
+ for (var element of allowContentIter(iterable)) {
+ // Step 5.b.ii.
+ if (typeof element !== "string") {
+ ThrowTypeError(
+ JSMSG_NOT_EXPECTED_TYPE,
+ methodName,
+ "string",
+ typeof element
+ );
+ }
+
+ // Step 5.b.iii.
+ DefineDataProperty(list, list.length, element);
+ }
+
+ // Step 6.
+ return list;
+}
+
+/**
+ * Intl.ListFormat.prototype.format ( list )
+ */
+function Intl_ListFormat_format(list) {
+ // Step 1.
+ var listFormat = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(listFormat) ||
+ (listFormat = intl_GuardToListFormat(listFormat)) === null
+ ) {
+ return callFunction(
+ intl_CallListFormatMethodIfWrapped,
+ this,
+ list,
+ "Intl_ListFormat_format"
+ );
+ }
+
+ // Step 4.
+ var stringList = StringListFromIterable(list, "format");
+
+ // We can directly return if |stringList| contains less than two elements.
+ if (stringList.length < 2) {
+ return stringList.length === 0 ? "" : stringList[0];
+ }
+
+ // Ensure the ListFormat internals are resolved.
+ getListFormatInternals(listFormat);
+
+ // Step 5.
+ return intl_FormatList(listFormat, stringList, /* formatToParts = */ false);
+}
+
+/**
+ * Intl.ListFormat.prototype.formatToParts ( list )
+ */
+function Intl_ListFormat_formatToParts(list) {
+ // Step 1.
+ var listFormat = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(listFormat) ||
+ (listFormat = intl_GuardToListFormat(listFormat)) === null
+ ) {
+ return callFunction(
+ intl_CallListFormatMethodIfWrapped,
+ this,
+ list,
+ "Intl_ListFormat_formatToParts"
+ );
+ }
+
+ // Step 4.
+ var stringList = StringListFromIterable(list, "formatToParts");
+
+ // We can directly return if |stringList| contains less than two elements.
+ if (stringList.length < 2) {
+ return stringList.length === 0
+ ? []
+ : [{ type: "element", value: stringList[0] }];
+ }
+
+ // Ensure the ListFormat internals are resolved.
+ getListFormatInternals(listFormat);
+
+ // Step 5.
+ return intl_FormatList(listFormat, stringList, /* formatToParts = */ true);
+}
+
+/**
+ * Returns the resolved options for a ListFormat object.
+ */
+function Intl_ListFormat_resolvedOptions() {
+ // Step 1.
+ var listFormat = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(listFormat) ||
+ (listFormat = intl_GuardToListFormat(listFormat)) === null
+ ) {
+ return callFunction(
+ intl_CallListFormatMethodIfWrapped,
+ this,
+ "Intl_ListFormat_resolvedOptions"
+ );
+ }
+
+ var internals = getListFormatInternals(listFormat);
+
+ // Steps 4-5.
+ var result = {
+ locale: internals.locale,
+ type: internals.type,
+ style: internals.style,
+ };
+
+ // Step 6.
+ return result;
+}
diff --git a/js/src/builtin/intl/Locale.cpp b/js/src/builtin/intl/Locale.cpp
new file mode 100644
index 0000000000..e5e0e7b6ae
--- /dev/null
+++ b/js/src/builtin/intl/Locale.cpp
@@ -0,0 +1,1517 @@
+/* -*- 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 "builtin/intl/Locale.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Span.h"
+#include "mozilla/TextUtils.h"
+
+#include <algorithm>
+#include <string>
+#include <string.h>
+#include <utility>
+
+#include "builtin/Boolean.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/FormatBuffer.h"
+#include "builtin/intl/LanguageTag.h"
+#include "builtin/intl/StringAsciiChars.h"
+#include "builtin/String.h"
+#include "js/Conversions.h"
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "js/TypeDecls.h"
+#include "js/Wrapper.h"
+#include "vm/Compartment.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/Printer.h"
+#include "vm/StringType.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+using namespace mozilla::intl::LanguageTagLimits;
+
+const JSClass LocaleObject::class_ = {
+ "Intl.Locale",
+ JSCLASS_HAS_RESERVED_SLOTS(LocaleObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_Locale),
+ JS_NULL_CLASS_OPS, &LocaleObject::classSpec_};
+
+const JSClass& LocaleObject::protoClass_ = PlainObject::class_;
+
+static inline bool IsLocale(HandleValue v) {
+ return v.isObject() && v.toObject().is<LocaleObject>();
+}
+
+// Return the length of the base-name subtags.
+static size_t BaseNameLength(const mozilla::intl::Locale& tag) {
+ size_t baseNameLength = tag.Language().Length();
+ if (tag.Script().Present()) {
+ baseNameLength += 1 + tag.Script().Length();
+ }
+ if (tag.Region().Present()) {
+ baseNameLength += 1 + tag.Region().Length();
+ }
+ for (const auto& variant : tag.Variants()) {
+ baseNameLength += 1 + variant.size();
+ }
+ 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::Span<const T> spanOf(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 mozilla::intl::Locale& tag) {
+ size_t index = 0;
+ for (const auto& extension : tag.Extensions()) {
+ MOZ_ASSERT(!mozilla::IsAsciiUppercaseAlpha(extension[0]),
+ "extensions are case normalized to lowercase");
+
+ size_t extensionLength = extension.size();
+ 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 LocaleObject* CreateLocaleObject(JSContext* cx, HandleObject prototype,
+ const mozilla::intl::Locale& tag) {
+ intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+
+ RootedString tagStr(cx, buffer.toAsciiString(cx));
+ 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 = NewObjectWithClassProto<LocaleObject>(cx, prototype);
+ if (!locale) {
+ return nullptr;
+ }
+
+ locale->setFixedSlot(LocaleObject::LANGUAGE_TAG_SLOT, StringValue(tagStr));
+ locale->setFixedSlot(LocaleObject::BASENAME_SLOT, StringValue(baseName));
+ locale->setFixedSlot(LocaleObject::UNICODE_EXTENSION_SLOT, unicodeExtension);
+
+ return locale;
+}
+
+static inline bool IsValidUnicodeExtensionValue(JSContext* cx,
+ JSLinearString* linear,
+ bool* isValid) {
+ if (linear->length() == 0) {
+ *isValid = false;
+ return true;
+ }
+
+ if (!StringIsAscii(linear)) {
+ *isValid = false;
+ return true;
+ }
+
+ intl::StringAsciiChars chars(linear);
+ if (!chars.init(cx)) {
+ return false;
+ }
+
+ *isValid =
+ mozilla::intl::LocaleParser::CanParseUnicodeExtensionType(chars).isOk();
+ return true;
+}
+
+/** 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,
+ Handle<PropertyName*> 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,
+ Handle<PropertyName*> 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.
+ linear = BooleanToString(cx, ToBoolean(option));
+ }
+
+ // Step 3.
+ string.set(linear);
+ return true;
+}
+
+/**
+ * ApplyOptionsToTag ( tag, options )
+ */
+static bool ApplyOptionsToTag(JSContext* cx, mozilla::intl::Locale& tag,
+ HandleObject options) {
+ // Steps 1-2 (Already performed in caller).
+
+ Rooted<JSLinearString*> option(cx);
+
+ // Step 3.
+ if (!GetStringOption(cx, options, cx->names().language, &option)) {
+ return false;
+ }
+
+ // Step 4.
+ mozilla::intl::LanguageSubtag language;
+ if (option && !intl::ParseStandaloneLanguageTag(option, language)) {
+ if (UniqueChars str = QuoteString(cx, option, '"')) {
+ JS_ReportErrorNumberASCII(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.
+ mozilla::intl::ScriptSubtag script;
+ if (option && !intl::ParseStandaloneScriptTag(option, script)) {
+ if (UniqueChars str = QuoteString(cx, option, '"')) {
+ JS_ReportErrorNumberASCII(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.
+ mozilla::intl::RegionSubtag region;
+ if (option && !intl::ParseStandaloneRegionTag(option, region)) {
+ if (UniqueChars str = QuoteString(cx, option, '"')) {
+ JS_ReportErrorNumberASCII(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.Present() || script.Present() || region.Present()) {
+ // Step 10.
+ if (language.Present()) {
+ tag.SetLanguage(language);
+ }
+
+ // Step 11.
+ if (script.Present()) {
+ tag.SetScript(script);
+ }
+
+ // Step 12.
+ if (region.Present()) {
+ tag.SetRegion(region);
+ }
+
+ // Step 13.
+ // Optimized to only canonicalize the base-name subtags. All other
+ // canonicalization steps will happen later.
+ auto result = tag.CanonicalizeBaseName();
+ if (result.isErr()) {
+ if (result.unwrapErr() ==
+ mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DUPLICATE_VARIANT_SUBTAG);
+ } else {
+ intl::ReportInternalError(cx);
+ }
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * ApplyUnicodeExtensionToTag( tag, options, relevantExtensionKeys )
+ */
+bool js::intl::ApplyUnicodeExtensionToTag(
+ JSContext* cx, mozilla::intl::Locale& tag,
+ JS::HandleVector<intl::UnicodeExtensionKeyword> keywords) {
+ // If no Unicode extensions were present in the options object, we can skip
+ // everything below and directly return.
+ if (keywords.length() == 0) {
+ return true;
+ }
+
+ Vector<char, 32> newExtension(cx);
+ if (!newExtension.append('u')) {
+ return false;
+ }
+
+ // Check if there's an existing Unicode extension subtag.
+
+ const char* unicodeExtensionEnd = nullptr;
+ const char* unicodeExtensionKeywords = nullptr;
+ if (auto unicodeExtension = tag.GetUnicodeExtension()) {
+ const char* unicodeExtensionBegin = unicodeExtension->data();
+ unicodeExtensionEnd = unicodeExtensionBegin + unicodeExtension->size();
+
+ SepKeywordIterator<char> iter(unicodeExtensionBegin, 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(unicodeExtensionBegin + 1, attributesEnd)) {
+ return false;
+ }
+ }
+
+ // 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.
+
+ for (const auto& keyword : keywords) {
+ UnicodeExtensionKeyword::UnicodeKeySpan key = keyword.key();
+ if (!newExtension.append('-')) {
+ return false;
+ }
+ if (!newExtension.append(key.data(), key.size())) {
+ return false;
+ }
+ if (!newExtension.append('-')) {
+ return false;
+ }
+
+ JS::AutoCheckCannotGC nogc;
+ JSLinearString* type = keyword.type();
+ if (type->hasLatin1Chars()) {
+ if (!newExtension.append(type->latin1Chars(nogc), type->length())) {
+ return false;
+ }
+ } else {
+ if (!newExtension.append(type->twoByteChars(nogc), type->length())) {
+ return false;
+ }
+ }
+ }
+
+ // Append the remaining keywords from the previous Unicode extension subtag.
+ if (unicodeExtensionKeywords) {
+ if (!newExtension.append(unicodeExtensionKeywords, unicodeExtensionEnd)) {
+ return false;
+ }
+ }
+
+ if (auto res = tag.SetUnicodeExtension(newExtension); res.isErr()) {
+ intl::ReportInternalError(cx, res.unwrapErr());
+ return false;
+ }
+
+ return true;
+}
+
+static JS::Result<JSString*> LanguageTagFromMaybeWrappedLocale(JSContext* cx,
+ JSObject* obj) {
+ if (obj->is<LocaleObject>()) {
+ return obj->as<LocaleObject>().languageTag();
+ }
+
+ JSObject* unwrapped = CheckedUnwrapStatic(obj);
+ if (!unwrapped) {
+ ReportAccessDenied(cx);
+ return cx->alreadyReportedError();
+ }
+
+ if (!unwrapped->is<LocaleObject>()) {
+ return nullptr;
+ }
+
+ RootedString tagStr(cx, unwrapped->as<LocaleObject>().languageTag());
+ if (!cx->compartment()->wrap(cx, &tagStr)) {
+ return cx->alreadyReportedError();
+ }
+ return tagStr.get();
+}
+
+/**
+ * Intl.Locale( tag[, options] )
+ */
+static bool Locale(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_Locale, &proto)) {
+ return false;
+ }
+
+ // Steps 7-9.
+ HandleValue tagValue = args.get(0);
+ JSString* tagStr;
+ if (tagValue.isObject()) {
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, tagStr,
+ LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
+ if (!tagStr) {
+ tagStr = ToString(cx, tagValue);
+ if (!tagStr) {
+ return false;
+ }
+ }
+ } else if (tagValue.isString()) {
+ tagStr = tagValue.toString();
+ } else {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INVALID_LOCALES_ELEMENT);
+ return false;
+ }
+
+ Rooted<JSLinearString*> tagLinearStr(cx, tagStr->ensureLinear(cx));
+ if (!tagLinearStr) {
+ return false;
+ }
+
+ // Steps 10-11.
+ RootedObject options(cx);
+ if (args.hasDefined(1)) {
+ options = ToObject(cx, args[1]);
+ if (!options) {
+ return false;
+ }
+ }
+
+ // ApplyOptionsToTag, steps 2 and 9.
+ mozilla::intl::Locale tag;
+ if (!intl::ParseLocale(cx, tagLinearStr, tag)) {
+ return false;
+ }
+
+ if (auto result = tag.CanonicalizeBaseName(); result.isErr()) {
+ if (result.unwrapErr() ==
+ mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DUPLICATE_VARIANT_SUBTAG);
+ } else {
+ intl::ReportInternalError(cx);
+ }
+ return false;
+ }
+
+ if (options) {
+ // Step 12.
+ if (!ApplyOptionsToTag(cx, tag, options)) {
+ return false;
+ }
+
+ // Step 13.
+ JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
+
+ // Step 14.
+ Rooted<JSLinearString*> calendar(cx);
+ if (!GetStringOption(cx, options, cx->names().calendar, &calendar)) {
+ return false;
+ }
+
+ // Steps 15-16.
+ if (calendar) {
+ bool isValid;
+ if (!IsValidUnicodeExtensionValue(cx, calendar, &isValid)) {
+ return false;
+ }
+
+ if (!isValid) {
+ if (UniqueChars str = QuoteString(cx, calendar, '"')) {
+ JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE, "calendar",
+ str.get());
+ }
+ return false;
+ }
+
+ if (!keywords.emplaceBack("ca", calendar)) {
+ return false;
+ }
+ }
+
+ // Step 17.
+ Rooted<JSLinearString*> collation(cx);
+ if (!GetStringOption(cx, options, cx->names().collation, &collation)) {
+ return false;
+ }
+
+ // Steps 18-19.
+ if (collation) {
+ bool isValid;
+ if (!IsValidUnicodeExtensionValue(cx, collation, &isValid)) {
+ return false;
+ }
+
+ if (!isValid) {
+ if (UniqueChars str = QuoteString(cx, collation, '"')) {
+ JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE, "collation",
+ str.get());
+ }
+ return false;
+ }
+
+ if (!keywords.emplaceBack("co", collation)) {
+ return false;
+ }
+ }
+
+ // Step 20 (without validation).
+ Rooted<JSLinearString*> hourCycle(cx);
+ if (!GetStringOption(cx, options, cx->names().hourCycle, &hourCycle)) {
+ return false;
+ }
+
+ // Steps 20-21.
+ if (hourCycle) {
+ if (!StringEqualsLiteral(hourCycle, "h11") &&
+ !StringEqualsLiteral(hourCycle, "h12") &&
+ !StringEqualsLiteral(hourCycle, "h23") &&
+ !StringEqualsLiteral(hourCycle, "h24")) {
+ if (UniqueChars str = QuoteString(cx, hourCycle, '"')) {
+ JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE, "hourCycle",
+ str.get());
+ }
+ return false;
+ }
+
+ if (!keywords.emplaceBack("hc", hourCycle)) {
+ return false;
+ }
+ }
+
+ // Step 22 (without validation).
+ Rooted<JSLinearString*> caseFirst(cx);
+ if (!GetStringOption(cx, options, cx->names().caseFirst, &caseFirst)) {
+ return false;
+ }
+
+ // Steps 22-23.
+ if (caseFirst) {
+ if (!StringEqualsLiteral(caseFirst, "upper") &&
+ !StringEqualsLiteral(caseFirst, "lower") &&
+ !StringEqualsLiteral(caseFirst, "false")) {
+ if (UniqueChars str = QuoteString(cx, caseFirst, '"')) {
+ JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE, "caseFirst",
+ str.get());
+ }
+ return false;
+ }
+
+ if (!keywords.emplaceBack("kf", caseFirst)) {
+ return false;
+ }
+ }
+
+ // Steps 24-25.
+ Rooted<JSLinearString*> numeric(cx);
+ if (!GetBooleanOption(cx, options, cx->names().numeric, &numeric)) {
+ return false;
+ }
+
+ // Step 26.
+ if (numeric) {
+ if (!keywords.emplaceBack("kn", numeric)) {
+ return false;
+ }
+ }
+
+ // Step 27.
+ Rooted<JSLinearString*> numberingSystem(cx);
+ if (!GetStringOption(cx, options, cx->names().numberingSystem,
+ &numberingSystem)) {
+ return false;
+ }
+
+ // Steps 28-29.
+ if (numberingSystem) {
+ bool isValid;
+ if (!IsValidUnicodeExtensionValue(cx, numberingSystem, &isValid)) {
+ return false;
+ }
+ if (!isValid) {
+ if (UniqueChars str = QuoteString(cx, numberingSystem, '"')) {
+ JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE,
+ "numberingSystem", str.get());
+ }
+ return false;
+ }
+
+ if (!keywords.emplaceBack("nu", numberingSystem)) {
+ return false;
+ }
+ }
+
+ // Step 30.
+ if (!ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
+ return false;
+ }
+ }
+
+ // ApplyOptionsToTag, steps 9 and 13.
+ // ApplyUnicodeExtensionToTag, step 9.
+ if (auto result = tag.CanonicalizeExtensions(); result.isErr()) {
+ if (result.unwrapErr() ==
+ mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DUPLICATE_VARIANT_SUBTAG);
+ } else {
+ intl::ReportInternalError(cx);
+ }
+ return false;
+ }
+
+ // Steps 6, 31-37.
+ JSObject* obj = CreateLocaleObject(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, LocaleObject* 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;
+ }
+
+ // Tell the analysis the |IsStructurallyValid*Tag| functions can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ IndexAndLength language{0, languageLength};
+ MOZ_ASSERT(
+ mozilla::intl::IsStructurallyValidLanguageTag(language.spanOf(baseName)));
+
+ mozilla::Maybe<IndexAndLength> script{};
+ if (scriptIndex) {
+ script.emplace(scriptIndex, ScriptLength);
+ MOZ_ASSERT(
+ mozilla::intl::IsStructurallyValidScriptTag(script->spanOf(baseName)));
+ }
+
+ mozilla::Maybe<IndexAndLength> region{};
+ if (regionIndex) {
+ region.emplace(regionIndex, regionLength);
+ MOZ_ASSERT(
+ mozilla::intl::IsStructurallyValidRegionTag(region->spanOf(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<LocaleObject>();
+ Rooted<JSLinearString*> tagStr(cx, locale->languageTag()->ensureLinear(cx));
+ if (!tagStr) {
+ return false;
+ }
+
+ mozilla::intl::Locale tag;
+ if (!intl::ParseLocale(cx, tagStr, tag)) {
+ return false;
+ }
+
+ if (auto result = tag.AddLikelySubtags(); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ // Step 4.
+ auto* result = CreateLocaleObject(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<LocaleObject>();
+ Rooted<JSLinearString*> tagStr(cx, locale->languageTag()->ensureLinear(cx));
+ if (!tagStr) {
+ return false;
+ }
+
+ mozilla::intl::Locale tag;
+ if (!intl::ParseLocale(cx, tagStr, tag)) {
+ return false;
+ }
+
+ if (auto result = tag.RemoveLikelySubtags(); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ // Step 4.
+ auto* result = CreateLocaleObject(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<LocaleObject>();
+ 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()));
+
+ // Steps 3-4.
+ auto* locale = &args.thisv().toObject().as<LocaleObject>();
+ 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<LocaleObject>();
+ 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.caseFirst
+static bool Locale_caseFirst(JSContext* cx, const CallArgs& args) {
+ MOZ_ASSERT(IsLocale(args.thisv()));
+
+ // Step 3.
+ auto* locale = &args.thisv().toObject().as<LocaleObject>();
+ 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.collation
+static bool Locale_collation(JSContext* cx, const CallArgs& args) {
+ MOZ_ASSERT(IsLocale(args.thisv()));
+
+ // Step 3.
+ auto* locale = &args.thisv().toObject().as<LocaleObject>();
+ 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<LocaleObject>();
+ 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.numeric
+static bool Locale_numeric(JSContext* cx, const CallArgs& args) {
+ MOZ_ASSERT(IsLocale(args.thisv()));
+
+ // Step 3.
+ auto* locale = &args.thisv().toObject().as<LocaleObject>();
+ RootedValue value(cx);
+ if (!GetUnicodeExtension(cx, locale, "kn", &value)) {
+ return false;
+ }
+
+ // Compare against the empty string per Intl.Locale, step 36.a. The Unicode
+ // extension is already canonicalized, so we don't need to compare against
+ // "true" at this point.
+ MOZ_ASSERT(value.isUndefined() || value.isString());
+ MOZ_ASSERT_IF(value.isString(),
+ !StringEqualsLiteral(&value.toString()->asLinear(), "true"));
+
+ 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<LocaleObject>();
+ 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<LocaleObject>();
+ 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.
+ 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<LocaleObject>();
+ JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
+ if (!baseName) {
+ return false;
+ }
+
+ // Step 4 (Unnecessary assertion).
+
+ auto script = BaseNameParts(baseName).script;
+
+ // Step 5.
+ 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<LocaleObject>();
+ 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_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_properties[] = {
+ JS_PSG("baseName", Locale_baseName, 0),
+ JS_PSG("calendar", Locale_calendar, 0),
+ JS_PSG("caseFirst", Locale_caseFirst, 0),
+ JS_PSG("collation", Locale_collation, 0),
+ JS_PSG("hourCycle", Locale_hourCycle, 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};
+
+const ClassSpec LocaleObject::classSpec_ = {
+ GenericCreateConstructor<Locale, 1, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<LocaleObject>,
+ nullptr,
+ nullptr,
+ locale_methods,
+ locale_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 2);
+
+ HandleValue tagValue = args[0];
+ bool applyToString = args[1].toBoolean();
+
+ if (tagValue.isObject()) {
+ JSString* tagStr;
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, tagStr,
+ LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
+ if (tagStr) {
+ args.rval().setString(tagStr);
+ return true;
+ }
+ }
+
+ if (!applyToString && !tagValue.isString()) {
+ args.rval().setNull();
+ return true;
+ }
+
+ JSString* tagStr = ToString(cx, tagValue);
+ if (!tagStr) {
+ return false;
+ }
+
+ Rooted<JSLinearString*> tagLinearStr(cx, tagStr->ensureLinear(cx));
+ if (!tagLinearStr) {
+ return false;
+ }
+
+ // Handle the common case (a standalone language) first.
+ // Only the following Unicode BCP 47 locale identifier subset is accepted:
+ // unicode_locale_id = unicode_language_id
+ // unicode_language_id = unicode_language_subtag
+ // unicode_language_subtag = alpha{2,3}
+ JSString* language;
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, language, intl::ParseStandaloneISO639LanguageTag(cx, tagLinearStr));
+ if (language) {
+ args.rval().setString(language);
+ return true;
+ }
+
+ mozilla::intl::Locale tag;
+ if (!intl::ParseLocale(cx, tagLinearStr, tag)) {
+ return false;
+ }
+
+ auto result = tag.Canonicalize();
+ if (result.isErr()) {
+ if (result.unwrapErr() ==
+ mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DUPLICATE_VARIANT_SUBTAG);
+ } else {
+ intl::ReportInternalError(cx);
+ }
+ return false;
+ }
+
+ intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ JSString* resultStr = buffer.toAsciiString(cx);
+ if (!resultStr) {
+ return false;
+ }
+
+ args.rval().setString(resultStr);
+ return true;
+}
+
+bool js::intl_TryValidateAndCanonicalizeLanguageTag(JSContext* cx,
+ unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+
+ Rooted<JSLinearString*> linear(cx, args[0].toString()->ensureLinear(cx));
+ if (!linear) {
+ return false;
+ }
+
+ mozilla::intl::Locale tag;
+ {
+ if (!StringIsAscii(linear)) {
+ // The caller handles invalid inputs.
+ args.rval().setNull();
+ return true;
+ }
+
+ intl::StringAsciiChars chars(linear);
+ if (!chars.init(cx)) {
+ return false;
+ }
+
+ if (mozilla::intl::LocaleParser::TryParse(chars, tag).isErr()) {
+ // The caller handles invalid inputs.
+ args.rval().setNull();
+ return true;
+ }
+ }
+
+ auto result = tag.Canonicalize();
+ if (result.isErr()) {
+ if (result.unwrapErr() ==
+ mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DUPLICATE_VARIANT_SUBTAG);
+ } else {
+ intl::ReportInternalError(cx);
+ }
+ return false;
+ }
+
+ intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ JSString* resultStr = buffer.toAsciiString(cx);
+ if (!resultStr) {
+ return false;
+ }
+ args.rval().setString(resultStr);
+ return true;
+}
+
+bool js::intl_ValidateAndCanonicalizeUnicodeExtensionType(JSContext* cx,
+ unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+
+ HandleValue typeArg = args[0];
+ MOZ_ASSERT(typeArg.isString(), "type must be a string");
+
+ HandleValue optionArg = args[1];
+ MOZ_ASSERT(optionArg.isString(), "option name must be a string");
+
+ HandleValue keyArg = args[2];
+ MOZ_ASSERT(keyArg.isString(), "key must be a string");
+
+ Rooted<JSLinearString*> unicodeType(cx, typeArg.toString()->ensureLinear(cx));
+ if (!unicodeType) {
+ return false;
+ }
+
+ bool isValid;
+ if (!IsValidUnicodeExtensionValue(cx, unicodeType, &isValid)) {
+ return false;
+ }
+ if (!isValid) {
+ UniqueChars optionChars = EncodeAscii(cx, optionArg.toString());
+ if (!optionChars) {
+ return false;
+ }
+
+ UniqueChars unicodeTypeChars = QuoteString(cx, unicodeType, '"');
+ if (!unicodeTypeChars) {
+ return false;
+ }
+
+ JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE, optionChars.get(),
+ unicodeTypeChars.get());
+ return false;
+ }
+
+ char unicodeKey[UnicodeKeyLength];
+ {
+ JSLinearString* str = keyArg.toString()->ensureLinear(cx);
+ if (!str) {
+ return false;
+ }
+ MOZ_ASSERT(str->length() == UnicodeKeyLength);
+
+ for (size_t i = 0; i < UnicodeKeyLength; i++) {
+ char16_t ch = str->latin1OrTwoByteChar(i);
+ MOZ_ASSERT(mozilla::IsAscii(ch));
+ unicodeKey[i] = char(ch);
+ }
+ }
+
+ UniqueChars unicodeTypeChars = EncodeAscii(cx, unicodeType);
+ if (!unicodeTypeChars) {
+ return false;
+ }
+
+ size_t unicodeTypeLength = unicodeType->length();
+ MOZ_ASSERT(strlen(unicodeTypeChars.get()) == unicodeTypeLength);
+
+ // Convert into canonical case before searching for replacements.
+ mozilla::intl::AsciiToLowerCase(unicodeTypeChars.get(), unicodeTypeLength,
+ unicodeTypeChars.get());
+
+ auto key = mozilla::Span(unicodeKey, UnicodeKeyLength);
+ auto type = mozilla::Span(unicodeTypeChars.get(), unicodeTypeLength);
+
+ // Search if there's a replacement for the current Unicode keyword.
+ JSString* result;
+ if (const char* replacement =
+ mozilla::intl::Locale::ReplaceUnicodeExtensionType(key, type)) {
+ result = NewStringCopyZ<CanGC>(cx, replacement);
+ } else {
+ result = StringToLowerCase(cx, unicodeType);
+ }
+ if (!result) {
+ return false;
+ }
+
+ args.rval().setString(result);
+ return true;
+}
diff --git a/js/src/builtin/intl/Locale.h b/js/src/builtin/intl/Locale.h
new file mode 100644
index 0000000000..93b618528a
--- /dev/null
+++ b/js/src/builtin/intl/Locale.h
@@ -0,0 +1,61 @@
+/* -*- 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>
+
+#include "js/Class.h"
+#include "vm/NativeObject.h"
+
+namespace js {
+
+class LocaleObject : public NativeObject {
+ public:
+ static const JSClass class_;
+ static const JSClass& protoClass_;
+
+ 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);
+ }
+
+ private:
+ static const ClassSpec classSpec_;
+};
+
+[[nodiscard]] extern bool intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx,
+ unsigned argc,
+ Value* vp);
+
+[[nodiscard]] extern bool intl_TryValidateAndCanonicalizeLanguageTag(
+ JSContext* cx, unsigned argc, Value* vp);
+
+[[nodiscard]] extern bool intl_ValidateAndCanonicalizeUnicodeExtensionType(
+ JSContext* cx, unsigned argc, Value* vp);
+
+} // namespace js
+
+#endif /* builtin_intl_Locale_h */
diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp
new file mode 100644
index 0000000000..21b0db030e
--- /dev/null
+++ b/js/src/builtin/intl/NumberFormat.cpp
@@ -0,0 +1,1374 @@
+/* -*- 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.NumberFormat implementation. */
+
+#include "builtin/intl/NumberFormat.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Casting.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/intl/MeasureUnit.h"
+#include "mozilla/intl/MeasureUnitGenerated.h"
+#include "mozilla/intl/NumberFormat.h"
+#include "mozilla/intl/NumberingSystem.h"
+#include "mozilla/intl/NumberRangeFormat.h"
+#include "mozilla/Span.h"
+#include "mozilla/TextUtils.h"
+#include "mozilla/UniquePtr.h"
+
+#include <algorithm>
+#include <stddef.h>
+#include <stdint.h>
+#include <string>
+#include <string_view>
+#include <type_traits>
+
+#include "builtin/Array.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/DecimalNumber.h"
+#include "builtin/intl/FormatBuffer.h"
+#include "builtin/intl/LanguageTag.h"
+#include "builtin/intl/RelativeTimeFormat.h"
+#include "gc/GCContext.h"
+#include "js/CharacterEncoding.h"
+#include "js/PropertySpec.h"
+#include "js/RootingAPI.h"
+#include "js/TypeDecls.h"
+#include "util/Text.h"
+#include "vm/BigIntType.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/StringType.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/GeckoProfiler-inl.h"
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+using mozilla::AssertedCast;
+
+using js::intl::DateTimeFormatOptions;
+using js::intl::FieldType;
+
+const JSClassOps NumberFormatObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ NumberFormatObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ nullptr, // trace
+};
+
+const JSClass NumberFormatObject::class_ = {
+ "Intl.NumberFormat",
+ JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_NumberFormat) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &NumberFormatObject::classOps_, &NumberFormatObject::classSpec_};
+
+const JSClass& NumberFormatObject::protoClass_ = PlainObject::class_;
+
+static bool numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().NumberFormat);
+ return true;
+}
+
+static const JSFunctionSpec numberFormat_static_methods[] = {
+ JS_SELF_HOSTED_FN("supportedLocalesOf",
+ "Intl_NumberFormat_supportedLocalesOf", 1, 0),
+ JS_FS_END,
+};
+
+static const JSFunctionSpec numberFormat_methods[] = {
+ JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0,
+ 0),
+ JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0),
+#ifdef NIGHTLY_BUILD
+ JS_SELF_HOSTED_FN("formatRange", "Intl_NumberFormat_formatRange", 2, 0),
+ JS_SELF_HOSTED_FN("formatRangeToParts",
+ "Intl_NumberFormat_formatRangeToParts", 2, 0),
+#endif
+ JS_FN(js_toSource_str, numberFormat_toSource, 0, 0),
+ JS_FS_END,
+};
+
+static const JSPropertySpec numberFormat_properties[] = {
+ JS_SELF_HOSTED_GET("format", "$Intl_NumberFormat_format_get", 0),
+ JS_STRING_SYM_PS(toStringTag, "Intl.NumberFormat", JSPROP_READONLY),
+ JS_PS_END,
+};
+
+static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp);
+
+const ClassSpec NumberFormatObject::classSpec_ = {
+ GenericCreateConstructor<NumberFormat, 0, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<NumberFormatObject>,
+ numberFormat_static_methods,
+ nullptr,
+ numberFormat_methods,
+ numberFormat_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+/**
+ * 11.2.1 Intl.NumberFormat([ locales [, options]])
+ *
+ * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
+ */
+static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) {
+ AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.NumberFormat");
+
+ // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
+
+ // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+ RootedObject proto(cx);
+ if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_NumberFormat,
+ &proto)) {
+ return false;
+ }
+
+ Rooted<NumberFormatObject*> numberFormat(cx);
+ numberFormat = NewObjectWithClassProto<NumberFormatObject>(cx, proto);
+ if (!numberFormat) {
+ return false;
+ }
+
+ RootedValue thisValue(cx,
+ construct ? ObjectValue(*numberFormat) : args.thisv());
+ HandleValue locales = args.get(0);
+ HandleValue options = args.get(1);
+
+ // Step 3.
+ return intl::LegacyInitializeObject(
+ cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, locales,
+ options, DateTimeFormatOptions::Standard, args.rval());
+}
+
+static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ return NumberFormat(cx, args, args.isConstructing());
+}
+
+bool js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 2);
+ MOZ_ASSERT(!args.isConstructing());
+ // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it
+ // cannot be used with "new", but it still has to be treated as a
+ // constructor.
+ return NumberFormat(cx, args, true);
+}
+
+void js::NumberFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ MOZ_ASSERT(gcx->onMainThread());
+
+ auto* numberFormat = &obj->as<NumberFormatObject>();
+ mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter();
+ mozilla::intl::NumberRangeFormat* nrf =
+ numberFormat->getNumberRangeFormatter();
+
+ if (nf) {
+ intl::RemoveICUCellMemory(gcx, obj, NumberFormatObject::EstimatedMemoryUse);
+ // This was allocated using `new` in mozilla::intl::NumberFormat, so we
+ // delete here.
+ delete nf;
+ }
+
+ if (nrf) {
+ intl::RemoveICUCellMemory(gcx, obj, EstimatedRangeFormatterMemoryUse);
+ // This was allocated using `new` in mozilla::intl::NumberRangeFormat, so we
+ // delete here.
+ delete nrf;
+ }
+}
+
+bool js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+
+ auto numberingSystem =
+ mozilla::intl::NumberingSystem::TryCreate(locale.get());
+ if (numberingSystem.isErr()) {
+ intl::ReportInternalError(cx, numberingSystem.unwrapErr());
+ return false;
+ }
+
+ auto name = numberingSystem.inspect()->GetName();
+ if (name.isErr()) {
+ intl::ReportInternalError(cx, name.unwrapErr());
+ return false;
+ }
+
+ JSString* jsname = NewStringCopy<CanGC>(cx, name.unwrap());
+ if (!jsname) {
+ return false;
+ }
+
+ args.rval().setString(jsname);
+ return true;
+}
+
+#if DEBUG || MOZ_SYSTEM_ICU
+bool js::intl_availableMeasurementUnits(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 0);
+
+ RootedObject measurementUnits(cx, NewPlainObjectWithProto(cx, nullptr));
+ if (!measurementUnits) {
+ return false;
+ }
+
+ auto units = mozilla::intl::MeasureUnit::GetAvailable();
+ if (units.isErr()) {
+ intl::ReportInternalError(cx, units.unwrapErr());
+ return false;
+ }
+
+ Rooted<JSAtom*> unitAtom(cx);
+ for (auto unit : units.unwrap()) {
+ if (unit.isErr()) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ auto unitIdentifier = unit.unwrap();
+
+ unitAtom = Atomize(cx, unitIdentifier.data(), unitIdentifier.size());
+ if (!unitAtom) {
+ return false;
+ }
+
+ if (!DefineDataProperty(cx, measurementUnits, unitAtom->asPropertyName(),
+ TrueHandleValue)) {
+ return false;
+ }
+ }
+
+ args.rval().setObject(*measurementUnits);
+ return true;
+}
+#endif
+
+static constexpr size_t MaxUnitLength() {
+ size_t length = 0;
+ for (const auto& unit : mozilla::intl::simpleMeasureUnits) {
+ length = std::max(length, std::char_traits<char>::length(unit.name));
+ }
+ return length * 2 + std::char_traits<char>::length("-per-");
+}
+
+static UniqueChars NumberFormatLocale(JSContext* cx, HandleObject internals) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
+ return nullptr;
+ }
+
+ // ICU expects numberingSystem as a Unicode locale extensions on locale.
+
+ mozilla::intl::Locale tag;
+ {
+ Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx));
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!intl::ParseLocale(cx, locale, tag)) {
+ return nullptr;
+ }
+ }
+
+ JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
+
+ if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
+ &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
+ if (!numberingSystem) {
+ return nullptr;
+ }
+
+ if (!keywords.emplaceBack("nu", numberingSystem)) {
+ return nullptr;
+ }
+ }
+
+ // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
+ // the Unicode extension subtag. We're then relying on ICU to follow RFC
+ // 6067, which states that any trailing keywords using the same key
+ // should be ignored.
+ if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
+ return nullptr;
+ }
+
+ intl::FormatBuffer<char> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+ return buffer.extractStringZ();
+}
+
+struct NumberFormatOptions : public mozilla::intl::NumberRangeFormatOptions {
+ static_assert(std::is_base_of_v<mozilla::intl::NumberFormatOptions,
+ mozilla::intl::NumberRangeFormatOptions>);
+
+ char currencyChars[3] = {};
+ char unitChars[MaxUnitLength()] = {};
+};
+
+static bool FillNumberFormatOptions(JSContext* cx, HandleObject internals,
+ NumberFormatOptions& options) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
+ return false;
+ }
+
+ bool accountingSign = false;
+ {
+ JSLinearString* style = value.toString()->ensureLinear(cx);
+ if (!style) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(style, "currency")) {
+ if (!GetProperty(cx, internals, internals, cx->names().currency,
+ &value)) {
+ return false;
+ }
+ JSLinearString* currency = value.toString()->ensureLinear(cx);
+ if (!currency) {
+ return false;
+ }
+
+ MOZ_RELEASE_ASSERT(
+ currency->length() == 3,
+ "IsWellFormedCurrencyCode permits only length-3 strings");
+ MOZ_ASSERT(StringIsAscii(currency),
+ "IsWellFormedCurrencyCode permits only ASCII strings");
+ CopyChars(reinterpret_cast<Latin1Char*>(options.currencyChars),
+ *currency);
+
+ if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay,
+ &value)) {
+ return false;
+ }
+ JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx);
+ if (!currencyDisplay) {
+ return false;
+ }
+
+ using CurrencyDisplay =
+ mozilla::intl::NumberFormatOptions::CurrencyDisplay;
+
+ CurrencyDisplay display;
+ if (StringEqualsLiteral(currencyDisplay, "code")) {
+ display = CurrencyDisplay::Code;
+ } else if (StringEqualsLiteral(currencyDisplay, "symbol")) {
+ display = CurrencyDisplay::Symbol;
+ } else if (StringEqualsLiteral(currencyDisplay, "narrowSymbol")) {
+ display = CurrencyDisplay::NarrowSymbol;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(currencyDisplay, "name"));
+ display = CurrencyDisplay::Name;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().currencySign,
+ &value)) {
+ return false;
+ }
+ JSLinearString* currencySign = value.toString()->ensureLinear(cx);
+ if (!currencySign) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(currencySign, "accounting")) {
+ accountingSign = true;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(currencySign, "standard"));
+ }
+
+ options.mCurrency = mozilla::Some(
+ std::make_pair(std::string_view(options.currencyChars, 3), display));
+ } else if (StringEqualsLiteral(style, "percent")) {
+ options.mPercent = true;
+ } else if (StringEqualsLiteral(style, "unit")) {
+ if (!GetProperty(cx, internals, internals, cx->names().unit, &value)) {
+ return false;
+ }
+ JSLinearString* unit = value.toString()->ensureLinear(cx);
+ if (!unit) {
+ return false;
+ }
+
+ size_t unit_str_length = unit->length();
+
+ MOZ_ASSERT(StringIsAscii(unit));
+ MOZ_RELEASE_ASSERT(unit_str_length <= MaxUnitLength());
+ CopyChars(reinterpret_cast<Latin1Char*>(options.unitChars), *unit);
+
+ if (!GetProperty(cx, internals, internals, cx->names().unitDisplay,
+ &value)) {
+ return false;
+ }
+ JSLinearString* unitDisplay = value.toString()->ensureLinear(cx);
+ if (!unitDisplay) {
+ return false;
+ }
+
+ using UnitDisplay = mozilla::intl::NumberFormatOptions::UnitDisplay;
+
+ UnitDisplay display;
+ if (StringEqualsLiteral(unitDisplay, "short")) {
+ display = UnitDisplay::Short;
+ } else if (StringEqualsLiteral(unitDisplay, "narrow")) {
+ display = UnitDisplay::Narrow;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(unitDisplay, "long"));
+ display = UnitDisplay::Long;
+ }
+
+ options.mUnit = mozilla::Some(std::make_pair(
+ std::string_view(options.unitChars, unit_str_length), display));
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(style, "decimal"));
+ }
+ }
+
+ bool hasMinimumSignificantDigits;
+ if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits,
+ &hasMinimumSignificantDigits)) {
+ return false;
+ }
+
+ if (hasMinimumSignificantDigits) {
+ if (!GetProperty(cx, internals, internals,
+ cx->names().minimumSignificantDigits, &value)) {
+ return false;
+ }
+ uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
+
+ if (!GetProperty(cx, internals, internals,
+ cx->names().maximumSignificantDigits, &value)) {
+ return false;
+ }
+ uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
+
+ options.mSignificantDigits = mozilla::Some(
+ std::make_pair(minimumSignificantDigits, maximumSignificantDigits));
+ }
+
+ bool hasMinimumFractionDigits;
+ if (!HasProperty(cx, internals, cx->names().minimumFractionDigits,
+ &hasMinimumFractionDigits)) {
+ return false;
+ }
+
+ if (hasMinimumFractionDigits) {
+ if (!GetProperty(cx, internals, internals,
+ cx->names().minimumFractionDigits, &value)) {
+ return false;
+ }
+ uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
+
+ if (!GetProperty(cx, internals, internals,
+ cx->names().maximumFractionDigits, &value)) {
+ return false;
+ }
+ uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
+
+ options.mFractionDigits = mozilla::Some(
+ std::make_pair(minimumFractionDigits, maximumFractionDigits));
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().roundingPriority,
+ &value)) {
+ return false;
+ }
+
+ {
+ JSLinearString* roundingPriority = value.toString()->ensureLinear(cx);
+ if (!roundingPriority) {
+ return false;
+ }
+
+ using RoundingPriority =
+ mozilla::intl::NumberFormatOptions::RoundingPriority;
+
+ RoundingPriority priority;
+ if (StringEqualsLiteral(roundingPriority, "auto")) {
+ priority = RoundingPriority::Auto;
+ } else if (StringEqualsLiteral(roundingPriority, "morePrecision")) {
+ priority = RoundingPriority::MorePrecision;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(roundingPriority, "lessPrecision"));
+ priority = RoundingPriority::LessPrecision;
+ }
+
+ options.mRoundingPriority = priority;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
+ &value)) {
+ return false;
+ }
+ options.mMinIntegerDigits =
+ mozilla::Some(AssertedCast<uint32_t>(value.toInt32()));
+
+ if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) {
+ return false;
+ }
+
+ if (value.isString()) {
+ JSLinearString* useGrouping = value.toString()->ensureLinear(cx);
+ if (!useGrouping) {
+ return false;
+ }
+
+ using Grouping = mozilla::intl::NumberFormatOptions::Grouping;
+
+ Grouping grouping;
+ if (StringEqualsLiteral(useGrouping, "auto")) {
+ grouping = Grouping::Auto;
+ } else if (StringEqualsLiteral(useGrouping, "always")) {
+ grouping = Grouping::Always;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(useGrouping, "min2"));
+ grouping = Grouping::Min2;
+ }
+
+ options.mGrouping = grouping;
+ } else {
+ MOZ_ASSERT(value.isBoolean());
+#ifdef NIGHTLY_BUILD
+ // The caller passes the string "always" instead of |true| when the
+ // NumberFormat V3 spec is being used.
+ MOZ_ASSERT(value.toBoolean() == false);
+#endif
+
+ using Grouping = mozilla::intl::NumberFormatOptions::Grouping;
+
+ Grouping grouping;
+ if (value.toBoolean()) {
+ grouping = Grouping::Auto;
+ } else {
+ grouping = Grouping::Never;
+ }
+
+ options.mGrouping = grouping;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().notation, &value)) {
+ return false;
+ }
+
+ {
+ JSLinearString* notation = value.toString()->ensureLinear(cx);
+ if (!notation) {
+ return false;
+ }
+
+ using Notation = mozilla::intl::NumberFormatOptions::Notation;
+
+ Notation style;
+ if (StringEqualsLiteral(notation, "standard")) {
+ style = Notation::Standard;
+ } else if (StringEqualsLiteral(notation, "scientific")) {
+ style = Notation::Scientific;
+ } else if (StringEqualsLiteral(notation, "engineering")) {
+ style = Notation::Engineering;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(notation, "compact"));
+
+ if (!GetProperty(cx, internals, internals, cx->names().compactDisplay,
+ &value)) {
+ return false;
+ }
+
+ JSLinearString* compactDisplay = value.toString()->ensureLinear(cx);
+ if (!compactDisplay) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(compactDisplay, "short")) {
+ style = Notation::CompactShort;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(compactDisplay, "long"));
+ style = Notation::CompactLong;
+ }
+ }
+
+ options.mNotation = style;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().signDisplay, &value)) {
+ return false;
+ }
+
+ {
+ JSLinearString* signDisplay = value.toString()->ensureLinear(cx);
+ if (!signDisplay) {
+ return false;
+ }
+
+ using SignDisplay = mozilla::intl::NumberFormatOptions::SignDisplay;
+
+ SignDisplay display;
+ if (StringEqualsLiteral(signDisplay, "auto")) {
+ if (accountingSign) {
+ display = SignDisplay::Accounting;
+ } else {
+ display = SignDisplay::Auto;
+ }
+ } else if (StringEqualsLiteral(signDisplay, "never")) {
+ display = SignDisplay::Never;
+ } else if (StringEqualsLiteral(signDisplay, "always")) {
+ if (accountingSign) {
+ display = SignDisplay::AccountingAlways;
+ } else {
+ display = SignDisplay::Always;
+ }
+ } else if (StringEqualsLiteral(signDisplay, "exceptZero")) {
+ if (accountingSign) {
+ display = SignDisplay::AccountingExceptZero;
+ } else {
+ display = SignDisplay::ExceptZero;
+ }
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(signDisplay, "negative"));
+ if (accountingSign) {
+ display = SignDisplay::AccountingNegative;
+ } else {
+ display = SignDisplay::Negative;
+ }
+ }
+
+ options.mSignDisplay = display;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().roundingIncrement,
+ &value)) {
+ return false;
+ }
+ options.mRoundingIncrement = AssertedCast<uint32_t>(value.toInt32());
+
+ if (!GetProperty(cx, internals, internals, cx->names().roundingMode,
+ &value)) {
+ return false;
+ }
+
+ {
+ JSLinearString* roundingMode = value.toString()->ensureLinear(cx);
+ if (!roundingMode) {
+ return false;
+ }
+
+ using RoundingMode = mozilla::intl::NumberFormatOptions::RoundingMode;
+
+ RoundingMode rounding;
+ if (StringEqualsLiteral(roundingMode, "halfExpand")) {
+ // "halfExpand" is the default mode, so we handle it first.
+ rounding = RoundingMode::HalfExpand;
+ } else if (StringEqualsLiteral(roundingMode, "ceil")) {
+ rounding = RoundingMode::Ceil;
+ } else if (StringEqualsLiteral(roundingMode, "floor")) {
+ rounding = RoundingMode::Floor;
+ } else if (StringEqualsLiteral(roundingMode, "expand")) {
+ rounding = RoundingMode::Expand;
+ } else if (StringEqualsLiteral(roundingMode, "trunc")) {
+ rounding = RoundingMode::Trunc;
+ } else if (StringEqualsLiteral(roundingMode, "halfCeil")) {
+ rounding = RoundingMode::HalfCeil;
+ } else if (StringEqualsLiteral(roundingMode, "halfFloor")) {
+ rounding = RoundingMode::HalfFloor;
+ } else if (StringEqualsLiteral(roundingMode, "halfTrunc")) {
+ rounding = RoundingMode::HalfTrunc;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(roundingMode, "halfEven"));
+ rounding = RoundingMode::HalfEven;
+ }
+
+ options.mRoundingMode = rounding;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().trailingZeroDisplay,
+ &value)) {
+ return false;
+ }
+
+ {
+ JSLinearString* trailingZeroDisplay = value.toString()->ensureLinear(cx);
+ if (!trailingZeroDisplay) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(trailingZeroDisplay, "auto")) {
+ options.mStripTrailingZero = false;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(trailingZeroDisplay, "stripIfInteger"));
+ options.mStripTrailingZero = true;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Returns a new mozilla::intl::Number[Range]Format with the locale and number
+ * formatting options of the given NumberFormat, or a nullptr if
+ * initialization failed.
+ */
+template <class Formatter>
+static Formatter* NewNumberFormat(JSContext* cx,
+ Handle<NumberFormatObject*> numberFormat) {
+ RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat));
+ if (!internals) {
+ return nullptr;
+ }
+
+ UniqueChars locale = NumberFormatLocale(cx, internals);
+ if (!locale) {
+ return nullptr;
+ }
+
+ NumberFormatOptions options;
+ if (!FillNumberFormatOptions(cx, internals, options)) {
+ return nullptr;
+ }
+
+ options.mRangeCollapse = NumberFormatOptions::RangeCollapse::Auto;
+ options.mRangeIdentityFallback =
+ NumberFormatOptions::RangeIdentityFallback::Approximately;
+
+ mozilla::Result<mozilla::UniquePtr<Formatter>, mozilla::intl::ICUError>
+ result = Formatter::TryCreate(locale.get(), options);
+
+ if (result.isOk()) {
+ return result.unwrap().release();
+ }
+
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+}
+
+static mozilla::intl::NumberFormat* GetOrCreateNumberFormat(
+ JSContext* cx, Handle<NumberFormatObject*> numberFormat) {
+ // Obtain a cached mozilla::intl::NumberFormat object.
+ mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter();
+ if (nf) {
+ return nf;
+ }
+
+ nf = NewNumberFormat<mozilla::intl::NumberFormat>(cx, numberFormat);
+ if (!nf) {
+ return nullptr;
+ }
+ numberFormat->setNumberFormatter(nf);
+
+ intl::AddICUCellMemory(numberFormat, NumberFormatObject::EstimatedMemoryUse);
+ return nf;
+}
+
+static mozilla::intl::NumberRangeFormat* GetOrCreateNumberRangeFormat(
+ JSContext* cx, Handle<NumberFormatObject*> numberFormat) {
+ // Obtain a cached mozilla::intl::NumberRangeFormat object.
+ mozilla::intl::NumberRangeFormat* nrf =
+ numberFormat->getNumberRangeFormatter();
+ if (nrf) {
+ return nrf;
+ }
+
+ nrf = NewNumberFormat<mozilla::intl::NumberRangeFormat>(cx, numberFormat);
+ if (!nrf) {
+ return nullptr;
+ }
+ numberFormat->setNumberRangeFormatter(nrf);
+
+ intl::AddICUCellMemory(numberFormat,
+ NumberFormatObject::EstimatedRangeFormatterMemoryUse);
+ return nrf;
+}
+
+static FieldType GetFieldTypeForNumberPartType(
+ mozilla::intl::NumberPartType type) {
+ switch (type) {
+ case mozilla::intl::NumberPartType::ApproximatelySign:
+ return &JSAtomState::approximatelySign;
+ case mozilla::intl::NumberPartType::Compact:
+ return &JSAtomState::compact;
+ case mozilla::intl::NumberPartType::Currency:
+ return &JSAtomState::currency;
+ case mozilla::intl::NumberPartType::Decimal:
+ return &JSAtomState::decimal;
+ case mozilla::intl::NumberPartType::ExponentInteger:
+ return &JSAtomState::exponentInteger;
+ case mozilla::intl::NumberPartType::ExponentMinusSign:
+ return &JSAtomState::exponentMinusSign;
+ case mozilla::intl::NumberPartType::ExponentSeparator:
+ return &JSAtomState::exponentSeparator;
+ case mozilla::intl::NumberPartType::Fraction:
+ return &JSAtomState::fraction;
+ case mozilla::intl::NumberPartType::Group:
+ return &JSAtomState::group;
+ case mozilla::intl::NumberPartType::Infinity:
+ return &JSAtomState::infinity;
+ case mozilla::intl::NumberPartType::Integer:
+ return &JSAtomState::integer;
+ case mozilla::intl::NumberPartType::Literal:
+ return &JSAtomState::literal;
+ case mozilla::intl::NumberPartType::MinusSign:
+ return &JSAtomState::minusSign;
+ case mozilla::intl::NumberPartType::Nan:
+ return &JSAtomState::nan;
+ case mozilla::intl::NumberPartType::Percent:
+ return &JSAtomState::percentSign;
+ case mozilla::intl::NumberPartType::PlusSign:
+ return &JSAtomState::plusSign;
+ case mozilla::intl::NumberPartType::Unit:
+ return &JSAtomState::unit;
+ }
+
+ MOZ_ASSERT_UNREACHABLE(
+ "unenumerated, undocumented format field returned by iterator");
+ return nullptr;
+}
+
+static FieldType GetFieldTypeForNumberPartSource(
+ mozilla::intl::NumberPartSource source) {
+ switch (source) {
+ case mozilla::intl::NumberPartSource::Shared:
+ return &JSAtomState::shared;
+ case mozilla::intl::NumberPartSource::Start:
+ return &JSAtomState::startRange;
+ case mozilla::intl::NumberPartSource::End:
+ return &JSAtomState::endRange;
+ }
+
+ MOZ_CRASH("unexpected number part source");
+}
+
+enum class DisplayNumberPartSource : bool { No, Yes };
+
+static bool FormattedNumberToParts(JSContext* cx, HandleString str,
+ const mozilla::intl::NumberPartVector& parts,
+ DisplayNumberPartSource displaySource,
+ FieldType unitType,
+ MutableHandleValue result) {
+ size_t lastEndIndex = 0;
+
+ RootedObject singlePart(cx);
+ RootedValue propVal(cx);
+
+ Rooted<ArrayObject*> partsArray(
+ cx, NewDenseFullyAllocatedArray(cx, parts.length()));
+ if (!partsArray) {
+ return false;
+ }
+ partsArray->ensureDenseInitializedLength(0, parts.length());
+
+ size_t index = 0;
+ for (const auto& part : parts) {
+ FieldType type = GetFieldTypeForNumberPartType(part.type);
+ size_t endIndex = part.endIndex;
+
+ MOZ_ASSERT(lastEndIndex < endIndex);
+
+ singlePart = NewPlainObject(cx);
+ if (!singlePart) {
+ return false;
+ }
+
+ propVal.setString(cx->names().*type);
+ if (!DefineDataProperty(cx, singlePart, cx->names().type, propVal)) {
+ return false;
+ }
+
+ JSLinearString* partSubstr =
+ NewDependentString(cx, str, lastEndIndex, endIndex - lastEndIndex);
+ if (!partSubstr) {
+ return false;
+ }
+
+ propVal.setString(partSubstr);
+ if (!DefineDataProperty(cx, singlePart, cx->names().value, propVal)) {
+ return false;
+ }
+
+ if (displaySource == DisplayNumberPartSource::Yes) {
+ FieldType source = GetFieldTypeForNumberPartSource(part.source);
+
+ propVal.setString(cx->names().*source);
+ if (!DefineDataProperty(cx, singlePart, cx->names().source, propVal)) {
+ return false;
+ }
+ }
+
+ if (unitType != nullptr && type != &JSAtomState::literal) {
+ propVal.setString(cx->names().*unitType);
+ if (!DefineDataProperty(cx, singlePart, cx->names().unit, propVal)) {
+ return false;
+ }
+ }
+
+ partsArray->initDenseElement(index++, ObjectValue(*singlePart));
+
+ lastEndIndex = endIndex;
+ }
+
+ MOZ_ASSERT(index == parts.length());
+ MOZ_ASSERT(lastEndIndex == str->length(),
+ "result array must partition the entire string");
+
+ result.setObject(*partsArray);
+ return true;
+}
+
+bool js::intl::FormattedRelativeTimeToParts(
+ JSContext* cx, HandleString str,
+ const mozilla::intl::NumberPartVector& parts, FieldType relativeTimeUnit,
+ MutableHandleValue result) {
+ return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No,
+ relativeTimeUnit, result);
+}
+
+// Return true if the string starts with "0[bBoOxX]", possibly skipping over
+// leading whitespace.
+template <typename CharT>
+static bool IsNonDecimalNumber(mozilla::Range<const CharT> chars) {
+ const CharT* end = chars.begin().get() + chars.length();
+ const CharT* start = SkipSpace(chars.begin().get(), end);
+
+ if (end - start >= 2 && start[0] == '0') {
+ CharT ch = start[1];
+ return ch == 'b' || ch == 'B' || ch == 'o' || ch == 'O' || ch == 'x' ||
+ ch == 'X';
+ }
+ return false;
+}
+
+static bool IsNonDecimalNumber(JSLinearString* str) {
+ JS::AutoCheckCannotGC nogc;
+ return str->hasLatin1Chars() ? IsNonDecimalNumber(str->latin1Range(nogc))
+ : IsNonDecimalNumber(str->twoByteRange(nogc));
+}
+
+static bool ToIntlMathematicalValue(JSContext* cx, MutableHandleValue value) {
+ if (!ToPrimitive(cx, JSTYPE_NUMBER, value)) {
+ return false;
+ }
+
+ // Maximum exponent supported by ICU. Exponents larger than this value will
+ // cause ICU to report an error.
+ // See also "intl/icu/source/i18n/decContext.h".
+ constexpr int32_t maximumExponent = 999'999'999;
+
+ // We further limit the maximum positive exponent to avoid spending multiple
+ // seconds or even minutes in ICU when formatting large numbers.
+ constexpr int32_t maximumPositiveExponent = 9'999'999;
+
+ // Compute the maximum BigInt digit length from the maximum positive exponent.
+ //
+ // BigInts are stored with base |2 ** BigInt::DigitBits|, so we have:
+ //
+ // |maximumPositiveExponent| * Log_DigitBase(10)
+ // = |maximumPositiveExponent| * Log2(10) / Log2(2 ** BigInt::DigitBits)
+ // = |maximumPositiveExponent| * Log2(10) / BigInt::DigitBits
+ // = 33219277.626945525... / BigInt::DigitBits
+ constexpr size_t maximumBigIntLength = 33219277.626945525 / BigInt::DigitBits;
+
+ if (!value.isString()) {
+ if (!ToNumeric(cx, value)) {
+ return false;
+ }
+
+ if (value.isBigInt() &&
+ value.toBigInt()->digitLength() > maximumBigIntLength) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_EXPONENT_TOO_LARGE);
+ return false;
+ }
+
+ return true;
+ }
+
+ JSLinearString* str = value.toString()->ensureLinear(cx);
+ if (!str) {
+ return false;
+ }
+
+ // Parse the string as a number.
+ double number = LinearStringToNumber(str);
+
+ bool exponentTooLarge = false;
+ if (mozilla::IsNaN(number)) {
+ // Set to NaN if the input can't be parsed as a number.
+ value.setNaN();
+ } else if (IsNonDecimalNumber(str)) {
+ // ICU doesn't accept non-decimal numbers, so we have to convert the input
+ // into a base-10 string.
+
+ MOZ_ASSERT(!mozilla::IsNegative(number),
+ "non-decimal numbers can't be negative");
+
+ if (number < DOUBLE_INTEGRAL_PRECISION_LIMIT) {
+ // Fast-path if we can guarantee there was no loss of precision.
+ value.setDouble(number);
+ } else {
+ // For the slow-path convert the string into a BigInt.
+
+ // StringToBigInt can't fail (other than OOM) when StringToNumber already
+ // succeeded.
+ RootedString rooted(cx, str);
+ BigInt* bi;
+ JS_TRY_VAR_OR_RETURN_FALSE(cx, bi, StringToBigInt(cx, rooted));
+ MOZ_ASSERT(bi);
+
+ if (bi->digitLength() > maximumBigIntLength) {
+ exponentTooLarge = true;
+ } else {
+ value.setBigInt(bi);
+ }
+ }
+ } else {
+ JS::AutoCheckCannotGC nogc;
+ if (auto decimal = intl::DecimalNumber::from(str, nogc)) {
+ if (decimal->isZero()) {
+ // Normalize positive/negative zero.
+ MOZ_ASSERT(number == 0);
+
+ value.setDouble(number);
+ } else if (decimal->exponentTooLarge() ||
+ std::abs(decimal->exponent()) >= maximumExponent ||
+ decimal->exponent() > maximumPositiveExponent) {
+ exponentTooLarge = true;
+ }
+ } else {
+ // If we can't parse the string as a decimal, it must be ±Infinity.
+ MOZ_ASSERT(mozilla::IsInfinite(number));
+ MOZ_ASSERT(StringFindPattern(str, cx->names().Infinity, 0) >= 0);
+
+ value.setDouble(number);
+ }
+ }
+
+ if (exponentTooLarge) {
+ // Throw an error if the exponent is too large.
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_EXPONENT_TOO_LARGE);
+ return false;
+ }
+
+ return true;
+}
+
+// Return the number part of the input by removing leading and trailing
+// whitespace.
+template <typename CharT>
+static mozilla::Span<const CharT> NumberPart(const CharT* chars,
+ size_t length) {
+ const CharT* start = chars;
+ const CharT* end = chars + length;
+
+ start = SkipSpace(start, end);
+
+ // |SkipSpace| only supports forward iteration, so inline the backwards
+ // iteration here.
+ MOZ_ASSERT(start <= end);
+ while (end > start && unicode::IsSpace(end[-1])) {
+ end--;
+ }
+
+ // The number part is a non-empty, ASCII-only substring.
+ MOZ_ASSERT(start < end);
+ MOZ_ASSERT(mozilla::IsAscii(mozilla::Span(start, end)));
+
+ return {start, end};
+}
+
+static bool NumberPart(JSContext* cx, JSLinearString* str,
+ const JS::AutoCheckCannotGC& nogc,
+ JS::UniqueChars& latin1, std::string_view& result) {
+ if (str->hasLatin1Chars()) {
+ auto span = NumberPart(
+ reinterpret_cast<const char*>(str->latin1Chars(nogc)), str->length());
+
+ result = {span.data(), span.size()};
+ return true;
+ }
+
+ auto span = NumberPart(str->twoByteChars(nogc), str->length());
+
+ latin1.reset(JS::LossyTwoByteCharsToNewLatin1CharsZ(cx, span).c_str());
+ if (!latin1) {
+ return false;
+ }
+
+ result = {latin1.get(), span.size()};
+ return true;
+}
+
+bool js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+ MOZ_ASSERT(args[0].isObject());
+#ifndef NIGHTLY_BUILD
+ MOZ_ASSERT(args[1].isNumeric());
+#endif
+ MOZ_ASSERT(args[2].isBoolean());
+
+ Rooted<NumberFormatObject*> numberFormat(
+ cx, &args[0].toObject().as<NumberFormatObject>());
+
+ RootedValue value(cx, args[1]);
+#ifdef NIGHTLY_BUILD
+ if (!ToIntlMathematicalValue(cx, &value)) {
+ return false;
+ }
+#endif
+
+ mozilla::intl::NumberFormat* nf = GetOrCreateNumberFormat(cx, numberFormat);
+ if (!nf) {
+ return false;
+ }
+
+ // Actually format the number
+ using ICUError = mozilla::intl::ICUError;
+
+ bool formatToParts = args[2].toBoolean();
+ mozilla::Result<std::u16string_view, ICUError> result =
+ mozilla::Err(ICUError::InternalError);
+ mozilla::intl::NumberPartVector parts;
+ if (value.isNumber()) {
+ double num = value.toNumber();
+ if (formatToParts) {
+ result = nf->formatToParts(num, parts);
+ } else {
+ result = nf->format(num);
+ }
+ } else if (value.isBigInt()) {
+ RootedBigInt bi(cx, value.toBigInt());
+
+ int64_t num;
+ if (BigInt::isInt64(bi, &num)) {
+ if (formatToParts) {
+ result = nf->formatToParts(num, parts);
+ } else {
+ result = nf->format(num);
+ }
+ } else {
+ JSLinearString* str = BigInt::toString<CanGC>(cx, bi, 10);
+ if (!str) {
+ return false;
+ }
+ MOZ_RELEASE_ASSERT(str->hasLatin1Chars());
+
+ JS::AutoCheckCannotGC nogc;
+
+ const char* chars = reinterpret_cast<const char*>(str->latin1Chars(nogc));
+ if (formatToParts) {
+ result =
+ nf->formatToParts(std::string_view(chars, str->length()), parts);
+ } else {
+ result = nf->format(std::string_view(chars, str->length()));
+ }
+ }
+ } else {
+ JSLinearString* str = value.toString()->ensureLinear(cx);
+ if (!str) {
+ return false;
+ }
+
+ JS::AutoCheckCannotGC nogc;
+
+ // Two-byte strings have to be copied into a separate |char| buffer.
+ JS::UniqueChars latin1;
+
+ std::string_view sv;
+ if (!NumberPart(cx, str, nogc, latin1, sv)) {
+ return false;
+ }
+
+ if (formatToParts) {
+ result = nf->formatToParts(sv, parts);
+ } else {
+ result = nf->format(sv);
+ }
+ }
+
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap()));
+ if (!str) {
+ return false;
+ }
+
+ if (formatToParts) {
+ return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No,
+ nullptr, args.rval());
+ }
+
+ args.rval().setString(str);
+ return true;
+}
+
+static JSLinearString* ToLinearString(JSContext* cx, HandleValue val) {
+ // Special case to preserve negative zero.
+ if (val.isDouble() && mozilla::IsNegativeZero(val.toDouble())) {
+ constexpr std::string_view negativeZero = "-0";
+ return NewStringCopy<CanGC>(cx, negativeZero);
+ }
+
+ JSString* str = ToString(cx, val);
+ return str ? str->ensureLinear(cx) : nullptr;
+};
+
+bool js::intl_FormatNumberRange(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 4);
+ MOZ_ASSERT(args[0].isObject());
+ MOZ_ASSERT(!args[1].isUndefined());
+ MOZ_ASSERT(!args[2].isUndefined());
+ MOZ_ASSERT(args[3].isBoolean());
+
+ Rooted<NumberFormatObject*> numberFormat(
+ cx, &args[0].toObject().as<NumberFormatObject>());
+ bool formatToParts = args[3].toBoolean();
+
+ RootedValue start(cx, args[1]);
+ if (!ToIntlMathematicalValue(cx, &start)) {
+ return false;
+ }
+
+ RootedValue end(cx, args[2]);
+ if (!ToIntlMathematicalValue(cx, &end)) {
+ return false;
+ }
+
+ // PartitionNumberRangePattern, step 1.
+ if (start.isDouble() && mozilla::IsNaN(start.toDouble())) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "start",
+ "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange");
+ return false;
+ }
+ if (end.isDouble() && mozilla::IsNaN(end.toDouble())) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "end",
+ "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange");
+ return false;
+ }
+
+ using NumberRangeFormat = mozilla::intl::NumberRangeFormat;
+ NumberRangeFormat* nf = GetOrCreateNumberRangeFormat(cx, numberFormat);
+ if (!nf) {
+ return false;
+ }
+
+ auto valueRepresentableAsDouble = [](const Value& val, double* num) {
+ if (val.isNumber()) {
+ *num = val.toNumber();
+ return true;
+ }
+ if (val.isBigInt()) {
+ int64_t i64;
+ if (BigInt::isInt64(val.toBigInt(), &i64) &&
+ i64 < int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT) &&
+ i64 > -int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) {
+ *num = double(i64);
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // Actually format the number range.
+ using ICUError = mozilla::intl::ICUError;
+
+ mozilla::Result<std::u16string_view, ICUError> result =
+ mozilla::Err(ICUError::InternalError);
+ mozilla::intl::NumberPartVector parts;
+
+ double numStart, numEnd;
+ if (valueRepresentableAsDouble(start, &numStart) &&
+ valueRepresentableAsDouble(end, &numEnd)) {
+ if (formatToParts) {
+ result = nf->formatToParts(numStart, numEnd, parts);
+ } else {
+ result = nf->format(numStart, numEnd);
+ }
+ } else {
+ Rooted<JSLinearString*> strStart(cx, ToLinearString(cx, start));
+ if (!strStart) {
+ return false;
+ }
+
+ Rooted<JSLinearString*> strEnd(cx, ToLinearString(cx, end));
+ if (!strEnd) {
+ return false;
+ }
+
+ JS::AutoCheckCannotGC nogc;
+
+ // Two-byte strings have to be copied into a separate |char| buffer.
+ JS::UniqueChars latin1Start;
+ JS::UniqueChars latin1End;
+
+ std::string_view svStart;
+ if (!NumberPart(cx, strStart, nogc, latin1Start, svStart)) {
+ return false;
+ }
+
+ std::string_view svEnd;
+ if (!NumberPart(cx, strEnd, nogc, latin1End, svEnd)) {
+ return false;
+ }
+
+ if (formatToParts) {
+ result = nf->formatToParts(svStart, svEnd, parts);
+ } else {
+ result = nf->format(svStart, svEnd);
+ }
+ }
+
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap()));
+ if (!str) {
+ return false;
+ }
+
+ if (formatToParts) {
+ return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::Yes,
+ nullptr, args.rval());
+ }
+
+ args.rval().setString(str);
+ return true;
+}
diff --git a/js/src/builtin/intl/NumberFormat.h b/js/src/builtin/intl/NumberFormat.h
new file mode 100644
index 0000000000..a0feb4cb4b
--- /dev/null
+++ b/js/src/builtin/intl/NumberFormat.h
@@ -0,0 +1,129 @@
+/* -*- 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_NumberFormat_h
+#define builtin_intl_NumberFormat_h
+
+#include <stdint.h>
+
+#include "builtin/SelfHostingDefines.h"
+#include "js/Class.h"
+#include "vm/NativeObject.h"
+
+namespace mozilla::intl {
+class NumberFormat;
+class NumberRangeFormat;
+} // namespace mozilla::intl
+
+namespace js {
+
+class NumberFormatObject : public NativeObject {
+ public:
+ static const JSClass class_;
+ static const JSClass& protoClass_;
+
+ static constexpr uint32_t INTERNALS_SLOT = 0;
+ static constexpr uint32_t UNUMBER_FORMATTER_SLOT = 1;
+ static constexpr uint32_t UNUMBER_RANGE_FORMATTER_SLOT = 2;
+ static constexpr uint32_t SLOT_COUNT = 3;
+
+ static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT,
+ "INTERNALS_SLOT must match self-hosting define for internals "
+ "object slot");
+
+ // Estimated memory use for UNumberFormatter and UFormattedNumber
+ // (see IcuMemoryUsage).
+ static constexpr size_t EstimatedMemoryUse = 972;
+
+ // Estimated memory use for UNumberRangeFormatter and UFormattedNumberRange
+ // (see IcuMemoryUsage).
+ static constexpr size_t EstimatedRangeFormatterMemoryUse = 14143;
+
+ mozilla::intl::NumberFormat* getNumberFormatter() const {
+ const auto& slot = getFixedSlot(UNUMBER_FORMATTER_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::NumberFormat*>(slot.toPrivate());
+ }
+
+ void setNumberFormatter(mozilla::intl::NumberFormat* formatter) {
+ setFixedSlot(UNUMBER_FORMATTER_SLOT, PrivateValue(formatter));
+ }
+
+ mozilla::intl::NumberRangeFormat* getNumberRangeFormatter() const {
+ const auto& slot = getFixedSlot(UNUMBER_RANGE_FORMATTER_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::NumberRangeFormat*>(slot.toPrivate());
+ }
+
+ void setNumberRangeFormatter(mozilla::intl::NumberRangeFormat* formatter) {
+ setFixedSlot(UNUMBER_RANGE_FORMATTER_SLOT, PrivateValue(formatter));
+ }
+
+ private:
+ static const JSClassOps classOps_;
+ static const ClassSpec classSpec_;
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+};
+
+/**
+ * Returns a new instance of the standard built-in NumberFormat constructor.
+ * Self-hosted code cannot cache this constructor (as it does for others in
+ * Utilities.js) because it is initialized after self-hosted code is compiled.
+ *
+ * Usage: numberFormat = intl_NumberFormat(locales, options)
+ */
+[[nodiscard]] extern bool intl_NumberFormat(JSContext* cx, unsigned argc,
+ Value* vp);
+
+/**
+ * Returns the numbering system type identifier per Unicode
+ * Technical Standard 35, Unicode Locale Data Markup Language, for the
+ * default numbering system for the given locale.
+ *
+ * Usage: defaultNumberingSystem = intl_numberingSystem(locale)
+ */
+[[nodiscard]] extern bool intl_numberingSystem(JSContext* cx, unsigned argc,
+ Value* vp);
+
+/**
+ * Returns a string representing the number x according to the effective
+ * locale and the formatting options of the given NumberFormat.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 11.3.2.
+ *
+ * Usage: formatted = intl_FormatNumber(numberFormat, x, formatToParts)
+ */
+[[nodiscard]] extern bool intl_FormatNumber(JSContext* cx, unsigned argc,
+ Value* vp);
+
+/**
+ * Returns a string representing the number range «x - y» according to the
+ * effective locale and the formatting options of the given NumberFormat.
+ *
+ * Usage: formatted = intl_FormatNumberRange(numberFormat, x, y, formatToParts)
+ */
+[[nodiscard]] extern bool intl_FormatNumberRange(JSContext* cx, unsigned argc,
+ Value* vp);
+
+#if DEBUG || MOZ_SYSTEM_ICU
+/**
+ * Returns an object with all available measurement units.
+ *
+ * Usage: units = intl_availableMeasurementUnits()
+ */
+[[nodiscard]] extern bool intl_availableMeasurementUnits(JSContext* cx,
+ unsigned argc,
+ Value* vp);
+#endif
+
+} // namespace js
+
+#endif /* builtin_intl_NumberFormat_h */
diff --git a/js/src/builtin/intl/NumberFormat.js b/js/src/builtin/intl/NumberFormat.js
new file mode 100644
index 0000000000..d381fc97a8
--- /dev/null
+++ b/js/src/builtin/intl/NumberFormat.js
@@ -0,0 +1,1210 @@
+/* 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/. */
+
+/* Portions Copyright Norbert Lindenberg 2011-2012. */
+
+#include "NumberingSystemsGenerated.h"
+
+/**
+ * NumberFormat internal properties.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.1 and 11.3.3.
+ */
+var numberFormatInternalProperties = {
+ localeData: numberFormatLocaleData,
+ relevantExtensionKeys: ["nu"],
+};
+
+/**
+ * Compute an internal properties object from |lazyNumberFormatData|.
+ */
+function resolveNumberFormatInternals(lazyNumberFormatData) {
+ assert(IsObject(lazyNumberFormatData), "lazy data not an object?");
+
+ var internalProps = std_Object_create(null);
+
+ var NumberFormat = numberFormatInternalProperties;
+
+ // Compute effective locale.
+
+ // Step 7.
+ var localeData = NumberFormat.localeData;
+
+ // Step 8.
+ var r = ResolveLocale(
+ "NumberFormat",
+ lazyNumberFormatData.requestedLocales,
+ lazyNumberFormatData.opt,
+ NumberFormat.relevantExtensionKeys,
+ localeData
+ );
+
+ // Steps 9-10. (Step 11 is not relevant to our implementation.)
+ internalProps.locale = r.locale;
+ internalProps.numberingSystem = r.nu;
+
+ // Compute formatting options.
+ // Step 13.
+ var style = lazyNumberFormatData.style;
+ internalProps.style = style;
+
+ // Steps 17, 19.
+ if (style === "currency") {
+ internalProps.currency = lazyNumberFormatData.currency;
+ internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay;
+ internalProps.currencySign = lazyNumberFormatData.currencySign;
+ }
+
+ // Intl.NumberFormat Unified API Proposal
+ if (style === "unit") {
+ internalProps.unit = lazyNumberFormatData.unit;
+ internalProps.unitDisplay = lazyNumberFormatData.unitDisplay;
+ }
+
+ // Intl.NumberFormat Unified API Proposal
+ var notation = lazyNumberFormatData.notation;
+ internalProps.notation = notation;
+
+ // Step 22.
+ internalProps.minimumIntegerDigits =
+ lazyNumberFormatData.minimumIntegerDigits;
+
+ if ("minimumFractionDigits" in lazyNumberFormatData) {
+ // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the
+ // actual presence (versus undefined-ness) of these properties.
+ assert(
+ "maximumFractionDigits" in lazyNumberFormatData,
+ "min/max frac digits mismatch"
+ );
+ internalProps.minimumFractionDigits =
+ lazyNumberFormatData.minimumFractionDigits;
+ internalProps.maximumFractionDigits =
+ lazyNumberFormatData.maximumFractionDigits;
+ }
+
+ if ("minimumSignificantDigits" in lazyNumberFormatData) {
+ // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the
+ // actual presence (versus undefined-ness) of these properties.
+ assert(
+ "maximumSignificantDigits" in lazyNumberFormatData,
+ "min/max sig digits mismatch"
+ );
+ internalProps.minimumSignificantDigits =
+ lazyNumberFormatData.minimumSignificantDigits;
+ internalProps.maximumSignificantDigits =
+ lazyNumberFormatData.maximumSignificantDigits;
+ }
+
+ // Intl.NumberFormat v3 Proposal
+ internalProps.trailingZeroDisplay = lazyNumberFormatData.trailingZeroDisplay;
+ internalProps.roundingIncrement = lazyNumberFormatData.roundingIncrement;
+
+ // Intl.NumberFormat Unified API Proposal
+ if (notation === "compact") {
+ internalProps.compactDisplay = lazyNumberFormatData.compactDisplay;
+ }
+
+ // Step 24.
+ internalProps.useGrouping = lazyNumberFormatData.useGrouping;
+
+ // Intl.NumberFormat Unified API Proposal
+ internalProps.signDisplay = lazyNumberFormatData.signDisplay;
+
+ // Intl.NumberFormat v3 Proposal
+ internalProps.roundingMode = lazyNumberFormatData.roundingMode;
+
+ // Intl.NumberFormat v3 Proposal
+ internalProps.roundingPriority = lazyNumberFormatData.roundingPriority;
+
+ // The caller is responsible for associating |internalProps| with the right
+ // object using |setInternalProperties|.
+ return internalProps;
+}
+
+/**
+ * Returns an object containing the NumberFormat internal properties of |obj|.
+ */
+function getNumberFormatInternals(obj) {
+ assert(IsObject(obj), "getNumberFormatInternals called with non-object");
+ assert(
+ intl_GuardToNumberFormat(obj) !== null,
+ "getNumberFormatInternals called with non-NumberFormat"
+ );
+
+ var internals = getIntlObjectInternals(obj);
+ assert(
+ internals.type === "NumberFormat",
+ "bad type escaped getIntlObjectInternals"
+ );
+
+ // If internal properties have already been computed, use them.
+ var internalProps = maybeInternalProperties(internals);
+ if (internalProps) {
+ return internalProps;
+ }
+
+ // Otherwise it's time to fully create them.
+ internalProps = resolveNumberFormatInternals(internals.lazyData);
+ setInternalProperties(internals, internalProps);
+ return internalProps;
+}
+
+/**
+ * 11.1.11 UnwrapNumberFormat( nf )
+ */
+function UnwrapNumberFormat(nf) {
+ // Steps 2 and 4 (error handling moved to caller).
+ if (
+ IsObject(nf) &&
+ intl_GuardToNumberFormat(nf) === null &&
+ !intl_IsWrappedNumberFormat(nf) &&
+ callFunction(
+ std_Object_isPrototypeOf,
+ GetBuiltinPrototype("NumberFormat"),
+ nf
+ )
+ ) {
+ nf = nf[intlFallbackSymbol()];
+ }
+ return nf;
+}
+
+/**
+ * Applies digit options used for number formatting onto the intl object.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 11.1.1.
+ */
+function SetNumberFormatDigitOptions(
+ lazyData,
+ options,
+ mnfdDefault,
+ mxfdDefault,
+ notation
+) {
+ // We skip step 1 because we set the properties on a lazyData object.
+
+ // Steps 2-4.
+ assert(IsObject(options), "SetNumberFormatDigitOptions");
+ assert(typeof mnfdDefault === "number", "SetNumberFormatDigitOptions");
+ assert(typeof mxfdDefault === "number", "SetNumberFormatDigitOptions");
+ assert(mnfdDefault <= mxfdDefault, "SetNumberFormatDigitOptions");
+ assert(typeof notation === "string", "SetNumberFormatDigitOptions");
+
+ // Steps 5-9.
+ const mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1);
+ let mnfd = options.minimumFractionDigits;
+ let mxfd = options.maximumFractionDigits;
+ let mnsd = options.minimumSignificantDigits;
+ let mxsd = options.maximumSignificantDigits;
+
+ // Step 10.
+ lazyData.minimumIntegerDigits = mnid;
+
+#ifdef NIGHTLY_BUILD
+ // Intl.NumberFormat v3 Proposal
+ var roundingPriority = GetOption(
+ options,
+ "roundingPriority",
+ "string",
+ ["auto", "morePrecision", "lessPrecision"],
+ "auto"
+ );
+#else
+ var roundingPriority = "auto";
+#endif
+
+ const hasSignificantDigits = mnsd !== undefined || mxsd !== undefined;
+ const hasFractionDigits = mnfd !== undefined || mxfd !== undefined;
+
+ const needSignificantDigits =
+ roundingPriority !== "auto" || hasSignificantDigits;
+ const needFractionalDigits =
+ roundingPriority !== "auto" ||
+ !(hasSignificantDigits || (!hasFractionDigits && notation === "compact"));
+
+ if (needSignificantDigits) {
+ // Step 11.
+ if (hasSignificantDigits) {
+ // Step 11.a (Omitted).
+
+ // Step 11.b.
+ mnsd = DefaultNumberOption(mnsd, 1, 21, 1);
+
+ // Step 11.c.
+ mxsd = DefaultNumberOption(mxsd, mnsd, 21, 21);
+
+ // Step 11.d.
+ lazyData.minimumSignificantDigits = mnsd;
+
+ // Step 11.e.
+ lazyData.maximumSignificantDigits = mxsd;
+ } else {
+ lazyData.minimumSignificantDigits = 1;
+ lazyData.maximumSignificantDigits = 21;
+ }
+ }
+
+ if (needFractionalDigits) {
+ // Step 12.
+ if (hasFractionDigits) {
+ // Step 12.a (Omitted).
+
+ // Step 12.b.
+ mnfd = DefaultNumberOption(mnfd, 0, 20, undefined);
+
+ // Step 12.c.
+ mxfd = DefaultNumberOption(mxfd, 0, 20, undefined);
+
+ // Step 12.d.
+ if (mnfd === undefined) {
+ assert(
+ mxfd !== undefined,
+ "mxfd isn't undefined when mnfd is undefined"
+ );
+ mnfd = std_Math_min(mnfdDefault, mxfd);
+ }
+
+ // Step 12.e.
+ else if (mxfd === undefined) {
+ mxfd = std_Math_max(mxfdDefault, mnfd);
+ }
+
+ // Step 12.f.
+ else if (mnfd > mxfd) {
+ ThrowRangeError(JSMSG_INVALID_DIGITS_VALUE, mxfd);
+ }
+
+ // Step 12.g.
+ lazyData.minimumFractionDigits = mnfd;
+
+ // Step 12.h.
+ lazyData.maximumFractionDigits = mxfd;
+ } else {
+ // Step 14.a (Omitted).
+
+ // Step 14.b.
+ lazyData.minimumFractionDigits = mnfdDefault;
+
+ // Step 14.c.
+ lazyData.maximumFractionDigits = mxfdDefault;
+ }
+ }
+
+ if (needSignificantDigits || needFractionalDigits) {
+ lazyData.roundingPriority = roundingPriority;
+ } else {
+ assert(!hasSignificantDigits, "bad significant digits in fallback case");
+ assert(
+ roundingPriority === "auto",
+ `bad rounding in fallback case: ${roundingPriority}`
+ );
+ assert(
+ notation === "compact",
+ `bad notation in fallback case: ${notation}`
+ );
+
+ lazyData.roundingPriority = "morePrecision";
+ lazyData.minimumFractionDigits = 0;
+ lazyData.maximumFractionDigits = 0;
+ lazyData.minimumSignificantDigits = 1;
+ lazyData.maximumSignificantDigits = 2;
+ }
+}
+
+/**
+ * Convert s to upper case, but limited to characters a-z.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 6.1.
+ */
+function toASCIIUpperCase(s) {
+ assert(typeof s === "string", "toASCIIUpperCase");
+
+ // String.prototype.toUpperCase may map non-ASCII characters into ASCII,
+ // so go character by character (actually code unit by code unit, but
+ // since we only care about ASCII characters here, that's OK).
+ var result = "";
+ for (var i = 0; i < s.length; i++) {
+ var c = callFunction(std_String_charCodeAt, s, i);
+ result +=
+ 0x61 <= c && c <= 0x7a
+ ? callFunction(std_String_fromCharCode, null, c & ~0x20)
+ : s[i];
+ }
+ return result;
+}
+
+/**
+ * Verifies that the given string is a well-formed ISO 4217 currency code.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 6.3.1.
+ */
+function IsWellFormedCurrencyCode(currency) {
+ assert(typeof currency === "string", "currency is a string value");
+
+ return currency.length === 3 && IsASCIIAlphaString(currency);
+}
+
+/**
+ * Verifies that the given string is a well-formed core unit identifier as
+ * defined in UTS #35, Part 2, Section 6. In addition to obeying the UTS #35
+ * core unit identifier syntax, |unitIdentifier| must be one of the identifiers
+ * sanctioned by UTS #35 or be a compound unit composed of two sanctioned simple
+ * units.
+ *
+ * Intl.NumberFormat Unified API Proposal
+ */
+function IsWellFormedUnitIdentifier(unitIdentifier) {
+ assert(
+ typeof unitIdentifier === "string",
+ "unitIdentifier is a string value"
+ );
+
+ // Step 1.
+ if (IsSanctionedSimpleUnitIdentifier(unitIdentifier)) {
+ return true;
+ }
+
+ // Step 2.
+ var pos = callFunction(std_String_indexOf, unitIdentifier, "-per-");
+ if (pos < 0) {
+ return false;
+ }
+
+ var next = pos + "-per-".length;
+
+ // Steps 3 and 5.
+ var numerator = Substring(unitIdentifier, 0, pos);
+ var denominator = Substring(
+ unitIdentifier,
+ next,
+ unitIdentifier.length - next
+ );
+
+ // Steps 4 and 6.
+ return (
+ IsSanctionedSimpleUnitIdentifier(numerator) &&
+ IsSanctionedSimpleUnitIdentifier(denominator)
+ );
+}
+
+#if DEBUG || MOZ_SYSTEM_ICU
+var availableMeasurementUnits = {
+ value: null,
+};
+#endif
+
+/**
+ * Verifies that the given string is a sanctioned simple core unit identifier.
+ *
+ * Intl.NumberFormat Unified API Proposal
+ *
+ * Also see: https://unicode.org/reports/tr35/tr35-general.html#Unit_Elements
+ */
+function IsSanctionedSimpleUnitIdentifier(unitIdentifier) {
+ assert(
+ typeof unitIdentifier === "string",
+ "unitIdentifier is a string value"
+ );
+
+ var isSanctioned = hasOwn(unitIdentifier, sanctionedSimpleUnitIdentifiers);
+
+#if DEBUG || MOZ_SYSTEM_ICU
+ if (isSanctioned) {
+ if (availableMeasurementUnits.value === null) {
+ availableMeasurementUnits.value = intl_availableMeasurementUnits();
+ }
+
+ var isSupported = hasOwn(unitIdentifier, availableMeasurementUnits.value);
+
+#if MOZ_SYSTEM_ICU
+ // A system ICU may support fewer measurement units, so we need to make
+ // sure the unit is actually supported.
+ isSanctioned = isSupported;
+#else
+ // Otherwise just assert that the sanctioned unit is also supported.
+ assert(
+ isSupported,
+ `"${unitIdentifier}" is sanctioned but not supported. Did you forget to update
+ intl/icu/data_filter.json to include the unit (and any implicit compound units)?
+ For example "speed/kilometer-per-hour" is implied by "length/kilometer" and
+ "duration/hour" and must therefore also be present.`
+ );
+#endif
+ }
+#endif
+
+ return isSanctioned;
+}
+
+/* eslint-disable complexity */
+/**
+ * Initializes an object as a NumberFormat.
+ *
+ * This method is complicated a moderate bit by its implementing initialization
+ * as a *lazy* concept. Everything that must happen now, does -- but we defer
+ * all the work we can until the object is actually used as a NumberFormat.
+ * This later work occurs in |resolveNumberFormatInternals|; steps not noted
+ * here occur there.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 11.1.2.
+ */
+function InitializeNumberFormat(numberFormat, thisValue, locales, options) {
+ assert(
+ IsObject(numberFormat),
+ "InitializeNumberFormat called with non-object"
+ );
+ assert(
+ intl_GuardToNumberFormat(numberFormat) !== null,
+ "InitializeNumberFormat called with non-NumberFormat"
+ );
+
+ // Lazy NumberFormat data has the following structure:
+ //
+ // {
+ // requestedLocales: List of locales,
+ // style: "decimal" / "percent" / "currency" / "unit",
+ //
+ // // fields present only if style === "currency":
+ // currency: a well-formed currency code (IsWellFormedCurrencyCode),
+ // currencyDisplay: "code" / "symbol" / "narrowSymbol" / "name",
+ // currencySign: "standard" / "accounting",
+ //
+ // // fields present only if style === "unit":
+ // unit: a well-formed unit identifier (IsWellFormedUnitIdentifier),
+ // unitDisplay: "short" / "narrow" / "long",
+ //
+ // opt: // opt object computed in InitializeNumberFormat
+ // {
+ // localeMatcher: "lookup" / "best fit",
+ //
+ // nu: string matching a Unicode extension type, // optional
+ // }
+ //
+ // minimumIntegerDigits: integer ∈ [1, 21],
+ //
+ // // optional, mutually exclusive with the significant-digits option
+ // minimumFractionDigits: integer ∈ [0, 20],
+ // maximumFractionDigits: integer ∈ [0, 20],
+ //
+ // // optional, mutually exclusive with the fraction-digits option
+ // minimumSignificantDigits: integer ∈ [1, 21],
+ // maximumSignificantDigits: integer ∈ [1, 21],
+ //
+ // roundingPriority: "auto" / "lessPrecision" / "morePrecision",
+ //
+ // // accepts different values when Intl.NumberFormat v3 proposal is enabled
+ // useGrouping: true / false,
+ // useGrouping: "auto" / "always" / "min2" / false,
+ //
+ // notation: "standard" / "scientific" / "engineering" / "compact",
+ //
+ // // optional, if notation is "compact"
+ // compactDisplay: "short" / "long",
+ //
+ // signDisplay: "auto" / "never" / "always" / "exceptZero",
+ //
+ // trailingZeroDisplay: "auto" / "stripIfInteger",
+ //
+ // roundingIncrement: integer ∈ (1, 2, 5,
+ // 10, 20, 25, 50,
+ // 100, 200, 250, 500,
+ // 1000, 2000, 2500, 5000),
+ //
+ // roundingMode: "ceil" / "floor" / "expand" / "trunc" /
+ // "halfCeil" / "halfFloor" / "halfExpand" / "halfTrunc" / "halfEven",
+ // }
+ //
+ // Note that lazy data is only installed as a final step of initialization,
+ // so every NumberFormat lazy data object has *all* these properties, never a
+ // subset of them.
+ var lazyNumberFormatData = std_Object_create(null);
+
+ // Step 1.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+ lazyNumberFormatData.requestedLocales = requestedLocales;
+
+ // Steps 2-3.
+ //
+ // If we ever need more speed here at startup, we should try to detect the
+ // case where |options === undefined| and then directly use the default
+ // value for each option. For now, just keep it simple.
+ if (options === undefined) {
+ options = std_Object_create(null);
+ } else {
+ options = ToObject(options);
+ }
+
+ // Compute options that impact interpretation of locale.
+ // Step 4.
+ var opt = new_Record();
+ lazyNumberFormatData.opt = opt;
+
+ // Steps 5-6.
+ var matcher = GetOption(
+ options,
+ "localeMatcher",
+ "string",
+ ["lookup", "best fit"],
+ "best fit"
+ );
+ opt.localeMatcher = matcher;
+
+ var numberingSystem = GetOption(
+ options,
+ "numberingSystem",
+ "string",
+ undefined,
+ undefined
+ );
+
+ if (numberingSystem !== undefined) {
+ numberingSystem = intl_ValidateAndCanonicalizeUnicodeExtensionType(
+ numberingSystem,
+ "numberingSystem",
+ "nu"
+ );
+ }
+
+ opt.nu = numberingSystem;
+
+ // Compute formatting options.
+ // Step 12.
+ var style = GetOption(
+ options,
+ "style",
+ "string",
+ ["decimal", "percent", "currency", "unit"],
+ "decimal"
+ );
+ lazyNumberFormatData.style = style;
+
+ // Steps 14-17.
+ var currency = GetOption(options, "currency", "string", undefined, undefined);
+
+ // Per the Intl.NumberFormat Unified API Proposal, this check should only
+ // happen for |style === "currency"|, which seems inconsistent, given that
+ // we normally validate all options when present, even the ones which are
+ // unused.
+ // TODO: File issue at <https://github.com/tc39/proposal-unified-intl-numberformat>.
+ if (currency !== undefined && !IsWellFormedCurrencyCode(currency)) {
+ ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, currency);
+ }
+
+ var cDigits;
+ if (style === "currency") {
+ if (currency === undefined) {
+ ThrowTypeError(JSMSG_UNDEFINED_CURRENCY);
+ }
+
+ // Steps 19.a-c.
+ currency = toASCIIUpperCase(currency);
+ lazyNumberFormatData.currency = currency;
+ cDigits = CurrencyDigits(currency);
+ }
+
+ // Step 18.
+ var currencyDisplay = GetOption(
+ options,
+ "currencyDisplay",
+ "string",
+ ["code", "symbol", "narrowSymbol", "name"],
+ "symbol"
+ );
+ if (style === "currency") {
+ lazyNumberFormatData.currencyDisplay = currencyDisplay;
+ }
+
+ // Intl.NumberFormat Unified API Proposal
+ var currencySign = GetOption(
+ options,
+ "currencySign",
+ "string",
+ ["standard", "accounting"],
+ "standard"
+ );
+ if (style === "currency") {
+ lazyNumberFormatData.currencySign = currencySign;
+ }
+
+ // Intl.NumberFormat Unified API Proposal
+ var unit = GetOption(options, "unit", "string", undefined, undefined);
+
+ // Aligned with |currency| check from above, see note about spec issue there.
+ if (unit !== undefined && !IsWellFormedUnitIdentifier(unit)) {
+ ThrowRangeError(JSMSG_INVALID_UNIT_IDENTIFIER, unit);
+ }
+
+ var unitDisplay = GetOption(
+ options,
+ "unitDisplay",
+ "string",
+ ["short", "narrow", "long"],
+ "short"
+ );
+
+ if (style === "unit") {
+ if (unit === undefined) {
+ ThrowTypeError(JSMSG_UNDEFINED_UNIT);
+ }
+
+ lazyNumberFormatData.unit = unit;
+ lazyNumberFormatData.unitDisplay = unitDisplay;
+ }
+
+ // Steps 20-21.
+ var mnfdDefault, mxfdDefault;
+ if (style === "currency") {
+ mnfdDefault = cDigits;
+ mxfdDefault = cDigits;
+ } else {
+ mnfdDefault = 0;
+ mxfdDefault = style === "percent" ? 0 : 3;
+ }
+
+ // Intl.NumberFormat Unified API Proposal
+ var notation = GetOption(
+ options,
+ "notation",
+ "string",
+ ["standard", "scientific", "engineering", "compact"],
+ "standard"
+ );
+ lazyNumberFormatData.notation = notation;
+
+ // Step 22.
+ SetNumberFormatDigitOptions(
+ lazyNumberFormatData,
+ options,
+ mnfdDefault,
+ mxfdDefault,
+ notation
+ );
+
+#ifdef NIGHTLY_BUILD
+ // Intl.NumberFormat v3 Proposal
+ var roundingIncrement = GetNumberOption(
+ options,
+ "roundingIncrement",
+ 1,
+ 5000,
+ 1
+ );
+ switch (roundingIncrement) {
+ case 1:
+ case 2:
+ case 5:
+ case 10:
+ case 20:
+ case 25:
+ case 50:
+ case 100:
+ case 200:
+ case 250:
+ case 500:
+ case 1000:
+ case 2000:
+ case 2500:
+ case 5000:
+ break;
+ default:
+ ThrowRangeError(
+ JSMSG_INVALID_OPTION_VALUE,
+ "roundingIncrement",
+ roundingIncrement
+ );
+ }
+ lazyNumberFormatData.roundingIncrement = roundingIncrement;
+
+ if (roundingIncrement !== 1) {
+ // [[RoundingType]] must be `fractionDigits`.
+ if (lazyNumberFormatData.roundingPriority !== "auto") {
+ ThrowTypeError(
+ JSMSG_INVALID_NUMBER_OPTION,
+ "roundingIncrement",
+ "roundingPriority"
+ );
+ }
+ if (hasOwn("minimumSignificantDigits", lazyNumberFormatData)) {
+ ThrowTypeError(
+ JSMSG_INVALID_NUMBER_OPTION,
+ "roundingIncrement",
+ "minimumSignificantDigits"
+ );
+ }
+
+ // Minimum and maximum fraction digits must be equal.
+ if (
+ lazyNumberFormatData.minimumFractionDigits !==
+ lazyNumberFormatData.maximumFractionDigits
+ ) {
+ ThrowRangeError(JSMSG_UNEQUAL_FRACTION_DIGITS);
+ }
+ }
+#else
+ lazyNumberFormatData.roundingIncrement = 1;
+#endif
+
+#ifdef NIGHTLY_BUILD
+ // Intl.NumberFormat v3 Proposal
+ var trailingZeroDisplay = GetOption(
+ options,
+ "trailingZeroDisplay",
+ "string",
+ ["auto", "stripIfInteger"],
+ "auto"
+ );
+ lazyNumberFormatData.trailingZeroDisplay = trailingZeroDisplay;
+#else
+ lazyNumberFormatData.trailingZeroDisplay = "auto";
+#endif
+
+ // Intl.NumberFormat Unified API Proposal
+ var compactDisplay = GetOption(
+ options,
+ "compactDisplay",
+ "string",
+ ["short", "long"],
+ "short"
+ );
+ if (notation === "compact") {
+ lazyNumberFormatData.compactDisplay = compactDisplay;
+ }
+
+ // Steps 23.
+#ifdef NIGHTLY_BUILD
+ var defaultUseGrouping = notation !== "compact" ? "auto" : "min2";
+ var useGrouping = GetStringOrBooleanOption(
+ options,
+ "useGrouping",
+ ["min2", "auto", "always"],
+ "always",
+ false,
+ defaultUseGrouping
+ );
+#else
+ var useGrouping = GetOption(
+ options,
+ "useGrouping",
+ "boolean",
+ undefined,
+ true
+ );
+#endif
+ lazyNumberFormatData.useGrouping = useGrouping;
+
+ // Intl.NumberFormat Unified API Proposal
+ var signDisplay = GetOption(
+ options,
+ "signDisplay",
+ "string",
+#ifdef NIGHTLY_BUILD
+ ["auto", "never", "always", "exceptZero", "negative"],
+#else
+ ["auto", "never", "always", "exceptZero"],
+#endif
+ "auto"
+ );
+ lazyNumberFormatData.signDisplay = signDisplay;
+
+#ifdef NIGHTLY_BUILD
+ // Intl.NumberFormat v3 Proposal
+ var roundingMode = GetOption(
+ options,
+ "roundingMode",
+ "string",
+ [
+ "ceil",
+ "floor",
+ "expand",
+ "trunc",
+ "halfCeil",
+ "halfFloor",
+ "halfExpand",
+ "halfTrunc",
+ "halfEven",
+ ],
+ "halfExpand"
+ );
+ lazyNumberFormatData.roundingMode = roundingMode;
+#else
+ lazyNumberFormatData.roundingMode = "halfExpand";
+#endif
+
+ // Step 31.
+ //
+ // We've done everything that must be done now: mark the lazy data as fully
+ // computed and install it.
+ initializeIntlObject(numberFormat, "NumberFormat", lazyNumberFormatData);
+
+ // 11.2.1, steps 4-5.
+ if (
+ numberFormat !== thisValue &&
+ callFunction(
+ std_Object_isPrototypeOf,
+ GetBuiltinPrototype("NumberFormat"),
+ thisValue
+ )
+ ) {
+ DefineDataProperty(
+ thisValue,
+ intlFallbackSymbol(),
+ numberFormat,
+ ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE
+ );
+
+ return thisValue;
+ }
+
+ // 11.2.1, step 6.
+ return numberFormat;
+}
+/* eslint-enable complexity */
+
+/**
+ * Returns the number of decimal digits to be used for the given currency.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 11.1.3.
+ */
+function CurrencyDigits(currency) {
+ assert(typeof currency === "string", "currency is a string value");
+ assert(IsWellFormedCurrencyCode(currency), "currency is well-formed");
+ assert(currency === toASCIIUpperCase(currency), "currency is all upper-case");
+
+ if (hasOwn(currency, currencyDigits)) {
+ return currencyDigits[currency];
+ }
+ return 2;
+}
+
+/**
+ * Returns the subset of the given locale list for which this locale list has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 11.3.2.
+ */
+function Intl_NumberFormat_supportedLocalesOf(locales /*, options*/) {
+ var options = arguments.length > 1 ? arguments[1] : undefined;
+
+ // Step 1.
+ var availableLocales = "NumberFormat";
+
+ // Step 2.
+ var requestedLocales = CanonicalizeLocaleList(locales);
+
+ // Step 3.
+ return SupportedLocales(availableLocales, requestedLocales, options);
+}
+
+function getNumberingSystems(locale) {
+ // ICU doesn't have an API to determine the set of numbering systems
+ // supported for a locale; it generally pretends that any numbering system
+ // can be used with any locale. Supporting a decimal numbering system
+ // (where only the digits are replaced) is easy, so we offer them all here.
+ // Algorithmic numbering systems are typically tied to one locale, so for
+ // lack of information we don't offer them.
+ // The one thing we can find out from ICU is the default numbering system
+ // for a locale.
+ var defaultNumberingSystem = intl_numberingSystem(locale);
+ return [defaultNumberingSystem, NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS];
+}
+
+function numberFormatLocaleData() {
+ return {
+ nu: getNumberingSystems,
+ default: {
+ nu: intl_numberingSystem,
+ },
+ };
+}
+
+/**
+ * Create function to be cached and returned by Intl.NumberFormat.prototype.format.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 11.1.4.
+ */
+function createNumberFormatFormat(nf) {
+ // This function is not inlined in $Intl_NumberFormat_format_get to avoid
+ // creating a call-object on each call to $Intl_NumberFormat_format_get.
+ return function(value) {
+ // Step 1 (implicit).
+
+ // Step 2.
+ assert(IsObject(nf), "InitializeNumberFormat called with non-object");
+ assert(
+ intl_GuardToNumberFormat(nf) !== null,
+ "InitializeNumberFormat called with non-NumberFormat"
+ );
+
+#ifdef NIGHTLY_BUILD
+ var x = value;
+#else
+ // Steps 3-4.
+ var x = ToNumeric(value);
+#endif
+
+ // Step 5.
+ return intl_FormatNumber(nf, x, /* formatToParts = */ false);
+ };
+}
+
+/**
+ * Returns a function bound to this NumberFormat that returns a String value
+ * representing the result of calling ToNumber(value) according to the
+ * effective locale and the formatting options of this NumberFormat.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 11.4.3.
+ */
+// Uncloned functions with `$` prefix are allocated as extended function
+// to store the original name in `SetCanonicalName`.
+function $Intl_NumberFormat_format_get() {
+ // Steps 1-3.
+ var thisArg = UnwrapNumberFormat(this);
+ var nf = thisArg;
+ if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) {
+ return callFunction(
+ intl_CallNumberFormatMethodIfWrapped,
+ thisArg,
+ "$Intl_NumberFormat_format_get"
+ );
+ }
+
+ var internals = getNumberFormatInternals(nf);
+
+ // Step 4.
+ if (internals.boundFormat === undefined) {
+ // Steps 4.a-c.
+ internals.boundFormat = createNumberFormatFormat(nf);
+ }
+
+ // Step 5.
+ return internals.boundFormat;
+}
+SetCanonicalName($Intl_NumberFormat_format_get, "get format");
+
+/**
+ * 11.4.4 Intl.NumberFormat.prototype.formatToParts ( value )
+ */
+function Intl_NumberFormat_formatToParts(value) {
+ // Step 1.
+ var nf = this;
+
+ // Steps 2-3.
+ if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) {
+ return callFunction(
+ intl_CallNumberFormatMethodIfWrapped,
+ this,
+ value,
+ "Intl_NumberFormat_formatToParts"
+ );
+ }
+
+#ifdef NIGHTLY_BUILD
+ var x = value;
+#else
+ // Step 4.
+ var x = ToNumeric(value);
+#endif
+
+ // Step 5.
+ return intl_FormatNumber(nf, x, /* formatToParts = */ true);
+}
+
+/**
+ * Intl.NumberFormat.prototype.formatRange ( start, end )
+ */
+function Intl_NumberFormat_formatRange(start, end) {
+ // Step 1.
+ var nf = this;
+
+ // Step 2.
+ if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) {
+ return callFunction(
+ intl_CallNumberFormatMethodIfWrapped,
+ this,
+ start,
+ end,
+ "Intl_NumberFormat_formatRange"
+ );
+ }
+
+ // Step 3.
+ if (start === undefined || end === undefined) {
+ ThrowTypeError(
+ JSMSG_UNDEFINED_NUMBER,
+ start === undefined ? "start" : "end",
+ "NumberFormat",
+ "formatRange"
+ );
+ }
+
+ // Steps 4-6.
+ return intl_FormatNumberRange(nf, start, end, /* formatToParts = */ false);
+}
+
+/**
+ * Intl.NumberFormat.prototype.formatRangeToParts ( start, end )
+ */
+function Intl_NumberFormat_formatRangeToParts(start, end) {
+ // Step 1.
+ var nf = this;
+
+ // Step 2.
+ if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) {
+ return callFunction(
+ intl_CallNumberFormatMethodIfWrapped,
+ this,
+ start,
+ end,
+ "Intl_NumberFormat_formatRangeToParts"
+ );
+ }
+
+ // Step 3.
+ if (start === undefined || end === undefined) {
+ ThrowTypeError(
+ JSMSG_UNDEFINED_NUMBER,
+ start === undefined ? "start" : "end",
+ "NumberFormat",
+ "formatRangeToParts"
+ );
+ }
+
+ // Steps 4-6.
+ return intl_FormatNumberRange(nf, start, end, /* formatToParts = */ true);
+}
+
+/**
+ * Returns the resolved options for a NumberFormat object.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 11.4.5.
+ */
+function Intl_NumberFormat_resolvedOptions() {
+ // Steps 1-3.
+ var thisArg = UnwrapNumberFormat(this);
+ var nf = thisArg;
+ if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) {
+ return callFunction(
+ intl_CallNumberFormatMethodIfWrapped,
+ thisArg,
+ "Intl_NumberFormat_resolvedOptions"
+ );
+ }
+
+ var internals = getNumberFormatInternals(nf);
+
+ // Steps 4-5.
+ var result = {
+ locale: internals.locale,
+ numberingSystem: internals.numberingSystem,
+ style: internals.style,
+ };
+
+ // currency, currencyDisplay, and currencySign are only present for currency
+ // formatters.
+ assert(
+ hasOwn("currency", internals) === (internals.style === "currency"),
+ "currency is present iff style is 'currency'"
+ );
+ assert(
+ hasOwn("currencyDisplay", internals) === (internals.style === "currency"),
+ "currencyDisplay is present iff style is 'currency'"
+ );
+ assert(
+ hasOwn("currencySign", internals) === (internals.style === "currency"),
+ "currencySign is present iff style is 'currency'"
+ );
+
+ if (hasOwn("currency", internals)) {
+ DefineDataProperty(result, "currency", internals.currency);
+ DefineDataProperty(result, "currencyDisplay", internals.currencyDisplay);
+ DefineDataProperty(result, "currencySign", internals.currencySign);
+ }
+
+ // unit and unitDisplay are only present for unit formatters.
+ assert(
+ hasOwn("unit", internals) === (internals.style === "unit"),
+ "unit is present iff style is 'unit'"
+ );
+ assert(
+ hasOwn("unitDisplay", internals) === (internals.style === "unit"),
+ "unitDisplay is present iff style is 'unit'"
+ );
+
+ if (hasOwn("unit", internals)) {
+ DefineDataProperty(result, "unit", internals.unit);
+ DefineDataProperty(result, "unitDisplay", internals.unitDisplay);
+ }
+
+ DefineDataProperty(
+ result,
+ "minimumIntegerDigits",
+ internals.minimumIntegerDigits
+ );
+
+ // Min/Max fraction digits are either both present or not present at all.
+ assert(
+ hasOwn("minimumFractionDigits", internals) ===
+ hasOwn("maximumFractionDigits", internals),
+ "minimumFractionDigits is present iff maximumFractionDigits is present"
+ );
+
+ if (hasOwn("minimumFractionDigits", internals)) {
+ DefineDataProperty(
+ result,
+ "minimumFractionDigits",
+ internals.minimumFractionDigits
+ );
+ DefineDataProperty(
+ result,
+ "maximumFractionDigits",
+ internals.maximumFractionDigits
+ );
+ }
+
+ // Min/Max significant digits are either both present or not present at all.
+ assert(
+ hasOwn("minimumSignificantDigits", internals) ===
+ hasOwn("maximumSignificantDigits", internals),
+ "minimumSignificantDigits is present iff maximumSignificantDigits is present"
+ );
+
+ if (hasOwn("minimumSignificantDigits", internals)) {
+ DefineDataProperty(
+ result,
+ "minimumSignificantDigits",
+ internals.minimumSignificantDigits
+ );
+ DefineDataProperty(
+ result,
+ "maximumSignificantDigits",
+ internals.maximumSignificantDigits
+ );
+ }
+
+ DefineDataProperty(result, "useGrouping", internals.useGrouping);
+
+ var notation = internals.notation;
+ DefineDataProperty(result, "notation", notation);
+
+ if (notation === "compact") {
+ DefineDataProperty(result, "compactDisplay", internals.compactDisplay);
+ }
+
+ DefineDataProperty(result, "signDisplay", internals.signDisplay);
+
+#ifdef NIGHTLY_BUILD
+ DefineDataProperty(result, "roundingMode", internals.roundingMode);
+ DefineDataProperty(result, "roundingIncrement", internals.roundingIncrement);
+ DefineDataProperty(
+ result,
+ "trailingZeroDisplay",
+ internals.trailingZeroDisplay
+ );
+ DefineDataProperty(result, "roundingPriority", internals.roundingPriority);
+#endif
+
+ // Step 6.
+ return result;
+}
diff --git a/js/src/builtin/intl/NumberingSystems.yaml b/js/src/builtin/intl/NumberingSystems.yaml
new file mode 100644
index 0000000000..db287c10ef
--- /dev/null
+++ b/js/src/builtin/intl/NumberingSystems.yaml
@@ -0,0 +1,82 @@
+# 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/.
+
+# 12.1.7 PartitionNotationSubPattern ( numberFormat, x, n, exponent )
+#
+# Numbering systems with simple digit mappings
+#
+# https://tc39.es/ecma402/#table-numbering-system-digits
+
+# Run |make_intl_data numbering| to regenerate all files which reference this list
+# of numbering systems.
+
+- adlm
+- ahom
+- arab
+- arabext
+- bali
+- beng
+- bhks
+- brah
+- cakm
+- cham
+- deva
+- diak
+- fullwide
+- gong
+- gonm
+- gujr
+- guru
+- hanidec
+- hmng
+- hmnp
+- java
+- kali
+- kawi
+- khmr
+- knda
+- lana
+- lanatham
+- laoo
+- latn
+- lepc
+- limb
+- mathbold
+- mathdbl
+- mathmono
+- mathsanb
+- mathsans
+- mlym
+- modi
+- mong
+- mroo
+- mtei
+- mymr
+- mymrshan
+- mymrtlng
+- nagm
+- newa
+- nkoo
+- olck
+- orya
+- osma
+- rohg
+- saur
+- segment
+- shrd
+- sind
+- sinh
+- sora
+- sund
+- takr
+- talu
+- tamldec
+- telu
+- thai
+- tibt
+- tirh
+- tnsa
+- vaii
+- wara
+- wcho
diff --git a/js/src/builtin/intl/NumberingSystemsGenerated.h b/js/src/builtin/intl/NumberingSystemsGenerated.h
new file mode 100644
index 0000000000..f51d0f9c53
--- /dev/null
+++ b/js/src/builtin/intl/NumberingSystemsGenerated.h
@@ -0,0 +1,83 @@
+// Generated by make_intl_data.py. DO NOT EDIT.
+
+/**
+ * The list of numbering systems with simple digit mappings.
+ */
+
+#ifndef builtin_intl_NumberingSystemsGenerated_h
+#define builtin_intl_NumberingSystemsGenerated_h
+
+// clang-format off
+#define NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS \
+ "adlm", \
+ "ahom", \
+ "arab", \
+ "arabext", \
+ "bali", \
+ "beng", \
+ "bhks", \
+ "brah", \
+ "cakm", \
+ "cham", \
+ "deva", \
+ "diak", \
+ "fullwide", \
+ "gong", \
+ "gonm", \
+ "gujr", \
+ "guru", \
+ "hanidec", \
+ "hmng", \
+ "hmnp", \
+ "java", \
+ "kali", \
+ "kawi", \
+ "khmr", \
+ "knda", \
+ "lana", \
+ "lanatham", \
+ "laoo", \
+ "latn", \
+ "lepc", \
+ "limb", \
+ "mathbold", \
+ "mathdbl", \
+ "mathmono", \
+ "mathsanb", \
+ "mathsans", \
+ "mlym", \
+ "modi", \
+ "mong", \
+ "mroo", \
+ "mtei", \
+ "mymr", \
+ "mymrshan", \
+ "mymrtlng", \
+ "nagm", \
+ "newa", \
+ "nkoo", \
+ "olck", \
+ "orya", \
+ "osma", \
+ "rohg", \
+ "saur", \
+ "segment", \
+ "shrd", \
+ "sind", \
+ "sinh", \
+ "sora", \
+ "sund", \
+ "takr", \
+ "talu", \
+ "tamldec", \
+ "telu", \
+ "thai", \
+ "tibt", \
+ "tirh", \
+ "tnsa", \
+ "vaii", \
+ "wara", \
+ "wcho"
+// clang-format on
+
+#endif // builtin_intl_NumberingSystemsGenerated_h
diff --git a/js/src/builtin/intl/PluralRules.cpp b/js/src/builtin/intl/PluralRules.cpp
new file mode 100644
index 0000000000..b7f9d3c6ca
--- /dev/null
+++ b/js/src/builtin/intl/PluralRules.cpp
@@ -0,0 +1,423 @@
+/* -*- 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/. */
+
+/* Implementation of the Intl.PluralRules proposal. */
+
+#include "builtin/intl/PluralRules.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Casting.h"
+#include "mozilla/intl/PluralRules.h"
+
+#include "builtin/Array.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "gc/GCContext.h"
+#include "js/PropertySpec.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/StringType.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+using mozilla::AssertedCast;
+
+const JSClassOps PluralRulesObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ PluralRulesObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ nullptr, // trace
+};
+
+const JSClass PluralRulesObject::class_ = {
+ "Intl.PluralRules",
+ JSCLASS_HAS_RESERVED_SLOTS(PluralRulesObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_PluralRules) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &PluralRulesObject::classOps_, &PluralRulesObject::classSpec_};
+
+const JSClass& PluralRulesObject::protoClass_ = PlainObject::class_;
+
+static bool pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().PluralRules);
+ return true;
+}
+
+static const JSFunctionSpec pluralRules_static_methods[] = {
+ JS_SELF_HOSTED_FN("supportedLocalesOf",
+ "Intl_PluralRules_supportedLocalesOf", 1, 0),
+ JS_FS_END};
+
+static const JSFunctionSpec pluralRules_methods[] = {
+ JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0,
+ 0),
+ JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0),
+#ifdef NIGHTLY_BUILD
+ JS_SELF_HOSTED_FN("selectRange", "Intl_PluralRules_selectRange", 2, 0),
+#endif
+ JS_FN(js_toSource_str, pluralRules_toSource, 0, 0), JS_FS_END};
+
+static const JSPropertySpec pluralRules_properties[] = {
+ JS_STRING_SYM_PS(toStringTag, "Intl.PluralRules", JSPROP_READONLY),
+ JS_PS_END};
+
+static bool PluralRules(JSContext* cx, unsigned argc, Value* vp);
+
+const ClassSpec PluralRulesObject::classSpec_ = {
+ GenericCreateConstructor<PluralRules, 0, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<PluralRulesObject>,
+ pluralRules_static_methods,
+ nullptr,
+ pluralRules_methods,
+ pluralRules_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+/**
+ * PluralRules constructor.
+ * Spec: ECMAScript 402 API, PluralRules, 13.2.1
+ */
+static bool PluralRules(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // Step 1.
+ if (!ThrowIfNotConstructing(cx, args, "Intl.PluralRules")) {
+ return false;
+ }
+
+ // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+ RootedObject proto(cx);
+ if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PluralRules,
+ &proto)) {
+ return false;
+ }
+
+ Rooted<PluralRulesObject*> pluralRules(cx);
+ pluralRules = NewObjectWithClassProto<PluralRulesObject>(cx, proto);
+ if (!pluralRules) {
+ return false;
+ }
+
+ HandleValue locales = args.get(0);
+ HandleValue options = args.get(1);
+
+ // Step 3.
+ if (!intl::InitializeObject(cx, pluralRules,
+ cx->names().InitializePluralRules, locales,
+ options)) {
+ return false;
+ }
+
+ args.rval().setObject(*pluralRules);
+ return true;
+}
+
+void js::PluralRulesObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ MOZ_ASSERT(gcx->onMainThread());
+
+ auto* pluralRules = &obj->as<PluralRulesObject>();
+ if (mozilla::intl::PluralRules* pr = pluralRules->getPluralRules()) {
+ intl::RemoveICUCellMemory(
+ gcx, obj, PluralRulesObject::UPluralRulesEstimatedMemoryUse);
+ delete pr;
+ }
+}
+
+static JSString* KeywordToString(mozilla::intl::PluralRules::Keyword keyword,
+ JSContext* cx) {
+ using Keyword = mozilla::intl::PluralRules::Keyword;
+ switch (keyword) {
+ case Keyword::Zero: {
+ return cx->names().zero;
+ }
+ case Keyword::One: {
+ return cx->names().one;
+ }
+ case Keyword::Two: {
+ return cx->names().two;
+ }
+ case Keyword::Few: {
+ return cx->names().few;
+ }
+ case Keyword::Many: {
+ return cx->names().many;
+ }
+ case Keyword::Other: {
+ return cx->names().other;
+ }
+ }
+ MOZ_CRASH("Unexpected PluralRules keyword");
+}
+
+/**
+ * Returns a new intl::PluralRules with the locale and type options of the given
+ * PluralRules.
+ */
+static mozilla::intl::PluralRules* NewPluralRules(
+ JSContext* cx, Handle<PluralRulesObject*> pluralRules) {
+ RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules));
+ if (!internals) {
+ return nullptr;
+ }
+
+ RootedValue value(cx);
+
+ if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
+ return nullptr;
+ }
+ UniqueChars locale = intl::EncodeLocale(cx, value.toString());
+ if (!locale) {
+ return nullptr;
+ }
+
+ using PluralRules = mozilla::intl::PluralRules;
+ mozilla::intl::PluralRulesOptions options;
+
+ if (!GetProperty(cx, internals, internals, cx->names().type, &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* type = value.toString()->ensureLinear(cx);
+ if (!type) {
+ return nullptr;
+ }
+
+ if (StringEqualsLiteral(type, "ordinal")) {
+ options.mPluralType = PluralRules::Type::Ordinal;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(type, "cardinal"));
+ options.mPluralType = PluralRules::Type::Cardinal;
+ }
+ }
+
+ bool hasMinimumSignificantDigits;
+ if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits,
+ &hasMinimumSignificantDigits)) {
+ return nullptr;
+ }
+
+ if (hasMinimumSignificantDigits) {
+ if (!GetProperty(cx, internals, internals,
+ cx->names().minimumSignificantDigits, &value)) {
+ return nullptr;
+ }
+ uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
+
+ if (!GetProperty(cx, internals, internals,
+ cx->names().maximumSignificantDigits, &value)) {
+ return nullptr;
+ }
+ uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
+
+ options.mSignificantDigits = mozilla::Some(
+ std::make_pair(minimumSignificantDigits, maximumSignificantDigits));
+ } else {
+ if (!GetProperty(cx, internals, internals,
+ cx->names().minimumFractionDigits, &value)) {
+ return nullptr;
+ }
+ uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
+
+ if (!GetProperty(cx, internals, internals,
+ cx->names().maximumFractionDigits, &value)) {
+ return nullptr;
+ }
+ uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
+
+ options.mFractionDigits = mozilla::Some(
+ std::make_pair(minimumFractionDigits, maximumFractionDigits));
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().roundingPriority,
+ &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* roundingPriority = value.toString()->ensureLinear(cx);
+ if (!roundingPriority) {
+ return nullptr;
+ }
+
+ using RoundingPriority =
+ mozilla::intl::PluralRulesOptions::RoundingPriority;
+
+ RoundingPriority priority;
+ if (StringEqualsLiteral(roundingPriority, "auto")) {
+ priority = RoundingPriority::Auto;
+ } else if (StringEqualsLiteral(roundingPriority, "morePrecision")) {
+ priority = RoundingPriority::MorePrecision;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(roundingPriority, "lessPrecision"));
+ priority = RoundingPriority::LessPrecision;
+ }
+
+ options.mRoundingPriority = priority;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
+ &value)) {
+ return nullptr;
+ }
+ options.mMinIntegerDigits =
+ mozilla::Some(AssertedCast<uint32_t>(value.toInt32()));
+
+ auto result = PluralRules::TryCreate(locale.get(), options);
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+
+ return result.unwrap().release();
+}
+
+static mozilla::intl::PluralRules* GetOrCreatePluralRules(
+ JSContext* cx, Handle<PluralRulesObject*> pluralRules) {
+ // Obtain a cached PluralRules object.
+ mozilla::intl::PluralRules* pr = pluralRules->getPluralRules();
+ if (pr) {
+ return pr;
+ }
+
+ pr = NewPluralRules(cx, pluralRules);
+ if (!pr) {
+ return nullptr;
+ }
+ pluralRules->setPluralRules(pr);
+
+ intl::AddICUCellMemory(pluralRules,
+ PluralRulesObject::UPluralRulesEstimatedMemoryUse);
+ return pr;
+}
+
+bool js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 2);
+
+ Rooted<PluralRulesObject*> pluralRules(
+ cx, &args[0].toObject().as<PluralRulesObject>());
+
+ double x = args[1].toNumber();
+
+ using PluralRules = mozilla::intl::PluralRules;
+ PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules);
+ if (!pr) {
+ return false;
+ }
+
+ auto keywordResult = pr->Select(x);
+ if (keywordResult.isErr()) {
+ intl::ReportInternalError(cx, keywordResult.unwrapErr());
+ return false;
+ }
+
+ JSString* str = KeywordToString(keywordResult.unwrap(), cx);
+ MOZ_ASSERT(str);
+
+ args.rval().setString(str);
+ return true;
+}
+
+/**
+ * ResolvePluralRange ( pluralRules, x, y )
+ * PluralRuleSelectRange ( locale, type, xp, yp )
+ */
+bool js::intl_SelectPluralRuleRange(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+
+ // Steps 1-2.
+ Rooted<PluralRulesObject*> pluralRules(
+ cx, &args[0].toObject().as<PluralRulesObject>());
+
+ // Steps 3-4.
+ double x = args[1].toNumber();
+ double y = args[2].toNumber();
+
+ // Step 5.
+ if (mozilla::IsNaN(x)) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_NAN_NUMBER_RANGE, "start", "PluralRules",
+ "selectRange");
+ return false;
+ }
+ if (mozilla::IsNaN(y)) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_NAN_NUMBER_RANGE, "end", "PluralRules",
+ "selectRange");
+ return false;
+ }
+
+ using PluralRules = mozilla::intl::PluralRules;
+ PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules);
+ if (!pr) {
+ return false;
+ }
+
+ // Steps 6-10.
+ auto keywordResult = pr->SelectRange(x, y);
+ if (keywordResult.isErr()) {
+ intl::ReportInternalError(cx, keywordResult.unwrapErr());
+ return false;
+ }
+
+ JSString* str = KeywordToString(keywordResult.unwrap(), cx);
+ MOZ_ASSERT(str);
+
+ args.rval().setString(str);
+ return true;
+}
+
+bool js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+
+ Rooted<PluralRulesObject*> pluralRules(
+ cx, &args[0].toObject().as<PluralRulesObject>());
+
+ using PluralRules = mozilla::intl::PluralRules;
+ PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules);
+ if (!pr) {
+ return false;
+ }
+
+ auto categoriesResult = pr->Categories();
+ if (categoriesResult.isErr()) {
+ intl::ReportInternalError(cx, categoriesResult.unwrapErr());
+ return false;
+ }
+ auto categories = categoriesResult.unwrap();
+
+ ArrayObject* res = NewDenseFullyAllocatedArray(cx, categories.size());
+ if (!res) {
+ return false;
+ }
+ res->setDenseInitializedLength(categories.size());
+
+ size_t index = 0;
+ for (PluralRules::Keyword keyword : categories) {
+ JSString* str = KeywordToString(keyword, cx);
+ MOZ_ASSERT(str);
+
+ res->initDenseElement(index++, StringValue(str));
+ }
+ MOZ_ASSERT(index == categories.size());
+
+ args.rval().setObject(*res);
+ return true;
+}
diff --git a/js/src/builtin/intl/PluralRules.h b/js/src/builtin/intl/PluralRules.h
new file mode 100644
index 0000000000..86d8ec105d
--- /dev/null
+++ b/js/src/builtin/intl/PluralRules.h
@@ -0,0 +1,98 @@
+/* -*- 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_PluralRules_h
+#define builtin_intl_PluralRules_h
+
+#include "builtin/SelfHostingDefines.h"
+#include "js/Class.h"
+#include "vm/NativeObject.h"
+
+namespace mozilla::intl {
+class PluralRules;
+}
+
+namespace js {
+
+class PluralRulesObject : public NativeObject {
+ public:
+ static const JSClass class_;
+ static const JSClass& protoClass_;
+
+ static constexpr uint32_t INTERNALS_SLOT = 0;
+ static constexpr uint32_t PLURAL_RULES_SLOT = 1;
+ static constexpr uint32_t SLOT_COUNT = 2;
+
+ static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT,
+ "INTERNALS_SLOT must match self-hosting define for internals "
+ "object slot");
+
+ // Estimated memory use for UPluralRules (see IcuMemoryUsage).
+ // Includes usage for UNumberFormat and UNumberRangeFormatter since our
+ // PluralRules implementations contains a NumberFormat and a NumberRangeFormat
+ // object.
+ static constexpr size_t UPluralRulesEstimatedMemoryUse = 5736;
+
+ mozilla::intl::PluralRules* getPluralRules() const {
+ const auto& slot = getFixedSlot(PLURAL_RULES_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::PluralRules*>(slot.toPrivate());
+ }
+
+ void setPluralRules(mozilla::intl::PluralRules* pluralRules) {
+ setFixedSlot(PLURAL_RULES_SLOT, PrivateValue(pluralRules));
+ }
+
+ private:
+ static const JSClassOps classOps_;
+ static const ClassSpec classSpec_;
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+};
+
+/**
+ * Returns a plural rule for the number x according to the effective
+ * locale and the formatting options of the given PluralRules.
+ *
+ * A plural rule is a grammatical category that expresses count distinctions
+ * (such as "one", "two", "few" etc.).
+ *
+ * Usage: rule = intl_SelectPluralRule(pluralRules, x)
+ */
+[[nodiscard]] extern bool intl_SelectPluralRule(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns a plural rule for the number range «x - y» according to the effective
+ * locale and the formatting options of the given PluralRules.
+ *
+ * A plural rule is a grammatical category that expresses count distinctions
+ * (such as "one", "two", "few" etc.).
+ *
+ * Usage: rule = intl_SelectPluralRuleRange(pluralRules, x, y)
+ */
+[[nodiscard]] extern bool intl_SelectPluralRuleRange(JSContext* cx,
+ unsigned argc,
+ JS::Value* vp);
+
+/**
+ * Returns an array of plural rules categories for a given pluralRules object.
+ *
+ * Usage: categories = intl_GetPluralCategories(pluralRules)
+ *
+ * Example:
+ *
+ * pluralRules = new Intl.PluralRules('pl', {type: 'cardinal'});
+ * intl_getPluralCategories(pluralRules); // ['one', 'few', 'many', 'other']
+ */
+[[nodiscard]] extern bool intl_GetPluralCategories(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+} // namespace js
+
+#endif /* builtin_intl_PluralRules_h */
diff --git a/js/src/builtin/intl/PluralRules.js b/js/src/builtin/intl/PluralRules.js
new file mode 100644
index 0000000000..f9d8e310a2
--- /dev/null
+++ b/js/src/builtin/intl/PluralRules.js
@@ -0,0 +1,390 @@
+/* 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/. */
+
+/**
+ * PluralRules internal properties.
+ *
+ * Spec: ECMAScript 402 API, PluralRules, 13.3.3.
+ */
+var pluralRulesInternalProperties = {
+ localeData: pluralRulesLocaleData,
+ relevantExtensionKeys: [],
+};
+
+function pluralRulesLocaleData() {
+ // PluralRules don't support any extension keys.
+ return {};
+}
+
+/**
+ * Compute an internal properties object from |lazyPluralRulesData|.
+ */
+function resolvePluralRulesInternals(lazyPluralRulesData) {
+ assert(IsObject(lazyPluralRulesData), "lazy data not an object?");
+
+ var internalProps = std_Object_create(null);
+
+ var PluralRules = pluralRulesInternalProperties;
+
+ // Compute effective locale.
+
+ // Step 10.
+ var localeData = PluralRules.localeData;
+
+ // Step 11.
+ const r = ResolveLocale(
+ "PluralRules",
+ lazyPluralRulesData.requestedLocales,
+ lazyPluralRulesData.opt,
+ PluralRules.relevantExtensionKeys,
+ localeData
+ );
+
+ // Step 12.
+ internalProps.locale = r.locale;
+
+ // Step 8.
+ internalProps.type = lazyPluralRulesData.type;
+
+ // Step 9.
+ internalProps.minimumIntegerDigits = lazyPluralRulesData.minimumIntegerDigits;
+
+ if ("minimumFractionDigits" in lazyPluralRulesData) {
+ assert(
+ "maximumFractionDigits" in lazyPluralRulesData,
+ "min/max frac digits mismatch"
+ );
+ internalProps.minimumFractionDigits =
+ lazyPluralRulesData.minimumFractionDigits;
+ internalProps.maximumFractionDigits =
+ lazyPluralRulesData.maximumFractionDigits;
+ }
+
+ if ("minimumSignificantDigits" in lazyPluralRulesData) {
+ assert(
+ "maximumSignificantDigits" in lazyPluralRulesData,
+ "min/max sig digits mismatch"
+ );
+ internalProps.minimumSignificantDigits =
+ lazyPluralRulesData.minimumSignificantDigits;
+ internalProps.maximumSignificantDigits =
+ lazyPluralRulesData.maximumSignificantDigits;
+ }
+
+ // Intl.NumberFormat v3 Proposal
+ internalProps.roundingPriority = lazyPluralRulesData.roundingPriority;
+
+ // Step 13 (lazily computed on first access).
+ internalProps.pluralCategories = null;
+
+ return internalProps;
+}
+
+/**
+ * Returns an object containing the PluralRules internal properties of |obj|.
+ */
+function getPluralRulesInternals(obj) {
+ assert(IsObject(obj), "getPluralRulesInternals called with non-object");
+ assert(
+ intl_GuardToPluralRules(obj) !== null,
+ "getPluralRulesInternals called with non-PluralRules"
+ );
+
+ var internals = getIntlObjectInternals(obj);
+ assert(
+ internals.type === "PluralRules",
+ "bad type escaped getIntlObjectInternals"
+ );
+
+ var internalProps = maybeInternalProperties(internals);
+ if (internalProps) {
+ return internalProps;
+ }
+
+ internalProps = resolvePluralRulesInternals(internals.lazyData);
+ setInternalProperties(internals, internalProps);
+ return internalProps;
+}
+
+/**
+ * Initializes an object as a PluralRules.
+ *
+ * This method is complicated a moderate bit by its implementing initialization
+ * as a *lazy* concept. Everything that must happen now, does -- but we defer
+ * all the work we can until the object is actually used as a PluralRules.
+ * This later work occurs in |resolvePluralRulesInternals|; steps not noted
+ * here occur there.
+ *
+ * Spec: ECMAScript 402 API, PluralRules, 13.1.1.
+ */
+function InitializePluralRules(pluralRules, locales, options) {
+ assert(IsObject(pluralRules), "InitializePluralRules called with non-object");
+ assert(
+ intl_GuardToPluralRules(pluralRules) !== null,
+ "InitializePluralRules called with non-PluralRules"
+ );
+
+ // Lazy PluralRules data has the following structure:
+ //
+ // {
+ // requestedLocales: List of locales,
+ // type: "cardinal" / "ordinal",
+ //
+ // opt: // opt object computer in InitializePluralRules
+ // {
+ // localeMatcher: "lookup" / "best fit",
+ // }
+ //
+ // minimumIntegerDigits: integer ∈ [1, 21],
+ //
+ // // optional, mutually exclusive with the significant-digits option
+ // minimumFractionDigits: integer ∈ [0, 20],
+ // maximumFractionDigits: integer ∈ [0, 20],
+ //
+ // // optional, mutually exclusive with the fraction-digits option
+ // minimumSignificantDigits: integer ∈ [1, 21],
+ // maximumSignificantDigits: integer ∈ [1, 21],
+ //
+ // roundingPriority: "auto" / "lessPrecision" / "morePrecision",
+ // }
+ //
+ // Note that lazy data is only installed as a final step of initialization,
+ // so every PluralRules lazy data object has *all* these properties, never a
+ // subset of them.
+ const lazyPluralRulesData = std_Object_create(null);
+
+ // Step 1.
+ let requestedLocales = CanonicalizeLocaleList(locales);
+ lazyPluralRulesData.requestedLocales = requestedLocales;
+
+ // Steps 2-3.
+ if (options === undefined) {
+ options = std_Object_create(null);
+ } else {
+ options = ToObject(options);
+ }
+
+ // Step 4.
+ let opt = new_Record();
+ lazyPluralRulesData.opt = opt;
+
+ // Steps 5-6.
+ let matcher = GetOption(
+ options,
+ "localeMatcher",
+ "string",
+ ["lookup", "best fit"],
+ "best fit"
+ );
+ opt.localeMatcher = matcher;
+
+ // Step 7.
+ const type = GetOption(
+ options,
+ "type",
+ "string",
+ ["cardinal", "ordinal"],
+ "cardinal"
+ );
+ lazyPluralRulesData.type = type;
+
+ // Step 9.
+ SetNumberFormatDigitOptions(lazyPluralRulesData, options, 0, 3, "standard");
+
+ // Step 15.
+ //
+ // We've done everything that must be done now: mark the lazy data as fully
+ // computed and install it.
+ initializeIntlObject(pluralRules, "PluralRules", lazyPluralRulesData);
+}
+
+/**
+ * Returns the subset of the given locale list for which this locale list has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript 402 API, PluralRules, 13.3.2.
+ */
+function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) {
+ var options = arguments.length > 1 ? arguments[1] : undefined;
+
+ // Step 1.
+ var availableLocales = "PluralRules";
+
+ // Step 2.
+ let requestedLocales = CanonicalizeLocaleList(locales);
+
+ // Step 3.
+ return SupportedLocales(availableLocales, requestedLocales, options);
+}
+
+/**
+ * Returns a String value representing the plural category matching
+ * the number passed as value according to the
+ * effective locale and the formatting options of this PluralRules.
+ *
+ * Spec: ECMAScript 402 API, PluralRules, 13.4.3.
+ */
+function Intl_PluralRules_select(value) {
+ // Step 1.
+ let pluralRules = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(pluralRules) ||
+ (pluralRules = intl_GuardToPluralRules(pluralRules)) === null
+ ) {
+ return callFunction(
+ intl_CallPluralRulesMethodIfWrapped,
+ this,
+ value,
+ "Intl_PluralRules_select"
+ );
+ }
+
+ // Step 4.
+ let n = ToNumber(value);
+
+ // Ensure the PluralRules internals are resolved.
+ getPluralRulesInternals(pluralRules);
+
+ // Step 5.
+ return intl_SelectPluralRule(pluralRules, n);
+}
+
+/**
+ * Returns a String value representing the plural category matching the input
+ * number range according to the effective locale and the formatting options
+ * of this PluralRules.
+ */
+function Intl_PluralRules_selectRange(start, end) {
+ // Step 1.
+ var pluralRules = this;
+
+ // Step 2.
+ if (
+ !IsObject(pluralRules) ||
+ (pluralRules = intl_GuardToPluralRules(pluralRules)) === null
+ ) {
+ return callFunction(
+ intl_CallPluralRulesMethodIfWrapped,
+ this,
+ start,
+ end,
+ "Intl_PluralRules_selectRange"
+ );
+ }
+
+ // Step 3.
+ if (start === undefined || end === undefined) {
+ ThrowTypeError(
+ JSMSG_UNDEFINED_NUMBER,
+ start === undefined ? "start" : "end",
+ "PluralRules",
+ "selectRange"
+ );
+ }
+
+ // Step 4.
+ var x = ToNumber(start);
+
+ // Step 5.
+ var y = ToNumber(end);
+
+ // Step 6.
+ return intl_SelectPluralRuleRange(pluralRules, x, y);
+}
+
+/**
+ * Returns the resolved options for a PluralRules object.
+ *
+ * Spec: ECMAScript 402 API, PluralRules, 13.4.4.
+ */
+function Intl_PluralRules_resolvedOptions() {
+ // Step 1.
+ var pluralRules = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(pluralRules) ||
+ (pluralRules = intl_GuardToPluralRules(pluralRules)) === null
+ ) {
+ return callFunction(
+ intl_CallPluralRulesMethodIfWrapped,
+ this,
+ "Intl_PluralRules_resolvedOptions"
+ );
+ }
+
+ var internals = getPluralRulesInternals(pluralRules);
+
+ // Steps 4-5.
+ var result = {
+ locale: internals.locale,
+ type: internals.type,
+ minimumIntegerDigits: internals.minimumIntegerDigits,
+ };
+
+ // Min/Max fraction digits are either both present or not present at all.
+ assert(
+ hasOwn("minimumFractionDigits", internals) ===
+ hasOwn("maximumFractionDigits", internals),
+ "minimumFractionDigits is present iff maximumFractionDigits is present"
+ );
+
+ if (hasOwn("minimumFractionDigits", internals)) {
+ DefineDataProperty(
+ result,
+ "minimumFractionDigits",
+ internals.minimumFractionDigits
+ );
+ DefineDataProperty(
+ result,
+ "maximumFractionDigits",
+ internals.maximumFractionDigits
+ );
+ }
+
+ // Min/Max significant digits are either both present or not present at all.
+ assert(
+ hasOwn("minimumSignificantDigits", internals) ===
+ hasOwn("maximumSignificantDigits", internals),
+ "minimumSignificantDigits is present iff maximumSignificantDigits is present"
+ );
+
+ if (hasOwn("minimumSignificantDigits", internals)) {
+ DefineDataProperty(
+ result,
+ "minimumSignificantDigits",
+ internals.minimumSignificantDigits
+ );
+ DefineDataProperty(
+ result,
+ "maximumSignificantDigits",
+ internals.maximumSignificantDigits
+ );
+ }
+
+ // Step 6.
+ var internalsPluralCategories = internals.pluralCategories;
+ if (internalsPluralCategories === null) {
+ internalsPluralCategories = intl_GetPluralCategories(pluralRules);
+ internals.pluralCategories = internalsPluralCategories;
+ }
+
+ var pluralCategories = [];
+ for (var i = 0; i < internalsPluralCategories.length; i++) {
+ DefineDataProperty(pluralCategories, i, internalsPluralCategories[i]);
+ }
+
+ // Step 7.
+ DefineDataProperty(result, "pluralCategories", pluralCategories);
+
+#ifdef NIGHTLY_BUILD
+ DefineDataProperty(result, "roundingPriority", internals.roundingPriority);
+#endif
+
+ // Step 8.
+ return result;
+}
diff --git a/js/src/builtin/intl/RelativeTimeFormat.cpp b/js/src/builtin/intl/RelativeTimeFormat.cpp
new file mode 100644
index 0000000000..c847fe0e6c
--- /dev/null
+++ b/js/src/builtin/intl/RelativeTimeFormat.cpp
@@ -0,0 +1,403 @@
+/* -*- 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/. */
+
+/* Implementation of the Intl.RelativeTimeFormat proposal. */
+
+#include "builtin/intl/RelativeTimeFormat.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/intl/RelativeTimeFormat.h"
+
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/FormatBuffer.h"
+#include "builtin/intl/LanguageTag.h"
+#include "gc/GCContext.h"
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "js/PropertySpec.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/Printer.h"
+#include "vm/StringType.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+/**************** RelativeTimeFormat *****************/
+
+const JSClassOps RelativeTimeFormatObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ RelativeTimeFormatObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ nullptr, // trace
+};
+
+const JSClass RelativeTimeFormatObject::class_ = {
+ "Intl.RelativeTimeFormat",
+ JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_RelativeTimeFormat) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &RelativeTimeFormatObject::classOps_,
+ &RelativeTimeFormatObject::classSpec_};
+
+const JSClass& RelativeTimeFormatObject::protoClass_ = PlainObject::class_;
+
+static bool relativeTimeFormat_toSource(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().RelativeTimeFormat);
+ return true;
+}
+
+static const JSFunctionSpec relativeTimeFormat_static_methods[] = {
+ JS_SELF_HOSTED_FN("supportedLocalesOf",
+ "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0),
+ JS_FS_END};
+
+static const JSFunctionSpec relativeTimeFormat_methods[] = {
+ JS_SELF_HOSTED_FN("resolvedOptions",
+ "Intl_RelativeTimeFormat_resolvedOptions", 0, 0),
+ JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0),
+ JS_SELF_HOSTED_FN("formatToParts", "Intl_RelativeTimeFormat_formatToParts",
+ 2, 0),
+ JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), JS_FS_END};
+
+static const JSPropertySpec relativeTimeFormat_properties[] = {
+ JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY),
+ JS_PS_END};
+
+static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp);
+
+const ClassSpec RelativeTimeFormatObject::classSpec_ = {
+ GenericCreateConstructor<RelativeTimeFormat, 0, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<RelativeTimeFormatObject>,
+ relativeTimeFormat_static_methods,
+ nullptr,
+ relativeTimeFormat_methods,
+ relativeTimeFormat_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+/**
+ * RelativeTimeFormat constructor.
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1
+ */
+static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // Step 1.
+ if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) {
+ return false;
+ }
+
+ // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+ RootedObject proto(cx);
+ if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_RelativeTimeFormat,
+ &proto)) {
+ return false;
+ }
+
+ Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx);
+ relativeTimeFormat =
+ NewObjectWithClassProto<RelativeTimeFormatObject>(cx, proto);
+ if (!relativeTimeFormat) {
+ return false;
+ }
+
+ HandleValue locales = args.get(0);
+ HandleValue options = args.get(1);
+
+ // Step 3.
+ if (!intl::InitializeObject(cx, relativeTimeFormat,
+ cx->names().InitializeRelativeTimeFormat, locales,
+ options)) {
+ return false;
+ }
+
+ args.rval().setObject(*relativeTimeFormat);
+ return true;
+}
+
+void js::RelativeTimeFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ MOZ_ASSERT(gcx->onMainThread());
+
+ if (mozilla::intl::RelativeTimeFormat* rtf =
+ obj->as<RelativeTimeFormatObject>().getRelativeTimeFormatter()) {
+ intl::RemoveICUCellMemory(gcx, obj,
+ RelativeTimeFormatObject::EstimatedMemoryUse);
+
+ // This was allocated using `new` in mozilla::intl::RelativeTimeFormat,
+ // so we delete here.
+ delete rtf;
+ }
+}
+
+/**
+ * Returns a new URelativeDateTimeFormatter with the locale and options of the
+ * given RelativeTimeFormatObject.
+ */
+static mozilla::intl::RelativeTimeFormat* NewRelativeTimeFormatter(
+ JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) {
+ RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat));
+ if (!internals) {
+ return nullptr;
+ }
+
+ RootedValue value(cx);
+
+ if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
+ return nullptr;
+ }
+
+ // ICU expects numberingSystem as a Unicode locale extensions on locale.
+
+ mozilla::intl::Locale tag;
+ {
+ Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx));
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!intl::ParseLocale(cx, locale, tag)) {
+ return nullptr;
+ }
+ }
+
+ JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
+
+ if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
+ &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
+ if (!numberingSystem) {
+ return nullptr;
+ }
+
+ if (!keywords.emplaceBack("nu", numberingSystem)) {
+ return nullptr;
+ }
+ }
+
+ // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of the
+ // Unicode extension subtag. We're then relying on ICU to follow RFC 6067,
+ // which states that any trailing keywords using the same key should be
+ // ignored.
+ if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
+ return nullptr;
+ }
+
+ intl::FormatBuffer<char> buffer(cx);
+ if (auto result = tag.ToString(buffer); result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+
+ UniqueChars locale = buffer.extractStringZ();
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
+ return nullptr;
+ }
+
+ using RelativeTimeFormatOptions = mozilla::intl::RelativeTimeFormatOptions;
+ RelativeTimeFormatOptions options;
+ {
+ JSLinearString* style = value.toString()->ensureLinear(cx);
+ if (!style) {
+ return nullptr;
+ }
+
+ if (StringEqualsLiteral(style, "short")) {
+ options.style = RelativeTimeFormatOptions::Style::Short;
+ } else if (StringEqualsLiteral(style, "narrow")) {
+ options.style = RelativeTimeFormatOptions::Style::Narrow;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(style, "long"));
+ options.style = RelativeTimeFormatOptions::Style::Long;
+ }
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* numeric = value.toString()->ensureLinear(cx);
+ if (!numeric) {
+ return nullptr;
+ }
+
+ if (StringEqualsLiteral(numeric, "auto")) {
+ options.numeric = RelativeTimeFormatOptions::Numeric::Auto;
+ } else {
+ MOZ_ASSERT(StringEqualsLiteral(numeric, "always"));
+ options.numeric = RelativeTimeFormatOptions::Numeric::Always;
+ }
+ }
+
+ using RelativeTimeFormat = mozilla::intl::RelativeTimeFormat;
+ mozilla::Result<mozilla::UniquePtr<RelativeTimeFormat>,
+ mozilla::intl::ICUError>
+ result = RelativeTimeFormat::TryCreate(locale.get(), options);
+
+ if (result.isOk()) {
+ return result.unwrap().release();
+ }
+
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+}
+
+static mozilla::intl::RelativeTimeFormat* GetOrCreateRelativeTimeFormat(
+ JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) {
+ // Obtain a cached RelativeDateTimeFormatter object.
+ mozilla::intl::RelativeTimeFormat* rtf =
+ relativeTimeFormat->getRelativeTimeFormatter();
+ if (rtf) {
+ return rtf;
+ }
+
+ rtf = NewRelativeTimeFormatter(cx, relativeTimeFormat);
+ if (!rtf) {
+ return nullptr;
+ }
+ relativeTimeFormat->setRelativeTimeFormatter(rtf);
+
+ intl::AddICUCellMemory(relativeTimeFormat,
+ RelativeTimeFormatObject::EstimatedMemoryUse);
+ return rtf;
+}
+
+bool js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 4);
+ MOZ_ASSERT(args[0].isObject());
+ MOZ_ASSERT(args[1].isNumber());
+ MOZ_ASSERT(args[2].isString());
+ MOZ_ASSERT(args[3].isBoolean());
+
+ Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx);
+ relativeTimeFormat = &args[0].toObject().as<RelativeTimeFormatObject>();
+
+ bool formatToParts = args[3].toBoolean();
+
+ // PartitionRelativeTimePattern, step 4.
+ double t = args[1].toNumber();
+ if (!mozilla::IsFinite(t)) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat",
+ formatToParts ? "formatToParts" : "format");
+ return false;
+ }
+
+ mozilla::intl::RelativeTimeFormat* rtf =
+ GetOrCreateRelativeTimeFormat(cx, relativeTimeFormat);
+ if (!rtf) {
+ return false;
+ }
+
+ intl::FieldType jsUnitType;
+ using FormatUnit = mozilla::intl::RelativeTimeFormat::FormatUnit;
+ FormatUnit relTimeUnit;
+ {
+ JSLinearString* unit = args[2].toString()->ensureLinear(cx);
+ if (!unit) {
+ return false;
+ }
+
+ // PartitionRelativeTimePattern, step 5.
+ if (StringEqualsLiteral(unit, "second") ||
+ StringEqualsLiteral(unit, "seconds")) {
+ jsUnitType = &JSAtomState::second;
+ relTimeUnit = FormatUnit::Second;
+ } else if (StringEqualsLiteral(unit, "minute") ||
+ StringEqualsLiteral(unit, "minutes")) {
+ jsUnitType = &JSAtomState::minute;
+ relTimeUnit = FormatUnit::Minute;
+ } else if (StringEqualsLiteral(unit, "hour") ||
+ StringEqualsLiteral(unit, "hours")) {
+ jsUnitType = &JSAtomState::hour;
+ relTimeUnit = FormatUnit::Hour;
+ } else if (StringEqualsLiteral(unit, "day") ||
+ StringEqualsLiteral(unit, "days")) {
+ jsUnitType = &JSAtomState::day;
+ relTimeUnit = FormatUnit::Day;
+ } else if (StringEqualsLiteral(unit, "week") ||
+ StringEqualsLiteral(unit, "weeks")) {
+ jsUnitType = &JSAtomState::week;
+ relTimeUnit = FormatUnit::Week;
+ } else if (StringEqualsLiteral(unit, "month") ||
+ StringEqualsLiteral(unit, "months")) {
+ jsUnitType = &JSAtomState::month;
+ relTimeUnit = FormatUnit::Month;
+ } else if (StringEqualsLiteral(unit, "quarter") ||
+ StringEqualsLiteral(unit, "quarters")) {
+ jsUnitType = &JSAtomState::quarter;
+ relTimeUnit = FormatUnit::Quarter;
+ } else if (StringEqualsLiteral(unit, "year") ||
+ StringEqualsLiteral(unit, "years")) {
+ jsUnitType = &JSAtomState::year;
+ relTimeUnit = FormatUnit::Year;
+ } else {
+ if (auto unitChars = QuoteString(cx, unit, '"')) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INVALID_OPTION_VALUE, "unit",
+ unitChars.get());
+ }
+ return false;
+ }
+ }
+
+ using ICUError = mozilla::intl::ICUError;
+ if (formatToParts) {
+ mozilla::intl::NumberPartVector parts;
+ mozilla::Result<mozilla::Span<const char16_t>, ICUError> result =
+ rtf->formatToParts(t, relTimeUnit, parts);
+
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap()));
+ if (!str) {
+ return false;
+ }
+
+ return js::intl::FormattedRelativeTimeToParts(cx, str, parts, jsUnitType,
+ args.rval());
+ }
+
+ js::intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
+ mozilla::Result<Ok, ICUError> result = rtf->format(t, relTimeUnit, buffer);
+
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return false;
+ }
+
+ JSString* str = buffer.toString(cx);
+ if (!str) {
+ return false;
+ }
+
+ args.rval().setString(str);
+ return true;
+}
diff --git a/js/src/builtin/intl/RelativeTimeFormat.h b/js/src/builtin/intl/RelativeTimeFormat.h
new file mode 100644
index 0000000000..f46aaa217b
--- /dev/null
+++ b/js/src/builtin/intl/RelativeTimeFormat.h
@@ -0,0 +1,87 @@
+/* -*- 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_RelativeTimeFormat_h
+#define builtin_intl_RelativeTimeFormat_h
+
+#include "mozilla/intl/NumberPart.h"
+
+#include <stdint.h>
+
+#include "builtin/SelfHostingDefines.h"
+#include "gc/Barrier.h"
+#include "js/Class.h"
+#include "vm/NativeObject.h"
+
+namespace mozilla::intl {
+class RelativeTimeFormat;
+}
+
+namespace js {
+
+class RelativeTimeFormatObject : public NativeObject {
+ public:
+ static const JSClass class_;
+ static const JSClass& protoClass_;
+
+ static constexpr uint32_t INTERNALS_SLOT = 0;
+ static constexpr uint32_t URELATIVE_TIME_FORMAT_SLOT = 1;
+ static constexpr uint32_t SLOT_COUNT = 2;
+
+ static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT,
+ "INTERNALS_SLOT must match self-hosting define for internals "
+ "object slot");
+
+ // Estimated memory use for URelativeDateTimeFormatter (see IcuMemoryUsage).
+ static constexpr size_t EstimatedMemoryUse = 10896;
+
+ mozilla::intl::RelativeTimeFormat* getRelativeTimeFormatter() const {
+ const auto& slot = getFixedSlot(URELATIVE_TIME_FORMAT_SLOT);
+ if (slot.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<mozilla::intl::RelativeTimeFormat*>(slot.toPrivate());
+ }
+
+ void setRelativeTimeFormatter(mozilla::intl::RelativeTimeFormat* rtf) {
+ setFixedSlot(URELATIVE_TIME_FORMAT_SLOT, PrivateValue(rtf));
+ }
+
+ private:
+ static const JSClassOps classOps_;
+ static const ClassSpec classSpec_;
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+};
+
+/**
+ * Returns a relative time as a string formatted according to the effective
+ * locale and the formatting options of the given RelativeTimeFormat.
+ *
+ * |t| should be a number representing a number to be formatted.
+ * |unit| should be "second", "minute", "hour", "day", "week", "month",
+ * "quarter", or "year".
+ * |numeric| should be "always" or "auto".
+ *
+ * Usage: formatted = intl_FormatRelativeTime(relativeTimeFormat, t,
+ * unit, numeric, formatToParts)
+ */
+[[nodiscard]] extern bool intl_FormatRelativeTime(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+namespace intl {
+
+using FieldType = js::ImmutableTenuredPtr<PropertyName*> JSAtomState::*;
+
+[[nodiscard]] bool FormattedRelativeTimeToParts(
+ JSContext* cx, HandleString str,
+ const mozilla::intl::NumberPartVector& parts, FieldType relativeTimeUnit,
+ MutableHandleValue result);
+
+} // namespace intl
+} // namespace js
+
+#endif /* builtin_intl_RelativeTimeFormat_h */
diff --git a/js/src/builtin/intl/RelativeTimeFormat.js b/js/src/builtin/intl/RelativeTimeFormat.js
new file mode 100644
index 0000000000..5b23650e73
--- /dev/null
+++ b/js/src/builtin/intl/RelativeTimeFormat.js
@@ -0,0 +1,329 @@
+/* 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/. */
+
+/**
+ * RelativeTimeFormat internal properties.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.3.
+ */
+var relativeTimeFormatInternalProperties = {
+ localeData: relativeTimeFormatLocaleData,
+ relevantExtensionKeys: ["nu"],
+};
+
+function relativeTimeFormatLocaleData() {
+ return {
+ nu: getNumberingSystems,
+ default: {
+ nu: intl_numberingSystem,
+ },
+ };
+}
+
+/**
+ * Compute an internal properties object from |lazyRelativeTimeFormatData|.
+ */
+function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) {
+ assert(IsObject(lazyRelativeTimeFormatData), "lazy data not an object?");
+
+ var internalProps = std_Object_create(null);
+
+ var RelativeTimeFormat = relativeTimeFormatInternalProperties;
+
+ // Steps 10-11.
+ const r = ResolveLocale(
+ "RelativeTimeFormat",
+ lazyRelativeTimeFormatData.requestedLocales,
+ lazyRelativeTimeFormatData.opt,
+ RelativeTimeFormat.relevantExtensionKeys,
+ RelativeTimeFormat.localeData
+ );
+
+ // Steps 12-13.
+ internalProps.locale = r.locale;
+
+ // Step 14.
+ internalProps.numberingSystem = r.nu;
+
+ // Step 15 (Not relevant in our implementation).
+
+ // Step 17.
+ internalProps.style = lazyRelativeTimeFormatData.style;
+
+ // Step 19.
+ internalProps.numeric = lazyRelativeTimeFormatData.numeric;
+
+ // Steps 20-24 (Not relevant in our implementation).
+
+ return internalProps;
+}
+
+/**
+ * Returns an object containing the RelativeTimeFormat internal properties of |obj|.
+ */
+function getRelativeTimeFormatInternals(obj) {
+ assert(
+ IsObject(obj),
+ "getRelativeTimeFormatInternals called with non-object"
+ );
+ assert(
+ intl_GuardToRelativeTimeFormat(obj) !== null,
+ "getRelativeTimeFormatInternals called with non-RelativeTimeFormat"
+ );
+
+ var internals = getIntlObjectInternals(obj);
+ assert(
+ internals.type === "RelativeTimeFormat",
+ "bad type escaped getIntlObjectInternals"
+ );
+
+ var internalProps = maybeInternalProperties(internals);
+ if (internalProps) {
+ return internalProps;
+ }
+
+ internalProps = resolveRelativeTimeFormatInternals(internals.lazyData);
+ setInternalProperties(internals, internalProps);
+ return internalProps;
+}
+
+/**
+ * Initializes an object as a RelativeTimeFormat.
+ *
+ * This method is complicated a moderate bit by its implementing initialization
+ * as a *lazy* concept. Everything that must happen now, does -- but we defer
+ * all the work we can until the object is actually used as a RelativeTimeFormat.
+ * This later work occurs in |resolveRelativeTimeFormatInternals|; steps not noted
+ * here occur there.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1.1.
+ */
+function InitializeRelativeTimeFormat(relativeTimeFormat, locales, options) {
+ assert(
+ IsObject(relativeTimeFormat),
+ "InitializeRelativeimeFormat called with non-object"
+ );
+ assert(
+ intl_GuardToRelativeTimeFormat(relativeTimeFormat) !== null,
+ "InitializeRelativeTimeFormat called with non-RelativeTimeFormat"
+ );
+
+ // Lazy RelativeTimeFormat data has the following structure:
+ //
+ // {
+ // requestedLocales: List of locales,
+ // style: "long" / "short" / "narrow",
+ // numeric: "always" / "auto",
+ //
+ // opt: // opt object computed in InitializeRelativeTimeFormat
+ // {
+ // localeMatcher: "lookup" / "best fit",
+ // }
+ // }
+ //
+ // Note that lazy data is only installed as a final step of initialization,
+ // so every RelativeTimeFormat lazy data object has *all* these properties, never a
+ // subset of them.
+ const lazyRelativeTimeFormatData = std_Object_create(null);
+
+ // Step 1.
+ let requestedLocales = CanonicalizeLocaleList(locales);
+ lazyRelativeTimeFormatData.requestedLocales = requestedLocales;
+
+ // Steps 2-3.
+ if (options === undefined) {
+ options = std_Object_create(null);
+ } else {
+ options = ToObject(options);
+ }
+
+ // Step 4.
+ let opt = new_Record();
+
+ // Steps 5-6.
+ let matcher = GetOption(
+ options,
+ "localeMatcher",
+ "string",
+ ["lookup", "best fit"],
+ "best fit"
+ );
+ opt.localeMatcher = matcher;
+
+ // Steps 7-9.
+ let numberingSystem = GetOption(
+ options,
+ "numberingSystem",
+ "string",
+ undefined,
+ undefined
+ );
+ if (numberingSystem !== undefined) {
+ numberingSystem = intl_ValidateAndCanonicalizeUnicodeExtensionType(
+ numberingSystem,
+ "numberingSystem",
+ "nu"
+ );
+ }
+ opt.nu = numberingSystem;
+
+ lazyRelativeTimeFormatData.opt = opt;
+
+ // Steps 16-17.
+ const style = GetOption(
+ options,
+ "style",
+ "string",
+ ["long", "short", "narrow"],
+ "long"
+ );
+ lazyRelativeTimeFormatData.style = style;
+
+ // Steps 18-19.
+ const numeric = GetOption(
+ options,
+ "numeric",
+ "string",
+ ["always", "auto"],
+ "always"
+ );
+ lazyRelativeTimeFormatData.numeric = numeric;
+
+ initializeIntlObject(
+ relativeTimeFormat,
+ "RelativeTimeFormat",
+ lazyRelativeTimeFormatData
+ );
+}
+
+/**
+ * Returns the subset of the given locale list for which this locale list has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.2.
+ */
+function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) {
+ var options = arguments.length > 1 ? arguments[1] : undefined;
+
+ // Step 1.
+ var availableLocales = "RelativeTimeFormat";
+
+ // Step 2.
+ let requestedLocales = CanonicalizeLocaleList(locales);
+
+ // Step 3.
+ return SupportedLocales(availableLocales, requestedLocales, options);
+}
+
+/**
+ * Returns a String value representing the written form of a relative date
+ * formatted according to the effective locale and the formatting options
+ * of this RelativeTimeFormat object.
+ *
+ * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.3.
+ */
+function Intl_RelativeTimeFormat_format(value, unit) {
+ // Step 1.
+ let relativeTimeFormat = this;
+
+ // Step 2.
+ if (
+ !IsObject(relativeTimeFormat) ||
+ (relativeTimeFormat = intl_GuardToRelativeTimeFormat(
+ relativeTimeFormat
+ )) === null
+ ) {
+ return callFunction(
+ intl_CallRelativeTimeFormatMethodIfWrapped,
+ this,
+ value,
+ unit,
+ "Intl_RelativeTimeFormat_format"
+ );
+ }
+
+ // Step 3.
+ let t = ToNumber(value);
+
+ // Step 4.
+ let u = ToString(unit);
+
+ // Step 5.
+ return intl_FormatRelativeTime(relativeTimeFormat, t, u, false);
+}
+
+/**
+ * Returns an Array composed of the components of a relative date formatted
+ * according to the effective locale and the formatting options of this
+ * RelativeTimeFormat object.
+ *
+ * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.4.
+ */
+function Intl_RelativeTimeFormat_formatToParts(value, unit) {
+ // Step 1.
+ let relativeTimeFormat = this;
+
+ // Step 2.
+ if (
+ !IsObject(relativeTimeFormat) ||
+ (relativeTimeFormat = intl_GuardToRelativeTimeFormat(
+ relativeTimeFormat
+ )) === null
+ ) {
+ return callFunction(
+ intl_CallRelativeTimeFormatMethodIfWrapped,
+ this,
+ value,
+ unit,
+ "Intl_RelativeTimeFormat_formatToParts"
+ );
+ }
+
+ // Step 3.
+ let t = ToNumber(value);
+
+ // Step 4.
+ let u = ToString(unit);
+
+ // Step 5.
+ return intl_FormatRelativeTime(relativeTimeFormat, t, u, true);
+}
+
+/**
+ * Returns the resolved options for a RelativeTimeFormat object.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.4.5.
+ */
+function Intl_RelativeTimeFormat_resolvedOptions() {
+ // Step 1.
+ var relativeTimeFormat = this;
+
+ // Steps 2-3.
+ if (
+ !IsObject(relativeTimeFormat) ||
+ (relativeTimeFormat = intl_GuardToRelativeTimeFormat(
+ relativeTimeFormat
+ )) === null
+ ) {
+ return callFunction(
+ intl_CallRelativeTimeFormatMethodIfWrapped,
+ this,
+ "Intl_RelativeTimeFormat_resolvedOptions"
+ );
+ }
+
+ var internals = getRelativeTimeFormatInternals(relativeTimeFormat);
+
+ // Steps 4-5.
+ var result = {
+ locale: internals.locale,
+ style: internals.style,
+ numeric: internals.numeric,
+ numberingSystem: internals.numberingSystem,
+ };
+
+ // Step 6.
+ return result;
+}
diff --git a/js/src/builtin/intl/SanctionedSimpleUnitIdentifiers.yaml b/js/src/builtin/intl/SanctionedSimpleUnitIdentifiers.yaml
new file mode 100644
index 0000000000..97cb44c12c
--- /dev/null
+++ b/js/src/builtin/intl/SanctionedSimpleUnitIdentifiers.yaml
@@ -0,0 +1,58 @@
+# 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/.
+
+# 6.5.2 IsSanctionedSimpleUnitIdentifier ( unitIdentifier )
+#
+# Simple units sanctioned for use in ECMAScript
+#
+# https://tc39.es/ecma402/#table-sanctioned-simple-unit-identifiers
+
+# Run |make_intl_data units| to regenerate all files which reference this list
+# of sanctioned unit identifiers.
+
+- acre
+- bit
+- byte
+- celsius
+- centimeter
+- day
+- degree
+- fahrenheit
+- fluid-ounce
+- foot
+- gallon
+- gigabit
+- gigabyte
+- gram
+- hectare
+- hour
+- inch
+- kilobit
+- kilobyte
+- kilogram
+- kilometer
+- liter
+- megabit
+- megabyte
+- meter
+- microsecond
+- mile
+- mile-scandinavian
+- milliliter
+- millimeter
+- millisecond
+- minute
+- month
+- nanosecond
+- ounce
+- percent
+- petabyte
+- pound
+- second
+- stone
+- terabit
+- terabyte
+- week
+- yard
+- year
diff --git a/js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js b/js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js
new file mode 100644
index 0000000000..7f762910ee
--- /dev/null
+++ b/js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js
@@ -0,0 +1,56 @@
+// Generated by make_intl_data.py. DO NOT EDIT.
+
+/**
+ * The list of currently supported simple unit identifiers.
+ *
+ * Intl.NumberFormat Unified API Proposal
+ */
+/* eslint-disable prettier/prettier */
+var sanctionedSimpleUnitIdentifiers = {
+ "acre": true,
+ "bit": true,
+ "byte": true,
+ "celsius": true,
+ "centimeter": true,
+ "day": true,
+ "degree": true,
+ "fahrenheit": true,
+ "fluid-ounce": true,
+ "foot": true,
+ "gallon": true,
+ "gigabit": true,
+ "gigabyte": true,
+ "gram": true,
+ "hectare": true,
+ "hour": true,
+ "inch": true,
+ "kilobit": true,
+ "kilobyte": true,
+ "kilogram": true,
+ "kilometer": true,
+ "liter": true,
+ "megabit": true,
+ "megabyte": true,
+ "meter": true,
+ "microsecond": true,
+ "mile": true,
+ "mile-scandinavian": true,
+ "milliliter": true,
+ "millimeter": true,
+ "millisecond": true,
+ "minute": true,
+ "month": true,
+ "nanosecond": true,
+ "ounce": true,
+ "percent": true,
+ "petabyte": true,
+ "pound": true,
+ "second": true,
+ "stone": true,
+ "terabit": true,
+ "terabyte": true,
+ "week": true,
+ "yard": true,
+ "year": true
+};
+/* eslint-enable prettier/prettier */
diff --git a/js/src/builtin/intl/SharedIntlData.cpp b/js/src/builtin/intl/SharedIntlData.cpp
new file mode 100644
index 0000000000..18e87b6399
--- /dev/null
+++ b/js/src/builtin/intl/SharedIntlData.cpp
@@ -0,0 +1,754 @@
+/* -*- 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/. */
+
+/* Runtime-wide Intl data shared across compartments. */
+
+#include "builtin/intl/SharedIntlData.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/intl/Collator.h"
+#include "mozilla/intl/DateTimeFormat.h"
+#include "mozilla/intl/DateTimePatternGenerator.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/intl/NumberFormat.h"
+#include "mozilla/intl/TimeZone.h"
+#include "mozilla/Span.h"
+#include "mozilla/TextUtils.h"
+
+#include <algorithm>
+#include <stdint.h>
+#include <string>
+#include <string.h>
+#include <string_view>
+#include <utility>
+
+#include "builtin/Array.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/TimeZoneDataGenerated.h"
+#include "js/Utility.h"
+#include "js/Vector.h"
+#include "vm/ArrayObject.h"
+#include "vm/JSAtom.h"
+#include "vm/JSContext.h"
+#include "vm/StringType.h"
+
+using js::HashNumber;
+
+template <typename Char>
+static constexpr Char ToUpperASCII(Char c) {
+ return mozilla::IsAsciiLowercaseAlpha(c) ? (c - 0x20) : c;
+}
+
+static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly");
+static_assert(ToUpperASCII('m') == 'M', "verifying 'm' uppercases correctly");
+static_assert(ToUpperASCII('z') == 'Z', "verifying 'z' uppercases correctly");
+static_assert(ToUpperASCII(u'a') == u'A',
+ "verifying u'a' uppercases correctly");
+static_assert(ToUpperASCII(u'k') == u'K',
+ "verifying u'k' uppercases correctly");
+static_assert(ToUpperASCII(u'z') == u'Z',
+ "verifying u'z' uppercases correctly");
+
+template <typename Char>
+static HashNumber HashStringIgnoreCaseASCII(const Char* s, size_t length) {
+ uint32_t hash = 0;
+ for (size_t i = 0; i < length; i++) {
+ hash = mozilla::AddToHash(hash, ToUpperASCII(s[i]));
+ }
+ return hash;
+}
+
+js::intl::SharedIntlData::TimeZoneHasher::Lookup::Lookup(
+ JSLinearString* timeZone)
+ : js::intl::SharedIntlData::LinearStringLookup(timeZone) {
+ if (isLatin1) {
+ hash = HashStringIgnoreCaseASCII(latin1Chars, length);
+ } else {
+ hash = HashStringIgnoreCaseASCII(twoByteChars, length);
+ }
+}
+
+template <typename Char1, typename Char2>
+static bool EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2,
+ size_t len) {
+ for (const Char1* s1end = s1 + len; s1 < s1end; s1++, s2++) {
+ if (ToUpperASCII(*s1) != ToUpperASCII(*s2)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool js::intl::SharedIntlData::TimeZoneHasher::match(TimeZoneName key,
+ const Lookup& lookup) {
+ if (key->length() != lookup.length) {
+ return false;
+ }
+
+ // Compare time zone names ignoring ASCII case differences.
+ if (key->hasLatin1Chars()) {
+ const Latin1Char* keyChars = key->latin1Chars(lookup.nogc);
+ if (lookup.isLatin1) {
+ return EqualCharsIgnoreCaseASCII(keyChars, lookup.latin1Chars,
+ lookup.length);
+ }
+ return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars,
+ lookup.length);
+ }
+
+ const char16_t* keyChars = key->twoByteChars(lookup.nogc);
+ if (lookup.isLatin1) {
+ return EqualCharsIgnoreCaseASCII(lookup.latin1Chars, keyChars,
+ lookup.length);
+ }
+ return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars,
+ lookup.length);
+}
+
+static bool IsLegacyICUTimeZone(mozilla::Span<const char> timeZone) {
+ std::string_view timeZoneView(timeZone.data(), timeZone.size());
+ for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) {
+ if (timeZoneView == legacyTimeZone) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool js::intl::SharedIntlData::ensureTimeZones(JSContext* cx) {
+ if (timeZoneDataInitialized) {
+ return true;
+ }
+
+ // If ensureTimeZones() was called previously, but didn't complete due to
+ // OOM, clear all sets/maps and start from scratch.
+ availableTimeZones.clearAndCompact();
+
+ auto timeZones = mozilla::intl::TimeZone::GetAvailableTimeZones();
+ if (timeZones.isErr()) {
+ ReportInternalError(cx, timeZones.unwrapErr());
+ return false;
+ }
+
+ Rooted<JSAtom*> timeZone(cx);
+ for (auto timeZoneName : timeZones.unwrap()) {
+ if (timeZoneName.isErr()) {
+ ReportInternalError(cx);
+ return false;
+ }
+ auto timeZoneSpan = timeZoneName.unwrap();
+
+ // Skip legacy ICU time zone names.
+ if (IsLegacyICUTimeZone(timeZoneSpan)) {
+ continue;
+ }
+
+ timeZone = Atomize(cx, timeZoneSpan.data(), timeZoneSpan.size());
+ if (!timeZone) {
+ return false;
+ }
+
+ TimeZoneHasher::Lookup lookup(timeZone);
+ TimeZoneSet::AddPtr p = availableTimeZones.lookupForAdd(lookup);
+
+ // ICU shouldn't report any duplicate time zone names, but if it does,
+ // just ignore the duplicate name.
+ if (!p && !availableTimeZones.add(p, timeZone)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+ }
+
+ ianaZonesTreatedAsLinksByICU.clearAndCompact();
+
+ for (const char* rawTimeZone : timezone::ianaZonesTreatedAsLinksByICU) {
+ MOZ_ASSERT(rawTimeZone != nullptr);
+ timeZone = Atomize(cx, rawTimeZone, strlen(rawTimeZone));
+ if (!timeZone) {
+ return false;
+ }
+
+ TimeZoneHasher::Lookup lookup(timeZone);
+ TimeZoneSet::AddPtr p = ianaZonesTreatedAsLinksByICU.lookupForAdd(lookup);
+ MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaZonesTreatedAsLinksByICU");
+
+ if (!ianaZonesTreatedAsLinksByICU.add(p, timeZone)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+ }
+
+ ianaLinksCanonicalizedDifferentlyByICU.clearAndCompact();
+
+ Rooted<JSAtom*> linkName(cx);
+ Rooted<JSAtom*>& target = timeZone;
+ for (const auto& linkAndTarget :
+ timezone::ianaLinksCanonicalizedDifferentlyByICU) {
+ const char* rawLinkName = linkAndTarget.link;
+ const char* rawTarget = linkAndTarget.target;
+
+ MOZ_ASSERT(rawLinkName != nullptr);
+ linkName = Atomize(cx, rawLinkName, strlen(rawLinkName));
+ if (!linkName) {
+ return false;
+ }
+
+ MOZ_ASSERT(rawTarget != nullptr);
+ target = Atomize(cx, rawTarget, strlen(rawTarget));
+ if (!target) {
+ return false;
+ }
+
+ TimeZoneHasher::Lookup lookup(linkName);
+ TimeZoneMap::AddPtr p =
+ ianaLinksCanonicalizedDifferentlyByICU.lookupForAdd(lookup);
+ MOZ_ASSERT(
+ !p,
+ "Duplicate entry in timezone::ianaLinksCanonicalizedDifferentlyByICU");
+
+ if (!ianaLinksCanonicalizedDifferentlyByICU.add(p, linkName, target)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+ }
+
+ MOZ_ASSERT(!timeZoneDataInitialized,
+ "ensureTimeZones is neither reentrant nor thread-safe");
+ timeZoneDataInitialized = true;
+
+ return true;
+}
+
+bool js::intl::SharedIntlData::validateTimeZoneName(
+ JSContext* cx, HandleString timeZone, MutableHandle<JSAtom*> result) {
+ if (!ensureTimeZones(cx)) {
+ return false;
+ }
+
+ Rooted<JSLinearString*> timeZoneLinear(cx, timeZone->ensureLinear(cx));
+ if (!timeZoneLinear) {
+ return false;
+ }
+
+ TimeZoneHasher::Lookup lookup(timeZoneLinear);
+ if (TimeZoneSet::Ptr p = availableTimeZones.lookup(lookup)) {
+ result.set(*p);
+ }
+
+ return true;
+}
+
+bool js::intl::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA(
+ JSContext* cx, HandleString timeZone, MutableHandle<JSAtom*> result) {
+ if (!ensureTimeZones(cx)) {
+ return false;
+ }
+
+ Rooted<JSLinearString*> timeZoneLinear(cx, timeZone->ensureLinear(cx));
+ if (!timeZoneLinear) {
+ return false;
+ }
+
+ TimeZoneHasher::Lookup lookup(timeZoneLinear);
+ MOZ_ASSERT(availableTimeZones.has(lookup), "Invalid time zone name");
+
+ if (TimeZoneMap::Ptr p =
+ ianaLinksCanonicalizedDifferentlyByICU.lookup(lookup)) {
+ // The effectively supported time zones aren't known at compile time,
+ // when
+ // 1. SpiderMonkey was compiled with "--with-system-icu".
+ // 2. ICU's dynamic time zone data loading feature was used.
+ // (ICU supports loading time zone files at runtime through the
+ // ICU_TIMEZONE_FILES_DIR environment variable.)
+ // Ensure ICU supports the new target zone before applying the update.
+ TimeZoneName targetTimeZone = p->value();
+ TimeZoneHasher::Lookup targetLookup(targetTimeZone);
+ if (availableTimeZones.has(targetLookup)) {
+ result.set(targetTimeZone);
+ }
+ } else if (TimeZoneSet::Ptr p = ianaZonesTreatedAsLinksByICU.lookup(lookup)) {
+ result.set(*p);
+ }
+
+ return true;
+}
+
+JS::Result<js::intl::SharedIntlData::TimeZoneSet::Iterator>
+js::intl::SharedIntlData::availableTimeZonesIteration(JSContext* cx) {
+ if (!ensureTimeZones(cx)) {
+ return cx->alreadyReportedError();
+ }
+ return availableTimeZones.iter();
+}
+
+js::intl::SharedIntlData::LocaleHasher::Lookup::Lookup(JSLinearString* locale)
+ : js::intl::SharedIntlData::LinearStringLookup(locale) {
+ if (isLatin1) {
+ hash = mozilla::HashString(latin1Chars, length);
+ } else {
+ hash = mozilla::HashString(twoByteChars, length);
+ }
+}
+
+js::intl::SharedIntlData::LocaleHasher::Lookup::Lookup(const char* chars,
+ size_t length)
+ : js::intl::SharedIntlData::LinearStringLookup(chars, length) {
+ hash = mozilla::HashString(latin1Chars, length);
+}
+
+bool js::intl::SharedIntlData::LocaleHasher::match(Locale key,
+ const Lookup& lookup) {
+ if (key->length() != lookup.length) {
+ return false;
+ }
+
+ if (key->hasLatin1Chars()) {
+ const Latin1Char* keyChars = key->latin1Chars(lookup.nogc);
+ if (lookup.isLatin1) {
+ return EqualChars(keyChars, lookup.latin1Chars, lookup.length);
+ }
+ return EqualChars(keyChars, lookup.twoByteChars, lookup.length);
+ }
+
+ const char16_t* keyChars = key->twoByteChars(lookup.nogc);
+ if (lookup.isLatin1) {
+ return EqualChars(lookup.latin1Chars, keyChars, lookup.length);
+ }
+ return EqualChars(keyChars, lookup.twoByteChars, lookup.length);
+}
+
+template <class AvailableLocales>
+bool js::intl::SharedIntlData::getAvailableLocales(
+ JSContext* cx, LocaleSet& locales,
+ const AvailableLocales& availableLocales) {
+ auto addLocale = [cx, &locales](const char* locale, size_t length) {
+ JSAtom* atom = Atomize(cx, locale, length);
+ if (!atom) {
+ return false;
+ }
+
+ LocaleHasher::Lookup lookup(atom);
+ LocaleSet::AddPtr p = locales.lookupForAdd(lookup);
+
+ // ICU shouldn't report any duplicate locales, but if it does, just
+ // ignore the duplicated locale.
+ if (!p && !locales.add(p, atom)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ return true;
+ };
+
+ js::Vector<char, 16> lang(cx);
+
+ for (const char* locale : availableLocales) {
+ size_t length = strlen(locale);
+
+ lang.clear();
+ if (!lang.append(locale, length)) {
+ return false;
+ }
+ MOZ_ASSERT(lang.length() == length);
+
+ std::replace(lang.begin(), lang.end(), '_', '-');
+
+ if (!addLocale(lang.begin(), length)) {
+ return false;
+ }
+
+ // From <https://tc39.es/ecma402/#sec-internal-slots>:
+ //
+ // For locales that include a script subtag in addition to language and
+ // region, the corresponding locale without a script subtag must also be
+ // supported; that is, if an implementation recognizes "zh-Hant-TW", it is
+ // also expected to recognize "zh-TW".
+
+ // 2 * Alpha language subtag
+ // + 1 separator
+ // + 4 * Alphanum script subtag
+ // + 1 separator
+ // + 2 * Alpha region subtag
+ using namespace mozilla::intl::LanguageTagLimits;
+ static constexpr size_t MinLanguageLength = 2;
+ static constexpr size_t MinLengthForScriptAndRegion =
+ MinLanguageLength + 1 + ScriptLength + 1 + AlphaRegionLength;
+
+ // Fast case: Skip locales without script subtags.
+ if (length < MinLengthForScriptAndRegion) {
+ continue;
+ }
+
+ // We don't need the full-fledged language tag parser when we just want to
+ // remove the script subtag.
+
+ // Find the separator between the language and script subtags.
+ const char* sep = std::char_traits<char>::find(lang.begin(), length, '-');
+ if (!sep) {
+ continue;
+ }
+
+ // Possible |script| subtag start position.
+ const char* script = sep + 1;
+
+ // Find the separator between the script and region subtags.
+ sep = std::char_traits<char>::find(script, lang.end() - script, '-');
+ if (!sep) {
+ continue;
+ }
+
+ // Continue with the next locale if we didn't find a script subtag.
+ size_t scriptLength = sep - script;
+ if (!mozilla::intl::IsStructurallyValidScriptTag<char>(
+ {script, scriptLength})) {
+ continue;
+ }
+
+ // Possible |region| subtag start position.
+ const char* region = sep + 1;
+
+ // Search if there's yet another subtag after the region subtag.
+ sep = std::char_traits<char>::find(region, lang.end() - region, '-');
+
+ // Continue with the next locale if we didn't find a region subtag.
+ size_t regionLength = (sep ? sep : lang.end()) - region;
+ if (!mozilla::intl::IsStructurallyValidRegionTag<char>(
+ {region, regionLength})) {
+ continue;
+ }
+
+ // We've found a script and a region subtag.
+
+ static constexpr size_t ScriptWithSeparatorLength = ScriptLength + 1;
+
+ // Remove the script subtag. Note: erase() needs non-const pointers, which
+ // means we can't directly pass |script|.
+ char* p = const_cast<char*>(script);
+ lang.erase(p, p + ScriptWithSeparatorLength);
+
+ MOZ_ASSERT(lang.length() == length - ScriptWithSeparatorLength);
+
+ // Add the locale with the script subtag removed.
+ if (!addLocale(lang.begin(), lang.length())) {
+ return false;
+ }
+ }
+
+ // Forcibly add an entry for the last-ditch locale, in case ICU doesn't
+ // directly support it (but does support it through fallback, e.g. supporting
+ // "en-GB" indirectly using "en" support).
+ {
+ const char* lastDitch = intl::LastDitchLocale();
+ MOZ_ASSERT(strcmp(lastDitch, "en-GB") == 0);
+
+#ifdef DEBUG
+ static constexpr char lastDitchParent[] = "en";
+
+ LocaleHasher::Lookup lookup(lastDitchParent, strlen(lastDitchParent));
+ MOZ_ASSERT(locales.has(lookup),
+ "shouldn't be a need to add every locale implied by the "
+ "last-ditch locale, merely just the last-ditch locale");
+#endif
+
+ if (!addLocale(lastDitch, strlen(lastDitch))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+#ifdef DEBUG
+template <class AvailableLocales1, class AvailableLocales2>
+static bool IsSameAvailableLocales(const AvailableLocales1& availableLocales1,
+ const AvailableLocales2& availableLocales2) {
+ return std::equal(std::begin(availableLocales1), std::end(availableLocales1),
+ std::begin(availableLocales2), std::end(availableLocales2),
+ [](const char* a, const char* b) {
+ // Intentionally comparing pointer equivalence.
+ return a == b;
+ });
+}
+#endif
+
+bool js::intl::SharedIntlData::ensureSupportedLocales(JSContext* cx) {
+ if (supportedLocalesInitialized) {
+ return true;
+ }
+
+ // If ensureSupportedLocales() was called previously, but didn't complete due
+ // to OOM, clear all data and start from scratch.
+ supportedLocales.clearAndCompact();
+ collatorSupportedLocales.clearAndCompact();
+
+ if (!getAvailableLocales(cx, supportedLocales,
+ mozilla::intl::Locale::GetAvailableLocales())) {
+ return false;
+ }
+ if (!getAvailableLocales(cx, collatorSupportedLocales,
+ mozilla::intl::Collator::GetAvailableLocales())) {
+ return false;
+ }
+
+ MOZ_ASSERT(IsSameAvailableLocales(
+ mozilla::intl::Locale::GetAvailableLocales(),
+ mozilla::intl::DateTimeFormat::GetAvailableLocales()));
+
+ MOZ_ASSERT(IsSameAvailableLocales(
+ mozilla::intl::Locale::GetAvailableLocales(),
+ mozilla::intl::NumberFormat::GetAvailableLocales()));
+
+ MOZ_ASSERT(!supportedLocalesInitialized,
+ "ensureSupportedLocales is neither reentrant nor thread-safe");
+ supportedLocalesInitialized = true;
+
+ return true;
+}
+
+bool js::intl::SharedIntlData::isSupportedLocale(JSContext* cx,
+ SupportedLocaleKind kind,
+ HandleString locale,
+ bool* supported) {
+ if (!ensureSupportedLocales(cx)) {
+ return false;
+ }
+
+ Rooted<JSLinearString*> localeLinear(cx, locale->ensureLinear(cx));
+ if (!localeLinear) {
+ return false;
+ }
+
+ LocaleHasher::Lookup lookup(localeLinear);
+
+ switch (kind) {
+ case SupportedLocaleKind::Collator:
+ *supported = collatorSupportedLocales.has(lookup);
+ return true;
+ case SupportedLocaleKind::DateTimeFormat:
+ case SupportedLocaleKind::DisplayNames:
+ case SupportedLocaleKind::ListFormat:
+ case SupportedLocaleKind::NumberFormat:
+ case SupportedLocaleKind::PluralRules:
+ case SupportedLocaleKind::RelativeTimeFormat:
+ *supported = supportedLocales.has(lookup);
+ return true;
+ }
+ MOZ_CRASH("Invalid Intl constructor");
+}
+
+js::ArrayObject* js::intl::SharedIntlData::availableLocalesOf(
+ JSContext* cx, SupportedLocaleKind kind) {
+ if (!ensureSupportedLocales(cx)) {
+ return nullptr;
+ }
+
+ LocaleSet* localeSet = nullptr;
+ switch (kind) {
+ case SupportedLocaleKind::Collator:
+ localeSet = &collatorSupportedLocales;
+ break;
+ case SupportedLocaleKind::DateTimeFormat:
+ case SupportedLocaleKind::DisplayNames:
+ case SupportedLocaleKind::ListFormat:
+ case SupportedLocaleKind::NumberFormat:
+ case SupportedLocaleKind::PluralRules:
+ case SupportedLocaleKind::RelativeTimeFormat:
+ localeSet = &supportedLocales;
+ break;
+ default:
+ MOZ_CRASH("Invalid Intl constructor");
+ }
+
+ const uint32_t count = localeSet->count();
+ ArrayObject* result = NewDenseFullyAllocatedArray(cx, count);
+ if (!result) {
+ return nullptr;
+ }
+ result->setDenseInitializedLength(count);
+
+ uint32_t index = 0;
+ for (auto range = localeSet->iter(); !range.done(); range.next()) {
+ JSAtom* locale = range.get();
+ cx->markAtom(locale);
+
+ result->initDenseElement(index++, StringValue(locale));
+ }
+ MOZ_ASSERT(index == count);
+
+ return result;
+}
+
+#if DEBUG || MOZ_SYSTEM_ICU
+bool js::intl::SharedIntlData::ensureUpperCaseFirstLocales(JSContext* cx) {
+ if (upperCaseFirstInitialized) {
+ return true;
+ }
+
+ // If ensureUpperCaseFirstLocales() was called previously, but didn't
+ // complete due to OOM, clear all data and start from scratch.
+ upperCaseFirstLocales.clearAndCompact();
+
+ Rooted<JSAtom*> locale(cx);
+ for (const char* rawLocale : mozilla::intl::Collator::GetAvailableLocales()) {
+ auto collator = mozilla::intl::Collator::TryCreate(rawLocale);
+ if (collator.isErr()) {
+ ReportInternalError(cx, collator.unwrapErr());
+ return false;
+ }
+
+ auto caseFirst = collator.unwrap()->GetCaseFirst();
+ if (caseFirst.isErr()) {
+ ReportInternalError(cx, caseFirst.unwrapErr());
+ return false;
+ }
+
+ if (caseFirst.unwrap() != mozilla::intl::Collator::CaseFirst::Upper) {
+ continue;
+ }
+
+ locale = Atomize(cx, rawLocale, strlen(rawLocale));
+ if (!locale) {
+ return false;
+ }
+
+ LocaleHasher::Lookup lookup(locale);
+ LocaleSet::AddPtr p = upperCaseFirstLocales.lookupForAdd(lookup);
+
+ // ICU shouldn't report any duplicate locales, but if it does, just
+ // ignore the duplicated locale.
+ if (!p && !upperCaseFirstLocales.add(p, locale)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+ }
+
+ MOZ_ASSERT(
+ !upperCaseFirstInitialized,
+ "ensureUpperCaseFirstLocales is neither reentrant nor thread-safe");
+ upperCaseFirstInitialized = true;
+
+ return true;
+}
+#endif // DEBUG || MOZ_SYSTEM_ICU
+
+bool js::intl::SharedIntlData::isUpperCaseFirst(JSContext* cx,
+ HandleString locale,
+ bool* isUpperFirst) {
+#if DEBUG || MOZ_SYSTEM_ICU
+ if (!ensureUpperCaseFirstLocales(cx)) {
+ return false;
+ }
+#endif
+
+ Rooted<JSLinearString*> localeLinear(cx, locale->ensureLinear(cx));
+ if (!localeLinear) {
+ return false;
+ }
+
+#if !MOZ_SYSTEM_ICU
+ // "da" (Danish) and "mt" (Maltese) are the only two supported locales using
+ // upper-case first. CLDR also lists "cu" (Church Slavic) as an upper-case
+ // first locale, but since it's not supported in ICU, we don't care about it
+ // here.
+ bool isDefaultUpperCaseFirstLocale =
+ js::StringEqualsLiteral(localeLinear, "da") ||
+ js::StringEqualsLiteral(localeLinear, "mt");
+#endif
+
+#if DEBUG || MOZ_SYSTEM_ICU
+ LocaleHasher::Lookup lookup(localeLinear);
+ *isUpperFirst = upperCaseFirstLocales.has(lookup);
+#else
+ *isUpperFirst = isDefaultUpperCaseFirstLocale;
+#endif
+
+#if !MOZ_SYSTEM_ICU
+ MOZ_ASSERT(*isUpperFirst == isDefaultUpperCaseFirstLocale,
+ "upper-case first locales don't match hard-coded list");
+#endif
+
+ return true;
+}
+
+void js::intl::DateTimePatternGeneratorDeleter::operator()(
+ mozilla::intl::DateTimePatternGenerator* ptr) {
+ delete ptr;
+}
+
+static bool StringsAreEqual(const char* s1, const char* s2) {
+ return !strcmp(s1, s2);
+}
+
+mozilla::intl::DateTimePatternGenerator*
+js::intl::SharedIntlData::getDateTimePatternGenerator(JSContext* cx,
+ const char* locale) {
+ // Return the cached instance if the requested locale matches the locale
+ // of the cached generator.
+ if (dateTimePatternGeneratorLocale &&
+ StringsAreEqual(dateTimePatternGeneratorLocale.get(), locale)) {
+ return dateTimePatternGenerator.get();
+ }
+
+ auto result = mozilla::intl::DateTimePatternGenerator::TryCreate(locale);
+ if (result.isErr()) {
+ intl::ReportInternalError(cx, result.unwrapErr());
+ return nullptr;
+ }
+ // The UniquePtr needs to be recreated as it's using a different Deleter in
+ // order to be able to forward declare DateTimePatternGenerator in
+ // SharedIntlData.h.
+ UniqueDateTimePatternGenerator gen(result.unwrap().release());
+
+ JS::UniqueChars localeCopy = js::DuplicateString(cx, locale);
+ if (!localeCopy) {
+ return nullptr;
+ }
+
+ dateTimePatternGenerator = std::move(gen);
+ dateTimePatternGeneratorLocale = std::move(localeCopy);
+
+ return dateTimePatternGenerator.get();
+}
+
+void js::intl::SharedIntlData::destroyInstance() {
+ availableTimeZones.clearAndCompact();
+ ianaZonesTreatedAsLinksByICU.clearAndCompact();
+ ianaLinksCanonicalizedDifferentlyByICU.clearAndCompact();
+ supportedLocales.clearAndCompact();
+ collatorSupportedLocales.clearAndCompact();
+#if DEBUG || MOZ_SYSTEM_ICU
+ upperCaseFirstLocales.clearAndCompact();
+#endif
+}
+
+void js::intl::SharedIntlData::trace(JSTracer* trc) {
+ // Atoms are always tenured.
+ if (!JS::RuntimeHeapIsMinorCollecting()) {
+ availableTimeZones.trace(trc);
+ ianaZonesTreatedAsLinksByICU.trace(trc);
+ ianaLinksCanonicalizedDifferentlyByICU.trace(trc);
+ supportedLocales.trace(trc);
+ collatorSupportedLocales.trace(trc);
+#if DEBUG || MOZ_SYSTEM_ICU
+ upperCaseFirstLocales.trace(trc);
+#endif
+ }
+}
+
+size_t js::intl::SharedIntlData::sizeOfExcludingThis(
+ mozilla::MallocSizeOf mallocSizeOf) const {
+ return availableTimeZones.shallowSizeOfExcludingThis(mallocSizeOf) +
+ ianaZonesTreatedAsLinksByICU.shallowSizeOfExcludingThis(mallocSizeOf) +
+ ianaLinksCanonicalizedDifferentlyByICU.shallowSizeOfExcludingThis(
+ mallocSizeOf) +
+ supportedLocales.shallowSizeOfExcludingThis(mallocSizeOf) +
+ collatorSupportedLocales.shallowSizeOfExcludingThis(mallocSizeOf) +
+#if DEBUG || MOZ_SYSTEM_ICU
+ upperCaseFirstLocales.shallowSizeOfExcludingThis(mallocSizeOf) +
+#endif
+ mallocSizeOf(dateTimePatternGeneratorLocale.get());
+}
diff --git a/js/src/builtin/intl/SharedIntlData.h b/js/src/builtin/intl/SharedIntlData.h
new file mode 100644
index 0000000000..8cada7c61b
--- /dev/null
+++ b/js/src/builtin/intl/SharedIntlData.h
@@ -0,0 +1,335 @@
+/* -*- 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_SharedIntlData_h
+#define builtin_intl_SharedIntlData_h
+
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/UniquePtr.h"
+
+#include <stddef.h>
+
+#include "js/AllocPolicy.h"
+#include "js/GCAPI.h"
+#include "js/GCHashTable.h"
+#include "js/Result.h"
+#include "js/RootingAPI.h"
+#include "js/Utility.h"
+#include "vm/StringType.h"
+
+namespace mozilla::intl {
+class DateTimePatternGenerator;
+} // namespace mozilla::intl
+
+namespace js {
+
+class ArrayObject;
+
+namespace intl {
+
+/**
+ * This deleter class exists so that mozilla::intl::DateTimePatternGenerator
+ * can be a forward declaration, but still be used inside of a UniquePtr.
+ */
+class DateTimePatternGeneratorDeleter {
+ public:
+ void operator()(mozilla::intl::DateTimePatternGenerator* ptr);
+};
+
+/**
+ * Stores Intl data which can be shared across compartments (but not contexts).
+ *
+ * Used for data which is expensive when computed repeatedly or is not
+ * available through ICU.
+ */
+class SharedIntlData {
+ struct LinearStringLookup {
+ union {
+ const JS::Latin1Char* latin1Chars;
+ const char16_t* twoByteChars;
+ };
+ bool isLatin1;
+ size_t length;
+ JS::AutoCheckCannotGC nogc;
+ HashNumber hash = 0;
+
+ explicit LinearStringLookup(JSLinearString* string)
+ : isLatin1(string->hasLatin1Chars()), length(string->length()) {
+ if (isLatin1) {
+ latin1Chars = string->latin1Chars(nogc);
+ } else {
+ twoByteChars = string->twoByteChars(nogc);
+ }
+ }
+
+ LinearStringLookup(const char* chars, size_t length)
+ : isLatin1(true), length(length) {
+ latin1Chars = reinterpret_cast<const JS::Latin1Char*>(chars);
+ }
+ };
+
+ public:
+ /**
+ * Information tracking the set of the supported time zone names, derived
+ * from the IANA time zone database <https://www.iana.org/time-zones>.
+ *
+ * There are two kinds of IANA time zone names: Zone and Link (denoted as
+ * such in database source files). Zone names are the canonical, preferred
+ * name for a time zone, e.g. Asia/Kolkata. Link names simply refer to
+ * target Zone names for their meaning, e.g. Asia/Calcutta targets
+ * Asia/Kolkata. That a name is a Link doesn't *necessarily* reflect a
+ * sense of deprecation: some Link names also exist partly for convenience,
+ * e.g. UTC and GMT as Link names targeting the Zone name Etc/UTC.
+ *
+ * Two data sources determine the time zone names we support: those ICU
+ * supports and IANA's zone information.
+ *
+ * Unfortunately the names ICU and IANA support, and their Link
+ * relationships from name to target, aren't identical, so we can't simply
+ * implicitly trust ICU's name handling. We must perform various
+ * preprocessing of user-provided zone names and post-processing of
+ * ICU-provided zone names to implement ECMA-402's IANA-consistent behavior.
+ *
+ * Also see <https://ssl.icu-project.org/trac/ticket/12044> and
+ * <http://unicode.org/cldr/trac/ticket/9892>.
+ */
+
+ using TimeZoneName = JSAtom*;
+
+ struct TimeZoneHasher {
+ struct Lookup : LinearStringLookup {
+ explicit Lookup(JSLinearString* timeZone);
+ };
+
+ static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; }
+ static bool match(TimeZoneName key, const Lookup& lookup);
+ };
+
+ using TimeZoneSet =
+ GCHashSet<TimeZoneName, TimeZoneHasher, SystemAllocPolicy>;
+ using TimeZoneMap =
+ GCHashMap<TimeZoneName, TimeZoneName, TimeZoneHasher, SystemAllocPolicy>;
+
+ private:
+ /**
+ * As a threshold matter, available time zones are those time zones ICU
+ * supports, via ucal_openTimeZones. But ICU supports additional non-IANA
+ * time zones described in intl/icu/source/tools/tzcode/icuzones (listed in
+ * IntlTimeZoneData.cpp's |legacyICUTimeZones|) for its own backwards
+ * compatibility purposes. This set consists of ICU's supported time zones,
+ * minus all backwards-compatibility time zones.
+ */
+ TimeZoneSet availableTimeZones;
+
+ /**
+ * IANA treats some time zone names as Zones, that ICU instead treats as
+ * Links. For example, IANA considers "America/Indiana/Indianapolis" to be
+ * a Zone and "America/Fort_Wayne" a Link that targets it, but ICU
+ * considers the former a Link that targets "America/Indianapolis" (which
+ * IANA treats as a Link).
+ *
+ * ECMA-402 requires that we respect IANA data, so if we're asked to
+ * canonicalize a time zone name in this set, we must *not* return ICU's
+ * canonicalization.
+ */
+ TimeZoneSet ianaZonesTreatedAsLinksByICU;
+
+ /**
+ * IANA treats some time zone names as Links to one target, that ICU
+ * instead treats as either Zones, or Links to different targets. An
+ * example of the former is "Asia/Calcutta, which IANA assigns the target
+ * "Asia/Kolkata" but ICU considers its own Zone. An example of the latter
+ * is "America/Virgin", which IANA assigns the target
+ * "America/Port_of_Spain" but ICU assigns the target "America/St_Thomas".
+ *
+ * ECMA-402 requires that we respect IANA data, so if we're asked to
+ * canonicalize a time zone name that's a key in this map, we *must* return
+ * the corresponding value and *must not* return ICU's canonicalization.
+ */
+ TimeZoneMap ianaLinksCanonicalizedDifferentlyByICU;
+
+ bool timeZoneDataInitialized = false;
+
+ /**
+ * Precomputes the available time zone names, because it's too expensive to
+ * call ucal_openTimeZones() repeatedly.
+ */
+ bool ensureTimeZones(JSContext* cx);
+
+ public:
+ /**
+ * Returns the validated time zone name in |result|. If the input time zone
+ * isn't a valid IANA time zone name, |result| remains unchanged.
+ */
+ bool validateTimeZoneName(JSContext* cx, JS::Handle<JSString*> timeZone,
+ JS::MutableHandle<JSAtom*> result);
+
+ /**
+ * Returns the canonical time zone name in |result|. If no canonical name
+ * was found, |result| remains unchanged.
+ *
+ * This method only handles time zones which are canonicalized differently
+ * by ICU when compared to IANA.
+ */
+ bool tryCanonicalizeTimeZoneConsistentWithIANA(
+ JSContext* cx, JS::Handle<JSString*> timeZone,
+ JS::MutableHandle<JSAtom*> result);
+
+ /**
+ * Returns an iterator over all available time zones supported by ICU. The
+ * returned time zone names aren't canonicalized.
+ */
+ JS::Result<TimeZoneSet::Iterator> availableTimeZonesIteration(JSContext* cx);
+
+ private:
+ using Locale = JSAtom*;
+
+ struct LocaleHasher {
+ struct Lookup : LinearStringLookup {
+ explicit Lookup(JSLinearString* locale);
+ Lookup(const char* chars, size_t length);
+ };
+
+ static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; }
+ static bool match(Locale key, const Lookup& lookup);
+ };
+
+ using LocaleSet = GCHashSet<Locale, LocaleHasher, SystemAllocPolicy>;
+
+ // Set of supported locales for all Intl service constructors except Collator,
+ // which uses its own set.
+ //
+ // UDateFormat:
+ // udat_[count,get]Available() return the same results as their
+ // uloc_[count,get]Available() counterparts.
+ //
+ // UNumberFormatter:
+ // unum_[count,get]Available() return the same results as their
+ // uloc_[count,get]Available() counterparts.
+ //
+ // UListFormatter, UPluralRules, and URelativeDateTimeFormatter:
+ // We're going to use ULocale availableLocales as per ICU recommendation:
+ // https://unicode-org.atlassian.net/browse/ICU-12756
+ LocaleSet supportedLocales;
+
+ // ucol_[count,get]Available() return different results compared to
+ // uloc_[count,get]Available(), we can't use |supportedLocales| here.
+ LocaleSet collatorSupportedLocales;
+
+ bool supportedLocalesInitialized = false;
+
+ // CountAvailable and GetAvailable describe the signatures used for ICU API
+ // to determine available locales for various functionality.
+ using CountAvailable = int32_t (*)();
+ using GetAvailable = const char* (*)(int32_t localeIndex);
+
+ template <class AvailableLocales>
+ static bool getAvailableLocales(JSContext* cx, LocaleSet& locales,
+ const AvailableLocales& availableLocales);
+
+ /**
+ * Precomputes the available locales sets.
+ */
+ bool ensureSupportedLocales(JSContext* cx);
+
+ public:
+ enum class SupportedLocaleKind {
+ Collator,
+ DateTimeFormat,
+ DisplayNames,
+ ListFormat,
+ NumberFormat,
+ PluralRules,
+ RelativeTimeFormat
+ };
+
+ /**
+ * Sets |supported| to true if |locale| is supported by the requested Intl
+ * service constructor. Otherwise sets |supported| to false.
+ */
+ [[nodiscard]] bool isSupportedLocale(JSContext* cx, SupportedLocaleKind kind,
+ JS::Handle<JSString*> locale,
+ bool* supported);
+
+ /**
+ * Returns all available locales for |kind|.
+ */
+ ArrayObject* availableLocalesOf(JSContext* cx, SupportedLocaleKind kind);
+
+ private:
+ /**
+ * The case first parameter (BCP47 key "kf") allows to switch the order of
+ * upper- and lower-case characters. ICU doesn't directly provide an API
+ * to query the default case first value of a given locale, but instead
+ * requires to instantiate a collator object and then query the case first
+ * attribute (UCOL_CASE_FIRST).
+ * To avoid instantiating an additional collator object whenever we need
+ * to retrieve the default case first value of a specific locale, we
+ * compute the default case first value for every supported locale only
+ * once and then keep a list of all locales which don't use the default
+ * case first setting.
+ * There is almost no difference between lower-case first and when case
+ * first is disabled (UCOL_LOWER_FIRST resp. UCOL_OFF), so we only need to
+ * track locales which use upper-case first as their default setting.
+ *
+ * Instantiating collator objects for each available locale is slow
+ * (bug 1527879), therefore we're hardcoding the two locales using upper-case
+ * first ("da" (Danish) and "mt" (Maltese)) and only assert in debug-mode
+ * these two locales match the upper-case first locales returned by ICU. A
+ * system-ICU may support a different set of locales, therefore we're always
+ * calling into ICU to find the upper-case first locales in that case.
+ */
+
+#if DEBUG || MOZ_SYSTEM_ICU
+ LocaleSet upperCaseFirstLocales;
+
+ bool upperCaseFirstInitialized = false;
+
+ /**
+ * Precomputes the available locales which use upper-case first sorting.
+ */
+ bool ensureUpperCaseFirstLocales(JSContext* cx);
+#endif
+
+ public:
+ /**
+ * Sets |isUpperFirst| to true if |locale| sorts upper-case characters
+ * before lower-case characters.
+ */
+ bool isUpperCaseFirst(JSContext* cx, JS::Handle<JSString*> locale,
+ bool* isUpperFirst);
+
+ private:
+ using UniqueDateTimePatternGenerator =
+ mozilla::UniquePtr<mozilla::intl::DateTimePatternGenerator,
+ DateTimePatternGeneratorDeleter>;
+
+ UniqueDateTimePatternGenerator dateTimePatternGenerator;
+ JS::UniqueChars dateTimePatternGeneratorLocale;
+
+ public:
+ /**
+ * Get a non-owned cached instance of the DateTimePatternGenerator, which is
+ * expensive to instantiate.
+ *
+ * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1549578
+ */
+ mozilla::intl::DateTimePatternGenerator* getDateTimePatternGenerator(
+ JSContext* cx, const char* locale);
+
+ public:
+ void destroyInstance();
+
+ void trace(JSTracer* trc);
+
+ size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
+};
+
+} // namespace intl
+
+} // namespace js
+
+#endif /* builtin_intl_SharedIntlData_h */
diff --git a/js/src/builtin/intl/StringAsciiChars.h b/js/src/builtin/intl/StringAsciiChars.h
new file mode 100644
index 0000000000..3323544d8c
--- /dev/null
+++ b/js/src/builtin/intl/StringAsciiChars.h
@@ -0,0 +1,77 @@
+/* -*- 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_StringAsciiChars_h
+#define builtin_intl_StringAsciiChars_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Span.h"
+
+#include <stddef.h>
+
+#include "js/GCAPI.h"
+#include "js/TypeDecls.h"
+#include "js/Vector.h"
+
+#include "vm/StringType.h"
+
+namespace js::intl {
+
+/**
+ * String view of an ASCII-only string.
+ *
+ * This holds a reference to a JSLinearString and can produce a string view
+ * into that string. If the string is represented by Latin1 characters, the
+ * span is returned directly. If the string is represented by UTF-16
+ * characters, it copies the char16_t characters into a char array, and then
+ * returns a span based on the copy.
+ *
+ * This allows us to avoid copying for the common use case that the ASCII
+ * characters are represented in Latin1.
+ */
+class MOZ_STACK_CLASS StringAsciiChars final {
+ // When copying string characters, use this many bytes of inline storage.
+ static const size_t InlineCapacity = 24;
+
+ JS::AutoCheckCannotGC nogc_;
+
+ JSLinearString* str_;
+
+ mozilla::Maybe<Vector<Latin1Char, InlineCapacity>> ownChars_;
+
+ public:
+ explicit StringAsciiChars(JSLinearString* str) : str_(str) {
+ MOZ_ASSERT(StringIsAscii(str));
+ }
+
+ operator mozilla::Span<const char>() const {
+ if (str_->hasLatin1Chars()) {
+ return mozilla::AsChars(str_->latin1Range(nogc_));
+ }
+ return mozilla::AsChars(mozilla::Span<const Latin1Char>(*ownChars_));
+ }
+
+ [[nodiscard]] bool init(JSContext* cx) {
+ if (str_->hasLatin1Chars()) {
+ return true;
+ }
+
+ ownChars_.emplace(cx);
+ if (!ownChars_->resize(str_->length())) {
+ return false;
+ }
+
+ js::CopyChars(ownChars_->begin(), *str_);
+
+ return true;
+ }
+};
+
+} // namespace js::intl
+
+#endif // builtin_intl_StringAsciiChars_h
diff --git a/js/src/builtin/intl/TimeZoneDataGenerated.h b/js/src/builtin/intl/TimeZoneDataGenerated.h
new file mode 100644
index 0000000000..04954ffbc7
--- /dev/null
+++ b/js/src/builtin/intl/TimeZoneDataGenerated.h
@@ -0,0 +1,142 @@
+// Generated by make_intl_data.py. DO NOT EDIT.
+// tzdata version = 2022g
+
+#ifndef builtin_intl_TimeZoneDataGenerated_h
+#define builtin_intl_TimeZoneDataGenerated_h
+
+namespace js {
+namespace timezone {
+
+// Format:
+// "ZoneName" // ICU-Name [time zone file]
+const char* const ianaZonesTreatedAsLinksByICU[] = {
+ "Africa/Asmara", // Africa/Asmera [backzone]
+ "Africa/Timbuktu", // Africa/Bamako [backzone]
+ "America/Argentina/Buenos_Aires", // America/Buenos_Aires [southamerica]
+ "America/Argentina/Catamarca", // America/Catamarca [southamerica]
+ "America/Argentina/ComodRivadavia", // America/Catamarca [backzone]
+ "America/Argentina/Cordoba", // America/Cordoba [southamerica]
+ "America/Argentina/Jujuy", // America/Jujuy [southamerica]
+ "America/Argentina/Mendoza", // America/Mendoza [southamerica]
+ "America/Atikokan", // America/Coral_Harbour [backzone]
+ "America/Ensenada", // America/Tijuana [backzone]
+ "America/Indiana/Indianapolis", // America/Indianapolis [northamerica]
+ "America/Kentucky/Louisville", // America/Louisville [northamerica]
+ "America/Nuuk", // America/Godthab [europe]
+ "America/Rosario", // America/Cordoba [backzone]
+ "Asia/Chongqing", // Asia/Shanghai [backzone]
+ "Asia/Harbin", // Asia/Shanghai [backzone]
+ "Asia/Ho_Chi_Minh", // Asia/Saigon [asia]
+ "Asia/Kashgar", // Asia/Urumqi [backzone]
+ "Asia/Kathmandu", // Asia/Katmandu [asia]
+ "Asia/Kolkata", // Asia/Calcutta [asia]
+ "Asia/Tel_Aviv", // Asia/Jerusalem [backzone]
+ "Asia/Yangon", // Asia/Rangoon [asia]
+ "Atlantic/Faroe", // Atlantic/Faeroe [europe]
+ "Atlantic/Jan_Mayen", // Arctic/Longyearbyen [backzone]
+ "EST", // Etc/GMT+5 [northamerica]
+ "Europe/Belfast", // Europe/London [backzone]
+ "Europe/Kyiv", // Europe/Kiev [europe]
+ "Europe/Tiraspol", // Europe/Chisinau [backzone]
+ "HST", // Etc/GMT+10 [northamerica]
+ "MST", // Etc/GMT+7 [northamerica]
+ "Pacific/Chuuk", // Pacific/Truk [backzone]
+ "Pacific/Kanton", // Pacific/Enderbury [australasia]
+ "Pacific/Pohnpei", // Pacific/Ponape [backzone]
+};
+
+// Format:
+// "LinkName", "Target" // ICU-Target [time zone file]
+struct LinkAndTarget
+{
+ const char* const link;
+ const char* const target;
+};
+
+const LinkAndTarget ianaLinksCanonicalizedDifferentlyByICU[] = {
+ { "Africa/Asmera", "Africa/Asmara" }, // Africa/Asmera [backward]
+ { "America/Buenos_Aires", "America/Argentina/Buenos_Aires" }, // America/Buenos_Aires [backward]
+ { "America/Catamarca", "America/Argentina/Catamarca" }, // America/Catamarca [backward]
+ { "America/Cordoba", "America/Argentina/Cordoba" }, // America/Cordoba [backward]
+ { "America/Fort_Wayne", "America/Indiana/Indianapolis" }, // America/Indianapolis [backward]
+ { "America/Godthab", "America/Nuuk" }, // America/Godthab [backward]
+ { "America/Indianapolis", "America/Indiana/Indianapolis" }, // America/Indianapolis [backward]
+ { "America/Jujuy", "America/Argentina/Jujuy" }, // America/Jujuy [backward]
+ { "America/Kralendijk", "America/Curacao" }, // America/Kralendijk [backward]
+ { "America/Louisville", "America/Kentucky/Louisville" }, // America/Louisville [backward]
+ { "America/Lower_Princes", "America/Curacao" }, // America/Lower_Princes [backward]
+ { "America/Marigot", "America/Port_of_Spain" }, // America/Marigot [backward]
+ { "America/Mendoza", "America/Argentina/Mendoza" }, // America/Mendoza [backward]
+ { "America/Santa_Isabel", "America/Tijuana" }, // America/Santa_Isabel [backward]
+ { "America/St_Barthelemy", "America/Port_of_Spain" }, // America/St_Barthelemy [backward]
+ { "Antarctica/South_Pole", "Antarctica/McMurdo" }, // Pacific/Auckland [backward]
+ { "Arctic/Longyearbyen", "Europe/Oslo" }, // Arctic/Longyearbyen [backward]
+ { "Asia/Calcutta", "Asia/Kolkata" }, // Asia/Calcutta [backward]
+ { "Asia/Chungking", "Asia/Chongqing" }, // Asia/Shanghai [backward]
+ { "Asia/Katmandu", "Asia/Kathmandu" }, // Asia/Katmandu [backward]
+ { "Asia/Rangoon", "Asia/Yangon" }, // Asia/Rangoon [backward]
+ { "Asia/Saigon", "Asia/Ho_Chi_Minh" }, // Asia/Saigon [backward]
+ { "Atlantic/Faeroe", "Atlantic/Faroe" }, // Atlantic/Faeroe [backward]
+ { "Europe/Bratislava", "Europe/Prague" }, // Europe/Bratislava [backward]
+ { "Europe/Busingen", "Europe/Zurich" }, // Europe/Busingen [backward]
+ { "Europe/Kiev", "Europe/Kyiv" }, // Europe/Kiev [backward]
+ { "Europe/Mariehamn", "Europe/Helsinki" }, // Europe/Mariehamn [backward]
+ { "Europe/Podgorica", "Europe/Belgrade" }, // Europe/Podgorica [backward]
+ { "Europe/San_Marino", "Europe/Rome" }, // Europe/San_Marino [backward]
+ { "Europe/Vatican", "Europe/Rome" }, // Europe/Vatican [backward]
+ { "Pacific/Ponape", "Pacific/Pohnpei" }, // Pacific/Ponape [backward]
+ { "Pacific/Truk", "Pacific/Chuuk" }, // Pacific/Truk [backward]
+ { "Pacific/Yap", "Pacific/Chuuk" }, // Pacific/Truk [backward]
+ { "US/East-Indiana", "America/Indiana/Indianapolis" }, // America/Indianapolis [backward]
+};
+
+// Legacy ICU time zones, these are not valid IANA time zone names. We also
+// disallow the old and deprecated System V time zones.
+// https://ssl.icu-project.org/repos/icu/trunk/icu4c/source/tools/tzcode/icuzones
+const char* const legacyICUTimeZones[] = {
+ "ACT",
+ "AET",
+ "AGT",
+ "ART",
+ "AST",
+ "BET",
+ "BST",
+ "CAT",
+ "CNT",
+ "CST",
+ "CTT",
+ "Canada/East-Saskatchewan",
+ "EAT",
+ "ECT",
+ "IET",
+ "IST",
+ "JST",
+ "MIT",
+ "NET",
+ "NST",
+ "PLT",
+ "PNT",
+ "PRT",
+ "PST",
+ "SST",
+ "US/Pacific-New",
+ "VST",
+ "SystemV/AST4",
+ "SystemV/AST4ADT",
+ "SystemV/CST6",
+ "SystemV/CST6CDT",
+ "SystemV/EST5",
+ "SystemV/EST5EDT",
+ "SystemV/HST10",
+ "SystemV/MST7",
+ "SystemV/MST7MDT",
+ "SystemV/PST8",
+ "SystemV/PST8PDT",
+ "SystemV/YST9",
+ "SystemV/YST9YDT",
+};
+
+} // namespace timezone
+} // namespace js
+
+#endif /* builtin_intl_TimeZoneDataGenerated_h */
diff --git a/js/src/builtin/intl/make_intl_data.py b/js/src/builtin/intl/make_intl_data.py
new file mode 100755
index 0000000000..1f9d7ff30e
--- /dev/null
+++ b/js/src/builtin/intl/make_intl_data.py
@@ -0,0 +1,4123 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# 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/.
+
+""" Usage:
+ make_intl_data.py langtags [cldr_core.zip]
+ make_intl_data.py tzdata
+ make_intl_data.py currency
+ make_intl_data.py units
+ make_intl_data.py numbering
+
+
+ Target "langtags":
+ This script extracts information about 1) mappings between deprecated and
+ current Unicode BCP 47 locale identifiers, and 2) deprecated and current
+ BCP 47 Unicode extension value from CLDR, and converts it to C++ mapping
+ code in intl/components/LocaleGenerated.cpp. The code is used in
+ intl/components/Locale.cpp.
+
+
+ Target "tzdata":
+ This script computes which time zone informations are not up-to-date in ICU
+ and provides the necessary mappings to workaround this problem.
+ https://ssl.icu-project.org/trac/ticket/12044
+
+
+ Target "currency":
+ Generates the mapping from currency codes to decimal digits used for them.
+
+
+ Target "units":
+ Generate source and test files using the list of so-called "sanctioned unit
+ identifiers" and verifies that the ICU data filter includes these units.
+
+
+ Target "numbering":
+ Generate source and test files using the list of numbering systems with
+ simple digit mappings and verifies that it's in sync with ICU/CLDR.
+"""
+
+import io
+import json
+import os
+import re
+import sys
+import tarfile
+import tempfile
+from contextlib import closing
+from functools import partial, total_ordering
+from itertools import chain, groupby, tee
+from operator import attrgetter, itemgetter
+from zipfile import ZipFile
+
+import yaml
+
+if sys.version_info.major == 2:
+ from itertools import ifilter as filter
+ from itertools import ifilterfalse as filterfalse
+ from itertools import imap as map
+ from itertools import izip_longest as zip_longest
+
+ from urllib2 import Request as UrlRequest
+ from urllib2 import urlopen
+ from urlparse import urlsplit
+else:
+ from itertools import filterfalse, zip_longest
+ from urllib.parse import urlsplit
+ from urllib.request import Request as UrlRequest
+ from urllib.request import urlopen
+
+
+# From https://docs.python.org/3/library/itertools.html
+def grouper(iterable, n, fillvalue=None):
+ "Collect data into fixed-length chunks or blocks"
+ # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
+ args = [iter(iterable)] * n
+ return zip_longest(*args, fillvalue=fillvalue)
+
+
+def writeMappingHeader(println, description, source, url):
+ if type(description) is not list:
+ description = [description]
+ for desc in description:
+ println("// {0}".format(desc))
+ println("// Derived from {0}.".format(source))
+ println("// {0}".format(url))
+
+
+def writeMappingsVar(println, mapping, name, description, source, url):
+ """Writes a variable definition with a mapping table.
+
+ Writes the contents of dictionary |mapping| through the |println|
+ function with the given variable name and a comment with description,
+ fileDate, and URL.
+ """
+ println("")
+ writeMappingHeader(println, description, source, url)
+ println("var {0} = {{".format(name))
+ for (key, value) in sorted(mapping.items(), key=itemgetter(0)):
+ println(' "{0}": "{1}",'.format(key, value))
+ println("};")
+
+
+def writeMappingsBinarySearch(
+ println,
+ fn_name,
+ type_name,
+ name,
+ validate_fn,
+ validate_case_fn,
+ mappings,
+ tag_maxlength,
+ description,
+ source,
+ url,
+):
+ """Emit code to perform a binary search on language tag subtags.
+
+ Uses the contents of |mapping|, which can either be a dictionary or set,
+ to emit a mapping function to find subtag replacements.
+ """
+ println("")
+ writeMappingHeader(println, description, source, url)
+ println(
+ """
+bool mozilla::intl::Locale::{0}({1} {2}) {{
+ MOZ_ASSERT({3}({2}.Span()));
+ MOZ_ASSERT({4}({2}.Span()));
+""".format(
+ fn_name, type_name, name, validate_fn, validate_case_fn
+ ).strip()
+ )
+ writeMappingsBinarySearchBody(println, name, name, mappings, tag_maxlength)
+
+ println(
+ """
+}""".lstrip(
+ "\n"
+ )
+ )
+
+
+def writeMappingsBinarySearchBody(
+ println, source_name, target_name, mappings, tag_maxlength
+):
+ def write_array(subtags, name, length, fixed):
+ if fixed:
+ println(
+ " static const char {}[{}][{}] = {{".format(
+ name, len(subtags), length + 1
+ )
+ )
+ else:
+ println(" static const char* {}[{}] = {{".format(name, len(subtags)))
+
+ # Group in pairs of ten to not exceed the 80 line column limit.
+ for entries in grouper(subtags, 10):
+ entries = (
+ '"{}"'.format(tag).rjust(length + 2)
+ for tag in entries
+ if tag is not None
+ )
+ println(" {},".format(", ".join(entries)))
+
+ println(" };")
+
+ trailing_return = True
+
+ # Sort the subtags by length. That enables using an optimized comparator
+ # for the binary search, which only performs a single |memcmp| for multiple
+ # of two subtag lengths.
+ mappings_keys = mappings.keys() if type(mappings) == dict else mappings
+ for (length, subtags) in groupby(sorted(mappings_keys, key=len), len):
+ # Omit the length check if the current length is the maximum length.
+ if length != tag_maxlength:
+ println(
+ """
+ if ({}.Length() == {}) {{
+""".format(
+ source_name, length
+ ).rstrip(
+ "\n"
+ )
+ )
+ else:
+ trailing_return = False
+ println(
+ """
+ {
+""".rstrip(
+ "\n"
+ )
+ )
+
+ # The subtags need to be sorted for binary search to work.
+ subtags = sorted(subtags)
+
+ def equals(subtag):
+ return """{}.EqualTo("{}")""".format(source_name, subtag)
+
+ # Don't emit a binary search for short lists.
+ if len(subtags) == 1:
+ if type(mappings) == dict:
+ println(
+ """
+ if ({}) {{
+ {}.Set(mozilla::MakeStringSpan("{}"));
+ return true;
+ }}
+ return false;
+""".format(
+ equals(subtags[0]), target_name, mappings[subtags[0]]
+ ).strip(
+ "\n"
+ )
+ )
+ else:
+ println(
+ """
+ return {};
+""".format(
+ equals(subtags[0])
+ ).strip(
+ "\n"
+ )
+ )
+ elif len(subtags) <= 4:
+ if type(mappings) == dict:
+ for subtag in subtags:
+ println(
+ """
+ if ({}) {{
+ {}.Set("{}");
+ return true;
+ }}
+""".format(
+ equals(subtag), target_name, mappings[subtag]
+ ).strip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+ return false;
+""".strip(
+ "\n"
+ )
+ )
+ else:
+ cond = (equals(subtag) for subtag in subtags)
+ cond = (" ||\n" + " " * (4 + len("return "))).join(cond)
+ println(
+ """
+ return {};
+""".format(
+ cond
+ ).strip(
+ "\n"
+ )
+ )
+ else:
+ write_array(subtags, source_name + "s", length, True)
+
+ if type(mappings) == dict:
+ write_array([mappings[k] for k in subtags], "aliases", length, False)
+
+ println(
+ """
+ if (const char* replacement = SearchReplacement({0}s, aliases, {0})) {{
+ {1}.Set(mozilla::MakeStringSpan(replacement));
+ return true;
+ }}
+ return false;
+""".format(
+ source_name, target_name
+ ).rstrip()
+ )
+ else:
+ println(
+ """
+ return HasReplacement({0}s, {0});
+""".format(
+ source_name
+ ).rstrip()
+ )
+
+ println(
+ """
+ }
+""".strip(
+ "\n"
+ )
+ )
+
+ if trailing_return:
+ println(
+ """
+ return false;"""
+ )
+
+
+def writeComplexLanguageTagMappings(
+ println, complex_language_mappings, description, source, url
+):
+ println("")
+ writeMappingHeader(println, description, source, url)
+ println(
+ """
+void mozilla::intl::Locale::PerformComplexLanguageMappings() {
+ MOZ_ASSERT(IsStructurallyValidLanguageTag(Language().Span()));
+ MOZ_ASSERT(IsCanonicallyCasedLanguageTag(Language().Span()));
+""".lstrip()
+ )
+
+ # Merge duplicate language entries.
+ language_aliases = {}
+ for (deprecated_language, (language, script, region)) in sorted(
+ complex_language_mappings.items(), key=itemgetter(0)
+ ):
+ key = (language, script, region)
+ if key not in language_aliases:
+ language_aliases[key] = []
+ else:
+ language_aliases[key].append(deprecated_language)
+
+ first_language = True
+ for (deprecated_language, (language, script, region)) in sorted(
+ complex_language_mappings.items(), key=itemgetter(0)
+ ):
+ key = (language, script, region)
+ if deprecated_language in language_aliases[key]:
+ continue
+
+ if_kind = "if" if first_language else "else if"
+ first_language = False
+
+ cond = (
+ 'Language().EqualTo("{}")'.format(lang)
+ for lang in [deprecated_language] + language_aliases[key]
+ )
+ cond = (" ||\n" + " " * (2 + len(if_kind) + 2)).join(cond)
+
+ println(
+ """
+ {} ({}) {{""".format(
+ if_kind, cond
+ ).strip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+ SetLanguage("{}");""".format(
+ language
+ ).strip(
+ "\n"
+ )
+ )
+
+ if script is not None:
+ println(
+ """
+ if (Script().Missing()) {{
+ SetScript("{}");
+ }}""".format(
+ script
+ ).strip(
+ "\n"
+ )
+ )
+ if region is not None:
+ println(
+ """
+ if (Region().Missing()) {{
+ SetRegion("{}");
+ }}""".format(
+ region
+ ).strip(
+ "\n"
+ )
+ )
+ println(
+ """
+ }""".strip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+}
+""".strip(
+ "\n"
+ )
+ )
+
+
+def writeComplexRegionTagMappings(
+ println, complex_region_mappings, description, source, url
+):
+ println("")
+ writeMappingHeader(println, description, source, url)
+ println(
+ """
+void mozilla::intl::Locale::PerformComplexRegionMappings() {
+ MOZ_ASSERT(IsStructurallyValidLanguageTag(Language().Span()));
+ MOZ_ASSERT(IsCanonicallyCasedLanguageTag(Language().Span()));
+ MOZ_ASSERT(IsStructurallyValidRegionTag(Region().Span()));
+ MOZ_ASSERT(IsCanonicallyCasedRegionTag(Region().Span()));
+""".lstrip()
+ )
+
+ # |non_default_replacements| is a list and hence not hashable. Convert it
+ # to a string to get a proper hashable value.
+ def hash_key(default, non_default_replacements):
+ return (default, str(sorted(str(v) for v in non_default_replacements)))
+
+ # Merge duplicate region entries.
+ region_aliases = {}
+ for (deprecated_region, (default, non_default_replacements)) in sorted(
+ complex_region_mappings.items(), key=itemgetter(0)
+ ):
+ key = hash_key(default, non_default_replacements)
+ if key not in region_aliases:
+ region_aliases[key] = []
+ else:
+ region_aliases[key].append(deprecated_region)
+
+ first_region = True
+ for (deprecated_region, (default, non_default_replacements)) in sorted(
+ complex_region_mappings.items(), key=itemgetter(0)
+ ):
+ key = hash_key(default, non_default_replacements)
+ if deprecated_region in region_aliases[key]:
+ continue
+
+ if_kind = "if" if first_region else "else if"
+ first_region = False
+
+ cond = (
+ 'Region().EqualTo("{}")'.format(region)
+ for region in [deprecated_region] + region_aliases[key]
+ )
+ cond = (" ||\n" + " " * (2 + len(if_kind) + 2)).join(cond)
+
+ println(
+ """
+ {} ({}) {{""".format(
+ if_kind, cond
+ ).strip(
+ "\n"
+ )
+ )
+
+ replacement_regions = sorted(
+ {region for (_, _, region) in non_default_replacements}
+ )
+
+ first_case = True
+ for replacement_region in replacement_regions:
+ replacement_language_script = sorted(
+ (language, script)
+ for (language, script, region) in (non_default_replacements)
+ if region == replacement_region
+ )
+
+ if_kind = "if" if first_case else "else if"
+ first_case = False
+
+ def compare_tags(language, script):
+ if script is None:
+ return 'Language().EqualTo("{}")'.format(language)
+ return '(Language().EqualTo("{}") && Script().EqualTo("{}"))'.format(
+ language, script
+ )
+
+ cond = (
+ compare_tags(language, script)
+ for (language, script) in replacement_language_script
+ )
+ cond = (" ||\n" + " " * (4 + len(if_kind) + 2)).join(cond)
+
+ println(
+ """
+ {} ({}) {{
+ SetRegion("{}");
+ }}""".format(
+ if_kind, cond, replacement_region
+ )
+ .rstrip()
+ .strip("\n")
+ )
+
+ println(
+ """
+ else {{
+ SetRegion("{}");
+ }}
+ }}""".format(
+ default
+ )
+ .rstrip()
+ .strip("\n")
+ )
+
+ println(
+ """
+}
+""".strip(
+ "\n"
+ )
+ )
+
+
+def writeVariantTagMappings(println, variant_mappings, description, source, url):
+ """Writes a function definition that maps variant subtags."""
+ println(
+ """
+static const char* ToCharPointer(const char* str) {
+ return str;
+}
+
+static const char* ToCharPointer(const mozilla::intl::UniqueChars& str) {
+ return str.get();
+}
+
+template <typename T, typename U = T>
+static bool IsLessThan(const T& a, const U& b) {
+ return strcmp(ToCharPointer(a), ToCharPointer(b)) < 0;
+}
+"""
+ )
+ writeMappingHeader(println, description, source, url)
+ println(
+ """
+bool mozilla::intl::Locale::PerformVariantMappings() {
+ // The variant subtags need to be sorted for binary search.
+ MOZ_ASSERT(std::is_sorted(mVariants.begin(), mVariants.end(),
+ IsLessThan<decltype(mVariants)::ElementType>));
+
+ auto removeVariantAt = [&](size_t index) {
+ mVariants.erase(mVariants.begin() + index);
+ };
+
+ auto insertVariantSortedIfNotPresent = [&](const char* variant) {
+ auto* p = std::lower_bound(
+ mVariants.begin(), mVariants.end(), variant,
+ IsLessThan<decltype(mVariants)::ElementType, decltype(variant)>);
+
+ // Don't insert the replacement when already present.
+ if (p != mVariants.end() && strcmp(p->get(), variant) == 0) {
+ return true;
+ }
+
+ // Insert the preferred variant in sort order.
+ auto preferred = DuplicateStringToUniqueChars(variant);
+ return !!mVariants.insert(p, std::move(preferred));
+ };
+
+ for (size_t i = 0; i < mVariants.length();) {
+ const char* variant = mVariants[i].get();
+ MOZ_ASSERT(IsCanonicallyCasedVariantTag(mozilla::MakeStringSpan(variant)));
+""".lstrip()
+ )
+
+ (no_alias, with_alias) = partition(
+ variant_mappings.items(), lambda item: item[1] is None
+ )
+
+ no_replacements = " ||\n ".join(
+ f"""strcmp(variant, "{deprecated_variant}") == 0"""
+ for (deprecated_variant, _) in sorted(no_alias, key=itemgetter(0))
+ )
+
+ println(
+ f"""
+ if ({no_replacements}) {{
+ removeVariantAt(i);
+ }}
+""".strip(
+ "\n"
+ )
+ )
+
+ for (deprecated_variant, (type, replacement)) in sorted(
+ with_alias, key=itemgetter(0)
+ ):
+ println(
+ f"""
+ else if (strcmp(variant, "{deprecated_variant}") == 0) {{
+ removeVariantAt(i);
+""".strip(
+ "\n"
+ )
+ )
+
+ if type == "language":
+ println(
+ f"""
+ SetLanguage("{replacement}");
+""".strip(
+ "\n"
+ )
+ )
+ elif type == "region":
+ println(
+ f"""
+ SetRegion("{replacement}");
+""".strip(
+ "\n"
+ )
+ )
+ else:
+ assert type == "variant"
+ println(
+ f"""
+ if (!insertVariantSortedIfNotPresent("{replacement}")) {{
+ return false;
+ }}
+""".strip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+ }
+""".strip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+ else {
+ i++;
+ }
+ }
+ return true;
+}
+""".strip(
+ "\n"
+ )
+ )
+
+
+def writeLegacyMappingsFunction(println, legacy_mappings, description, source, url):
+ """Writes a function definition that maps legacy language tags."""
+ println("")
+ writeMappingHeader(println, description, source, url)
+ println(
+ """\
+bool mozilla::intl::Locale::UpdateLegacyMappings() {
+ // We're mapping legacy tags to non-legacy form here.
+ // Other tags remain unchanged.
+ //
+ // Legacy tags are either sign language tags ("sgn") or have one or multiple
+ // variant subtags. Therefore we can quickly exclude most tags by checking
+ // these two subtags.
+
+ MOZ_ASSERT(IsCanonicallyCasedLanguageTag(Language().Span()));
+
+ if (!Language().EqualTo("sgn") && mVariants.length() == 0) {
+ return true;
+ }
+
+#ifdef DEBUG
+ for (const auto& variant : Variants()) {
+ MOZ_ASSERT(IsStructurallyValidVariantTag(variant));
+ MOZ_ASSERT(IsCanonicallyCasedVariantTag(variant));
+ }
+#endif
+
+ // The variant subtags need to be sorted for binary search.
+ MOZ_ASSERT(std::is_sorted(mVariants.begin(), mVariants.end(),
+ IsLessThan<decltype(mVariants)::ElementType>));
+
+ auto findVariant = [this](const char* variant) {
+ auto* p = std::lower_bound(mVariants.begin(), mVariants.end(), variant,
+ IsLessThan<decltype(mVariants)::ElementType,
+ decltype(variant)>);
+
+ if (p != mVariants.end() && strcmp(p->get(), variant) == 0) {
+ return p;
+ }
+ return static_cast<decltype(p)>(nullptr);
+ };
+
+ auto insertVariantSortedIfNotPresent = [&](const char* variant) {
+ auto* p = std::lower_bound(mVariants.begin(), mVariants.end(), variant,
+ IsLessThan<decltype(mVariants)::ElementType,
+ decltype(variant)>);
+
+ // Don't insert the replacement when already present.
+ if (p != mVariants.end() && strcmp(p->get(), variant) == 0) {
+ return true;
+ }
+
+ // Insert the preferred variant in sort order.
+ auto preferred = DuplicateStringToUniqueChars(variant);
+ return !!mVariants.insert(p, std::move(preferred));
+ };
+
+ auto removeVariant = [&](auto* p) {
+ size_t index = std::distance(mVariants.begin(), p);
+ mVariants.erase(mVariants.begin() + index);
+ };
+
+ auto removeVariants = [&](auto* p, auto* q) {
+ size_t pIndex = std::distance(mVariants.begin(), p);
+ size_t qIndex = std::distance(mVariants.begin(), q);
+ MOZ_ASSERT(pIndex < qIndex, "variant subtags are sorted");
+
+ mVariants.erase(mVariants.begin() + qIndex);
+ mVariants.erase(mVariants.begin() + pIndex);
+ };"""
+ )
+
+ # Helper class for pattern matching.
+ class AnyClass:
+ def __eq__(self, obj):
+ return obj is not None
+
+ Any = AnyClass()
+
+ # Group the mappings by language.
+ legacy_mappings_by_language = {}
+ for (type, replacement) in legacy_mappings.items():
+ (language, _, _, _) = type
+ legacy_mappings_by_language.setdefault(language, {})[type] = replacement
+
+ # Handle the empty language case first.
+ if None in legacy_mappings_by_language:
+ # Get the mappings and remove them from the dict.
+ mappings = legacy_mappings_by_language.pop(None)
+
+ # This case only applies for the "hepburn-heploc" -> "alalc97"
+ # mapping, so just inline it here.
+ from_tag = (None, None, None, "hepburn-heploc")
+ to_tag = (None, None, None, "alalc97")
+
+ assert len(mappings) == 1
+ assert mappings[from_tag] == to_tag
+
+ println(
+ """
+ if (mVariants.length() >= 2) {
+ if (auto* hepburn = findVariant("hepburn")) {
+ if (auto* heploc = findVariant("heploc")) {
+ removeVariants(hepburn, heploc);
+
+ if (!insertVariantSortedIfNotPresent("alalc97")) {
+ return false;
+ }
+ }
+ }
+ }
+"""
+ )
+
+ # Handle sign languages next.
+ if "sgn" in legacy_mappings_by_language:
+ mappings = legacy_mappings_by_language.pop("sgn")
+
+ # Legacy sign language mappings have the form "sgn-XX" where "XX" is
+ # some region code.
+ assert all(type == ("sgn", None, Any, None) for type in mappings.keys())
+
+ # Legacy sign languages are mapped to a single language subtag.
+ assert all(
+ replacement == (Any, None, None, None) for replacement in mappings.values()
+ )
+
+ println(
+ """
+ if (Language().EqualTo("sgn")) {
+ if (Region().Present() && SignLanguageMapping(mLanguage, Region())) {
+ mRegion.Set(mozilla::MakeStringSpan(""));
+ }
+ }
+""".rstrip().lstrip(
+ "\n"
+ )
+ )
+
+ # Finally handle all remaining cases.
+
+ # The remaining mappings have neither script nor region subtags in the source locale.
+ assert all(
+ type == (Any, None, None, Any)
+ for mappings in legacy_mappings_by_language.values()
+ for type in mappings.keys()
+ )
+
+ # And they have neither script nor region nor variant subtags in the target locale.
+ assert all(
+ replacement == (Any, None, None, None)
+ for mappings in legacy_mappings_by_language.values()
+ for replacement in mappings.values()
+ )
+
+ # Compact the mappings table by removing empty fields.
+ legacy_mappings_by_language = {
+ lang: {
+ variants: r_language
+ for ((_, _, _, variants), (r_language, _, _, _)) in mappings.items()
+ }
+ for (lang, mappings) in legacy_mappings_by_language.items()
+ }
+
+ # Try to combine the remaining cases.
+ legacy_mappings_compact = {}
+
+ # Python can't hash dicts or lists, so use the string representation as the hash key.
+ def hash_key(mappings):
+ return str(sorted(mappings.items(), key=itemgetter(0)))
+
+ for (lang, mappings) in sorted(
+ legacy_mappings_by_language.items(), key=itemgetter(0)
+ ):
+ key = hash_key(mappings)
+ legacy_mappings_compact.setdefault(key, []).append(lang)
+
+ for langs in legacy_mappings_compact.values():
+ language_equal_to = (
+ f"""Language().EqualTo("{lang}")""" for lang in sorted(langs)
+ )
+ cond = f""" ||\n{" " * len(" else if (")}""".join(language_equal_to)
+
+ println(
+ f"""
+ else if ({cond}) {{
+""".rstrip().lstrip(
+ "\n"
+ )
+ )
+
+ mappings = legacy_mappings_by_language[langs[0]]
+
+ # Count the variant subtags to determine the sort order.
+ def variant_size(m):
+ (k, _) = m
+ return len(k.split("-"))
+
+ # Alias rules are applied by largest union size first.
+ for (size, mappings_by_size) in groupby(
+ sorted(mappings.items(), key=variant_size, reverse=True), key=variant_size
+ ):
+
+ # Convert grouper object to dict.
+ mappings_by_size = dict(mappings_by_size)
+
+ is_first = True
+ chain_if = size == 1
+
+ # Alias rules are applied in alphabetical order
+ for (variants, r_language) in sorted(
+ mappings_by_size.items(), key=itemgetter(0)
+ ):
+ sorted_variants = sorted(variants.split("-"))
+ len_variants = len(sorted_variants)
+
+ maybe_else = "else " if chain_if and not is_first else ""
+ is_first = False
+
+ for (i, variant) in enumerate(sorted_variants):
+ println(
+ f"""
+ {" " * i}{maybe_else}if (auto* {variant} = findVariant("{variant}")) {{
+""".rstrip().lstrip(
+ "\n"
+ )
+ )
+
+ indent = " " * len_variants
+
+ println(
+ f"""
+ {indent}removeVariant{"s" if len_variants > 1 else ""}({", ".join(sorted_variants)});
+ {indent}SetLanguage("{r_language}");
+ {indent}{"return true;" if not chain_if else ""}
+""".rstrip().lstrip(
+ "\n"
+ )
+ )
+
+ for i in range(len_variants, 0, -1):
+ println(
+ f"""
+ {" " * (i - 1)}}}
+""".rstrip().lstrip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+ }
+""".rstrip().lstrip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+ return true;
+}"""
+ )
+
+
+def writeSignLanguageMappingsFunction(
+ println, legacy_mappings, description, source, url
+):
+ """Writes a function definition that maps legacy sign language tags."""
+ println("")
+ writeMappingHeader(println, description, source, url)
+ println(
+ """\
+bool mozilla::intl::Locale::SignLanguageMapping(LanguageSubtag& language,
+ const RegionSubtag& region) {
+ MOZ_ASSERT(language.EqualTo("sgn"));
+ MOZ_ASSERT(IsStructurallyValidRegionTag(region.Span()));
+ MOZ_ASSERT(IsCanonicallyCasedRegionTag(region.Span()));
+""".rstrip()
+ )
+
+ region_mappings = {
+ rg: lg
+ for ((lang, _, rg, _), (lg, _, _, _)) in legacy_mappings.items()
+ if lang == "sgn"
+ }
+
+ source_name = "region"
+ target_name = "language"
+ tag_maxlength = 3
+ writeMappingsBinarySearchBody(
+ println, source_name, target_name, region_mappings, tag_maxlength
+ )
+
+ println(
+ """
+}""".lstrip()
+ )
+
+
+def readSupplementalData(core_file):
+ """Reads CLDR Supplemental Data and extracts information for Intl.js.
+
+ Information extracted:
+ - legacyMappings: mappings from legacy tags to preferred complete language tags
+ - languageMappings: mappings from language subtags to preferred subtags
+ - complexLanguageMappings: mappings from language subtags with complex rules
+ - regionMappings: mappings from region subtags to preferred subtags
+ - complexRegionMappings: mappings from region subtags with complex rules
+ - variantMappings: mappings from variant subtags to preferred subtags
+ - likelySubtags: likely subtags used for generating test data only
+ Returns these mappings as dictionaries.
+ """
+ import xml.etree.ElementTree as ET
+
+ # From Unicode BCP 47 locale identifier <https://unicode.org/reports/tr35/>.
+ re_unicode_language_id = re.compile(
+ r"""
+ ^
+ # unicode_language_id = unicode_language_subtag
+ # unicode_language_subtag = alpha{2,3} | alpha{5,8}
+ (?P<language>[a-z]{2,3}|[a-z]{5,8})
+
+ # (sep unicode_script_subtag)?
+ # unicode_script_subtag = alpha{4}
+ (?:-(?P<script>[a-z]{4}))?
+
+ # (sep unicode_region_subtag)?
+ # unicode_region_subtag = (alpha{2} | digit{3})
+ (?:-(?P<region>([a-z]{2}|[0-9]{3})))?
+
+ # (sep unicode_variant_subtag)*
+ # unicode_variant_subtag = (alphanum{5,8} | digit alphanum{3})
+ (?P<variants>(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))+)?
+ $
+ """,
+ re.IGNORECASE | re.VERBOSE,
+ )
+
+ # CLDR uses "_" as the separator for some elements. Replace it with "-".
+ def bcp47_id(cldr_id):
+ return cldr_id.replace("_", "-")
+
+ # Return the tuple (language, script, region, variants) and assert all
+ # subtags are in canonical case.
+ def bcp47_canonical(language, script, region, variants):
+ # Canonical case for language subtags is lower case.
+ assert language is None or language.lower() == language
+
+ # Canonical case for script subtags is title case.
+ assert script is None or script.title() == script
+
+ # Canonical case for region subtags is upper case.
+ assert region is None or region.upper() == region
+
+ # Canonical case for variant subtags is lower case.
+ assert variants is None or variants.lower() == variants
+
+ return (language, script, region, variants[1:] if variants else None)
+
+ # Language ids are interpreted as multi-maps in
+ # <https://www.unicode.org/reports/tr35/#LocaleId_Canonicalization>.
+ #
+ # See UTS35, §Annex C, Definitions - 1. Multimap interpretation.
+ def language_id_to_multimap(language_id):
+ match = re_unicode_language_id.match(language_id)
+ assert (
+ match is not None
+ ), f"{language_id} invalid Unicode BCP 47 locale identifier"
+
+ canonical_language_id = bcp47_canonical(
+ *match.group("language", "script", "region", "variants")
+ )
+ (language, _, _, _) = canonical_language_id
+
+ # Normalize "und" language to None, but keep the rest as is.
+ return (language if language != "und" else None,) + canonical_language_id[1:]
+
+ rules = {}
+ territory_exception_rules = {}
+
+ tree = ET.parse(core_file.open("common/supplemental/supplementalMetadata.xml"))
+
+ # Load the rules from supplementalMetadata.xml.
+ #
+ # See UTS35, §Annex C, Definitions - 2. Alias elements.
+ # See UTS35, §Annex C, Preprocessing.
+ for alias_name in [
+ "languageAlias",
+ "scriptAlias",
+ "territoryAlias",
+ "variantAlias",
+ ]:
+ for alias in tree.iterfind(".//" + alias_name):
+ # Replace '_' by '-'.
+ type = bcp47_id(alias.get("type"))
+ replacement = bcp47_id(alias.get("replacement"))
+
+ # Prefix with "und-".
+ if alias_name != "languageAlias":
+ type = "und-" + type
+
+ # Discard all rules where the type is an invalid languageId.
+ if re_unicode_language_id.match(type) is None:
+ continue
+
+ type = language_id_to_multimap(type)
+
+ # Multiple, whitespace-separated territory replacements may be present.
+ if alias_name == "territoryAlias" and " " in replacement:
+ replacements = replacement.split(" ")
+ replacement_list = [
+ language_id_to_multimap("und-" + r) for r in replacements
+ ]
+
+ assert (
+ type not in territory_exception_rules
+ ), f"Duplicate alias rule: {type}"
+
+ territory_exception_rules[type] = replacement_list
+
+ # The first element is the default territory replacement.
+ replacement = replacements[0]
+
+ # Prefix with "und-".
+ if alias_name != "languageAlias":
+ replacement = "und-" + replacement
+
+ replacement = language_id_to_multimap(replacement)
+
+ assert type not in rules, f"Duplicate alias rule: {type}"
+
+ rules[type] = replacement
+
+ # Helper class for pattern matching.
+ class AnyClass:
+ def __eq__(self, obj):
+ return obj is not None
+
+ Any = AnyClass()
+
+ modified_rules = True
+ loop_count = 0
+
+ while modified_rules:
+ modified_rules = False
+ loop_count += 1
+
+ # UTS 35 defines that canonicalization is applied until a fixed point has
+ # been reached. This iterative application of the canonicalization algorithm
+ # is only needed for a relatively small set of rules, so we can precompute
+ # the transitive closure of all rules here and then perform a single pass
+ # when canonicalizing language tags at runtime.
+ transitive_rules = {}
+
+ # Compute the transitive closure.
+ # Any case which currently doesn't occur in the CLDR sources isn't supported
+ # and will lead to throwing an error.
+ for (type, replacement) in rules.items():
+ (language, script, region, variants) = type
+ (r_language, r_script, r_region, r_variants) = replacement
+
+ for (i_type, i_replacement) in rules.items():
+ (i_language, i_script, i_region, i_variants) = i_type
+ (i_r_language, i_r_script, i_r_region, i_r_variants) = i_replacement
+
+ if i_language is not None and i_language == r_language:
+ # This case currently only occurs when neither script nor region
+ # subtags are present. A single variant subtags may be present
+ # in |type|. And |i_type| definitely has a single variant subtag.
+ # Should this ever change, update this code accordingly.
+ assert type == (Any, None, None, None) or type == (
+ Any,
+ None,
+ None,
+ Any,
+ )
+ assert replacement == (Any, None, None, None)
+ assert i_type == (Any, None, None, Any)
+ assert i_replacement == (Any, None, None, None)
+
+ # This case happens for the rules
+ # "zh-guoyu -> zh",
+ # "zh-hakka -> hak", and
+ # "und-hakka -> und".
+ # Given the possible input "zh-guoyu-hakka", the first rule will
+ # change it to "zh-hakka", and then the second rule can be
+ # applied. (The third rule isn't applied ever.)
+ #
+ # Let's assume there's a hypothetical rule
+ # "zh-aaaaa" -> "en"
+ # And we have the input "zh-aaaaa-hakka", then "zh-aaaaa -> en"
+ # is applied before "zh-hakka -> hak", because rules are sorted
+ # alphabetically. That means the overall result is "en":
+ # "zh-aaaaa-hakka" is first canonicalized to "en-hakka" and then
+ # "hakka" is removed through the third rule.
+ #
+ # No current rule requires to handle this special case, so we
+ # don't yet support it.
+ assert variants is None or variants <= i_variants
+
+ # Combine all variants and remove duplicates.
+ vars = set(
+ i_variants.split("-")
+ + (variants.split("-") if variants else [])
+ )
+
+ # Add the variants alphabetically sorted.
+ n_type = (language, None, None, "-".join(sorted(vars)))
+
+ assert (
+ n_type not in transitive_rules
+ or transitive_rules[n_type] == i_replacement
+ )
+ transitive_rules[n_type] = i_replacement
+
+ continue
+
+ if i_script is not None and i_script == r_script:
+ # This case currently doesn't occur, so we don't yet support it.
+ raise ValueError(
+ f"{type} -> {replacement} :: {i_type} -> {i_replacement}"
+ )
+ if i_region is not None and i_region == r_region:
+ # This case currently only applies for sign language
+ # replacements. Similar to the language subtag case any other
+ # combination isn't currently supported.
+ assert type == (None, None, Any, None)
+ assert replacement == (None, None, Any, None)
+ assert i_type == ("sgn", None, Any, None)
+ assert i_replacement == (Any, None, None, None)
+
+ n_type = ("sgn", None, region, None)
+
+ assert n_type not in transitive_rules
+ transitive_rules[n_type] = i_replacement
+
+ continue
+
+ if i_variants is not None and i_variants == r_variants:
+ # This case currently doesn't occur, so we don't yet support it.
+ raise ValueError(
+ f"{type} -> {replacement} :: {i_type} -> {i_replacement}"
+ )
+
+ # Ensure there are no contradicting rules.
+ assert all(
+ rules[type] == replacement
+ for (type, replacement) in transitive_rules.items()
+ if type in rules
+ )
+
+ # If |transitive_rules| is not a subset of |rules|, new rules will be added.
+ modified_rules = not (transitive_rules.keys() <= rules.keys())
+
+ # Ensure we only have to iterate more than once for the "guoyo-{hakka,xiang}"
+ # case. Failing this assertion means either there's a bug when computing the
+ # stop condition of this loop or a new kind of legacy language tags was added.
+ if modified_rules and loop_count > 1:
+ new_rules = {k for k in transitive_rules.keys() if k not in rules}
+ for k in new_rules:
+ assert k == (Any, None, None, "guoyu-hakka") or k == (
+ Any,
+ None,
+ None,
+ "guoyu-xiang",
+ )
+
+ # Merge the transitive rules.
+ rules.update(transitive_rules)
+
+ # Computes the size of the union of all field value sets.
+ def multi_map_size(locale_id):
+ (language, script, region, variants) = locale_id
+
+ return (
+ (1 if language is not None else 0)
+ + (1 if script is not None else 0)
+ + (1 if region is not None else 0)
+ + (len(variants.split("-")) if variants is not None else 0)
+ )
+
+ # Dictionary of legacy mappings, contains raw rules, e.g.
+ # (None, None, None, "hepburn-heploc") -> (None, None, None, "alalc97").
+ legacy_mappings = {}
+
+ # Dictionary of simple language subtag mappings, e.g. "in" -> "id".
+ language_mappings = {}
+
+ # Dictionary of complex language subtag mappings, modifying more than one
+ # subtag, e.g. "sh" -> ("sr", "Latn", None) and "cnr" -> ("sr", None, "ME").
+ complex_language_mappings = {}
+
+ # Dictionary of simple script subtag mappings, e.g. "Qaai" -> "Zinh".
+ script_mappings = {}
+
+ # Dictionary of simple region subtag mappings, e.g. "DD" -> "DE".
+ region_mappings = {}
+
+ # Dictionary of complex region subtag mappings, containing more than one
+ # replacement, e.g. "SU" -> ("RU", ["AM", "AZ", "BY", ...]).
+ complex_region_mappings = {}
+
+ # Dictionary of aliased variant subtags to a tuple of preferred replacement
+ # type and replacement, e.g. "arevela" -> ("language", "hy") or
+ # "aaland" -> ("region", "AX") or "heploc" -> ("variant", "alalc97").
+ variant_mappings = {}
+
+ # Preprocess all rules so we can perform a single lookup per subtag at runtime.
+ for (type, replacement) in rules.items():
+ (language, script, region, variants) = type
+ (r_language, r_script, r_region, r_variants) = replacement
+
+ type_map_size = multi_map_size(type)
+
+ # Most mappings are one-to-one and can be encoded through lookup tables.
+ if type_map_size == 1:
+ if language is not None:
+ assert r_language is not None, "Can't remove a language subtag"
+
+ # We don't yet support this case.
+ assert (
+ r_variants is None
+ ), f"Unhandled variant replacement in language alias: {replacement}"
+
+ if replacement == (Any, None, None, None):
+ language_mappings[language] = r_language
+ else:
+ complex_language_mappings[language] = replacement[:-1]
+ elif script is not None:
+ # We don't support removing script subtags.
+ assert (
+ r_script is not None
+ ), f"Can't remove a script subtag: {replacement}"
+
+ # We only support one-to-one script mappings for now.
+ assert replacement == (
+ None,
+ Any,
+ None,
+ None,
+ ), f"Unhandled replacement in script alias: {replacement}"
+
+ script_mappings[script] = r_script
+ elif region is not None:
+ # We don't support removing region subtags.
+ assert (
+ r_region is not None
+ ), f"Can't remove a region subtag: {replacement}"
+
+ # We only support one-to-one region mappings for now.
+ assert replacement == (
+ None,
+ None,
+ Any,
+ None,
+ ), f"Unhandled replacement in region alias: {replacement}"
+
+ if type not in territory_exception_rules:
+ region_mappings[region] = r_region
+ else:
+ complex_region_mappings[region] = [
+ r_region
+ for (_, _, r_region, _) in territory_exception_rules[type]
+ ]
+ else:
+ assert variants is not None
+ assert len(variants.split("-")) == 1
+
+ # We only support one-to-one variant mappings for now.
+ assert (
+ multi_map_size(replacement) <= 1
+ ), f"Unhandled replacement in variant alias: {replacement}"
+
+ if r_language is not None:
+ variant_mappings[variants] = ("language", r_language)
+ elif r_script is not None:
+ variant_mappings[variants] = ("script", r_script)
+ elif r_region is not None:
+ variant_mappings[variants] = ("region", r_region)
+ elif r_variants is not None:
+ assert len(r_variants.split("-")) == 1
+ variant_mappings[variants] = ("variant", r_variants)
+ else:
+ variant_mappings[variants] = None
+ else:
+ # Alias rules which have multiple input fields must be processed
+ # first. This applies only to a handful of rules, so our generated
+ # code adds fast paths to skip these rules in the common case.
+
+ # Case 1: Language and at least one variant subtag.
+ if language is not None and variants is not None:
+ pass
+
+ # Case 2: Sign language and a region subtag.
+ elif language == "sgn" and region is not None:
+ pass
+
+ # Case 3: "hepburn-heploc" to "alalc97" canonicalization.
+ elif (
+ language is None
+ and variants is not None
+ and len(variants.split("-")) == 2
+ ):
+ pass
+
+ # Any other combination is currently unsupported.
+ else:
+ raise ValueError(f"{type} -> {replacement}")
+
+ legacy_mappings[type] = replacement
+
+ tree = ET.parse(core_file.open("common/supplemental/likelySubtags.xml"))
+
+ likely_subtags = {}
+
+ for likely_subtag in tree.iterfind(".//likelySubtag"):
+ from_tag = bcp47_id(likely_subtag.get("from"))
+ from_match = re_unicode_language_id.match(from_tag)
+ assert (
+ from_match is not None
+ ), f"{from_tag} invalid Unicode BCP 47 locale identifier"
+ assert (
+ from_match.group("variants") is None
+ ), f"unexpected variant subtags in {from_tag}"
+
+ to_tag = bcp47_id(likely_subtag.get("to"))
+ to_match = re_unicode_language_id.match(to_tag)
+ assert (
+ to_match is not None
+ ), f"{to_tag} invalid Unicode BCP 47 locale identifier"
+ assert (
+ to_match.group("variants") is None
+ ), f"unexpected variant subtags in {to_tag}"
+
+ from_canonical = bcp47_canonical(
+ *from_match.group("language", "script", "region", "variants")
+ )
+
+ to_canonical = bcp47_canonical(
+ *to_match.group("language", "script", "region", "variants")
+ )
+
+ # Remove the empty variant subtags.
+ from_canonical = from_canonical[:-1]
+ to_canonical = to_canonical[:-1]
+
+ likely_subtags[from_canonical] = to_canonical
+
+ complex_region_mappings_final = {}
+
+ for (deprecated_region, replacements) in complex_region_mappings.items():
+ # Find all likely subtag entries which don't already contain a region
+ # subtag and whose target region is in the list of replacement regions.
+ region_likely_subtags = [
+ (from_language, from_script, to_region)
+ for (
+ (from_language, from_script, from_region),
+ (_, _, to_region),
+ ) in likely_subtags.items()
+ if from_region is None and to_region in replacements
+ ]
+
+ # The first replacement entry is the default region.
+ default = replacements[0]
+
+ # Find all likely subtag entries whose region matches the default region.
+ default_replacements = {
+ (language, script)
+ for (language, script, region) in region_likely_subtags
+ if region == default
+ }
+
+ # And finally find those entries which don't use the default region.
+ # These are the entries we're actually interested in, because those need
+ # to be handled specially when selecting the correct preferred region.
+ non_default_replacements = [
+ (language, script, region)
+ for (language, script, region) in region_likely_subtags
+ if (language, script) not in default_replacements
+ ]
+
+ # If there are no non-default replacements, we can handle the region as
+ # part of the simple region mapping.
+ if non_default_replacements:
+ complex_region_mappings_final[deprecated_region] = (
+ default,
+ non_default_replacements,
+ )
+ else:
+ region_mappings[deprecated_region] = default
+
+ return {
+ "legacyMappings": legacy_mappings,
+ "languageMappings": language_mappings,
+ "complexLanguageMappings": complex_language_mappings,
+ "scriptMappings": script_mappings,
+ "regionMappings": region_mappings,
+ "complexRegionMappings": complex_region_mappings_final,
+ "variantMappings": variant_mappings,
+ "likelySubtags": likely_subtags,
+ }
+
+
+def readUnicodeExtensions(core_file):
+ import xml.etree.ElementTree as ET
+
+ # Match all xml-files in the BCP 47 directory.
+ bcpFileRE = re.compile(r"^common/bcp47/.+\.xml$")
+
+ # https://www.unicode.org/reports/tr35/#Unicode_locale_identifier
+ #
+ # type = alphanum{3,8} (sep alphanum{3,8})* ;
+ typeRE = re.compile(r"^[a-z0-9]{3,8}(-[a-z0-9]{3,8})*$")
+
+ # https://www.unicode.org/reports/tr35/#Unicode_language_identifier
+ #
+ # unicode_region_subtag = alpha{2} ;
+ alphaRegionRE = re.compile(r"^[A-Z]{2}$", re.IGNORECASE)
+
+ # Mapping from Unicode extension types to dict of deprecated to
+ # preferred values.
+ mapping = {
+ # Unicode BCP 47 U Extension
+ "u": {},
+ # Unicode BCP 47 T Extension
+ "t": {},
+ }
+
+ def readBCP47File(file):
+ tree = ET.parse(file)
+ for keyword in tree.iterfind(".//keyword/key"):
+ extension = keyword.get("extension", "u")
+ assert (
+ extension == "u" or extension == "t"
+ ), "unknown extension type: {}".format(extension)
+
+ extension_name = keyword.get("name")
+
+ for type in keyword.iterfind("type"):
+ # <https://unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files>:
+ #
+ # The key or type name used by Unicode locale extension with 'u' extension
+ # syntax or the 't' extensions syntax. When alias below is absent, this name
+ # can be also used with the old style "@key=type" syntax.
+ name = type.get("name")
+
+ # Ignore the special name:
+ # - <https://unicode.org/reports/tr35/#CODEPOINTS>
+ # - <https://unicode.org/reports/tr35/#REORDER_CODE>
+ # - <https://unicode.org/reports/tr35/#RG_KEY_VALUE>
+ # - <https://unicode.org/reports/tr35/#SCRIPT_CODE>
+ # - <https://unicode.org/reports/tr35/#SUBDIVISION_CODE>
+ # - <https://unicode.org/reports/tr35/#PRIVATE_USE>
+ if name in (
+ "CODEPOINTS",
+ "REORDER_CODE",
+ "RG_KEY_VALUE",
+ "SCRIPT_CODE",
+ "SUBDIVISION_CODE",
+ "PRIVATE_USE",
+ ):
+ continue
+
+ # All other names should match the 'type' production.
+ assert (
+ typeRE.match(name) is not None
+ ), "{} matches the 'type' production".format(name)
+
+ # <https://unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files>:
+ #
+ # The preferred value of the deprecated key, type or attribute element.
+ # When a key, type or attribute element is deprecated, this attribute is
+ # used for specifying a new canonical form if available.
+ preferred = type.get("preferred")
+
+ # <https://unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files>:
+ #
+ # The BCP 47 form is the canonical form, and recommended. Other aliases are
+ # included only for backwards compatibility.
+ alias = type.get("alias")
+
+ # <https://unicode.org/reports/tr35/#Canonical_Unicode_Locale_Identifiers>
+ #
+ # Use the bcp47 data to replace keys, types, tfields, and tvalues by their
+ # canonical forms. See Section 3.6.4 U Extension Data Files) and Section
+ # 3.7.1 T Extension Data Files. The aliases are in the alias attribute
+ # value, while the canonical is in the name attribute value.
+
+ # 'preferred' contains the new preferred name, 'alias' the compatibility
+ # name, but then there's this entry where 'preferred' and 'alias' are the
+ # same. So which one to choose? Assume 'preferred' is the actual canonical
+ # name.
+ #
+ # <type name="islamicc"
+ # description="Civil (algorithmic) Arabic calendar"
+ # deprecated="true"
+ # preferred="islamic-civil"
+ # alias="islamic-civil"/>
+
+ if preferred is not None:
+ assert typeRE.match(preferred), preferred
+ mapping[extension].setdefault(extension_name, {})[name] = preferred
+
+ if alias is not None:
+ for alias_name in alias.lower().split(" "):
+ # Ignore alias entries which don't match the 'type' production.
+ if typeRE.match(alias_name) is None:
+ continue
+
+ # See comment above when 'alias' and 'preferred' are both present.
+ if (
+ preferred is not None
+ and name in mapping[extension][extension_name]
+ ):
+ continue
+
+ # Skip over entries where 'name' and 'alias' are equal.
+ #
+ # <type name="pst8pdt"
+ # description="POSIX style time zone for US Pacific Time"
+ # alias="PST8PDT"
+ # since="1.8"/>
+ if name == alias_name:
+ continue
+
+ mapping[extension].setdefault(extension_name, {})[
+ alias_name
+ ] = name
+
+ def readSupplementalMetadata(file):
+ # Find subdivision and region replacements.
+ #
+ # <https://www.unicode.org/reports/tr35/#Canonical_Unicode_Locale_Identifiers>
+ #
+ # Replace aliases in special key values:
+ # - If there is an 'sd' or 'rg' key, replace any subdivision alias
+ # in its value in the same way, using subdivisionAlias data.
+ tree = ET.parse(file)
+ for alias in tree.iterfind(".//subdivisionAlias"):
+ type = alias.get("type")
+ assert (
+ typeRE.match(type) is not None
+ ), "{} matches the 'type' production".format(type)
+
+ # Take the first replacement when multiple ones are present.
+ replacement = alias.get("replacement").split(" ")[0].lower()
+
+ # Append "zzzz" if the replacement is a two-letter region code.
+ if alphaRegionRE.match(replacement) is not None:
+ replacement += "zzzz"
+
+ # Assert the replacement is syntactically correct.
+ assert (
+ typeRE.match(replacement) is not None
+ ), "replacement {} matches the 'type' production".format(replacement)
+
+ # 'subdivisionAlias' applies to 'rg' and 'sd' keys.
+ mapping["u"].setdefault("rg", {})[type] = replacement
+ mapping["u"].setdefault("sd", {})[type] = replacement
+
+ for name in core_file.namelist():
+ if bcpFileRE.match(name):
+ readBCP47File(core_file.open(name))
+
+ readSupplementalMetadata(
+ core_file.open("common/supplemental/supplementalMetadata.xml")
+ )
+
+ return {
+ "unicodeMappings": mapping["u"],
+ "transformMappings": mapping["t"],
+ }
+
+
+def writeCLDRLanguageTagData(println, data, url):
+ """Writes the language tag data to the Intl data file."""
+
+ println(generatedFileWarning)
+ println("// Version: CLDR-{}".format(data["version"]))
+ println("// URL: {}".format(url))
+
+ println(
+ """
+#include "mozilla/Assertions.h"
+#include "mozilla/Span.h"
+#include "mozilla/TextUtils.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <iterator>
+#include <string>
+#include <type_traits>
+
+#include "mozilla/intl/Locale.h"
+
+using namespace mozilla::intl::LanguageTagLimits;
+
+template <size_t Length, size_t TagLength, size_t SubtagLength>
+static inline bool HasReplacement(
+ const char (&subtags)[Length][TagLength],
+ const mozilla::intl::LanguageTagSubtag<SubtagLength>& subtag) {
+ MOZ_ASSERT(subtag.Length() == TagLength - 1,
+ "subtag must have the same length as the list of subtags");
+
+ const char* ptr = subtag.Span().data();
+ return std::binary_search(std::begin(subtags), std::end(subtags), ptr,
+ [](const char* a, const char* b) {
+ return memcmp(a, b, TagLength - 1) < 0;
+ });
+}
+
+template <size_t Length, size_t TagLength, size_t SubtagLength>
+static inline const char* SearchReplacement(
+ const char (&subtags)[Length][TagLength], const char* (&aliases)[Length],
+ const mozilla::intl::LanguageTagSubtag<SubtagLength>& subtag) {
+ MOZ_ASSERT(subtag.Length() == TagLength - 1,
+ "subtag must have the same length as the list of subtags");
+
+ const char* ptr = subtag.Span().data();
+ auto p = std::lower_bound(std::begin(subtags), std::end(subtags), ptr,
+ [](const char* a, const char* b) {
+ return memcmp(a, b, TagLength - 1) < 0;
+ });
+ if (p != std::end(subtags) && memcmp(*p, ptr, TagLength - 1) == 0) {
+ return aliases[std::distance(std::begin(subtags), p)];
+ }
+ return nullptr;
+}
+
+#ifdef DEBUG
+static bool IsAsciiLowercaseAlphanumeric(char c) {
+ return mozilla::IsAsciiLowercaseAlpha(c) || mozilla::IsAsciiDigit(c);
+}
+
+static bool IsAsciiLowercaseAlphanumericOrDash(char c) {
+ return IsAsciiLowercaseAlphanumeric(c) || c == '-';
+}
+
+static bool IsCanonicallyCasedLanguageTag(mozilla::Span<const char> span) {
+ return std::all_of(span.begin(), span.end(),
+ mozilla::IsAsciiLowercaseAlpha<char>);
+}
+
+static bool IsCanonicallyCasedScriptTag(mozilla::Span<const char> span) {
+ return mozilla::IsAsciiUppercaseAlpha(span[0]) &&
+ std::all_of(span.begin() + 1, span.end(),
+ mozilla::IsAsciiLowercaseAlpha<char>);
+}
+
+static bool IsCanonicallyCasedRegionTag(mozilla::Span<const char> span) {
+ return std::all_of(span.begin(), span.end(),
+ mozilla::IsAsciiUppercaseAlpha<char>) ||
+ std::all_of(span.begin(), span.end(), mozilla::IsAsciiDigit<char>);
+}
+
+static bool IsCanonicallyCasedVariantTag(mozilla::Span<const char> span) {
+ return std::all_of(span.begin(), span.end(), IsAsciiLowercaseAlphanumeric);
+}
+
+static bool IsCanonicallyCasedUnicodeKey(mozilla::Span<const char> key) {
+ return std::all_of(key.begin(), key.end(), IsAsciiLowercaseAlphanumeric);
+}
+
+static bool IsCanonicallyCasedUnicodeType(mozilla::Span<const char> type) {
+ return std::all_of(type.begin(), type.end(),
+ IsAsciiLowercaseAlphanumericOrDash);
+}
+
+static bool IsCanonicallyCasedTransformKey(mozilla::Span<const char> key) {
+ return std::all_of(key.begin(), key.end(), IsAsciiLowercaseAlphanumeric);
+}
+
+static bool IsCanonicallyCasedTransformType(mozilla::Span<const char> type) {
+ return std::all_of(type.begin(), type.end(),
+ IsAsciiLowercaseAlphanumericOrDash);
+}
+#endif
+""".rstrip()
+ )
+
+ source = "CLDR Supplemental Data, version {}".format(data["version"])
+ legacy_mappings = data["legacyMappings"]
+ language_mappings = data["languageMappings"]
+ complex_language_mappings = data["complexLanguageMappings"]
+ script_mappings = data["scriptMappings"]
+ region_mappings = data["regionMappings"]
+ complex_region_mappings = data["complexRegionMappings"]
+ variant_mappings = data["variantMappings"]
+ unicode_mappings = data["unicodeMappings"]
+ transform_mappings = data["transformMappings"]
+
+ # unicode_language_subtag = alpha{2,3} | alpha{5,8} ;
+ language_maxlength = 8
+
+ # unicode_script_subtag = alpha{4} ;
+ script_maxlength = 4
+
+ # unicode_region_subtag = (alpha{2} | digit{3}) ;
+ region_maxlength = 3
+
+ writeMappingsBinarySearch(
+ println,
+ "LanguageMapping",
+ "LanguageSubtag&",
+ "language",
+ "IsStructurallyValidLanguageTag",
+ "IsCanonicallyCasedLanguageTag",
+ language_mappings,
+ language_maxlength,
+ "Mappings from language subtags to preferred values.",
+ source,
+ url,
+ )
+ writeMappingsBinarySearch(
+ println,
+ "ComplexLanguageMapping",
+ "const LanguageSubtag&",
+ "language",
+ "IsStructurallyValidLanguageTag",
+ "IsCanonicallyCasedLanguageTag",
+ complex_language_mappings.keys(),
+ language_maxlength,
+ "Language subtags with complex mappings.",
+ source,
+ url,
+ )
+ writeMappingsBinarySearch(
+ println,
+ "ScriptMapping",
+ "ScriptSubtag&",
+ "script",
+ "IsStructurallyValidScriptTag",
+ "IsCanonicallyCasedScriptTag",
+ script_mappings,
+ script_maxlength,
+ "Mappings from script subtags to preferred values.",
+ source,
+ url,
+ )
+ writeMappingsBinarySearch(
+ println,
+ "RegionMapping",
+ "RegionSubtag&",
+ "region",
+ "IsStructurallyValidRegionTag",
+ "IsCanonicallyCasedRegionTag",
+ region_mappings,
+ region_maxlength,
+ "Mappings from region subtags to preferred values.",
+ source,
+ url,
+ )
+ writeMappingsBinarySearch(
+ println,
+ "ComplexRegionMapping",
+ "const RegionSubtag&",
+ "region",
+ "IsStructurallyValidRegionTag",
+ "IsCanonicallyCasedRegionTag",
+ complex_region_mappings.keys(),
+ region_maxlength,
+ "Region subtags with complex mappings.",
+ source,
+ url,
+ )
+
+ writeComplexLanguageTagMappings(
+ println,
+ complex_language_mappings,
+ "Language subtags with complex mappings.",
+ source,
+ url,
+ )
+ writeComplexRegionTagMappings(
+ println,
+ complex_region_mappings,
+ "Region subtags with complex mappings.",
+ source,
+ url,
+ )
+
+ writeVariantTagMappings(
+ println,
+ variant_mappings,
+ "Mappings from variant subtags to preferred values.",
+ source,
+ url,
+ )
+
+ writeLegacyMappingsFunction(
+ println, legacy_mappings, "Canonicalize legacy locale identifiers.", source, url
+ )
+
+ writeSignLanguageMappingsFunction(
+ println, legacy_mappings, "Mappings from legacy sign languages.", source, url
+ )
+
+ writeUnicodeExtensionsMappings(println, unicode_mappings, "Unicode")
+ writeUnicodeExtensionsMappings(println, transform_mappings, "Transform")
+
+
+def writeCLDRLanguageTagLikelySubtagsTest(println, data, url):
+ """Writes the likely-subtags test file."""
+
+ println(generatedFileWarning)
+
+ source = "CLDR Supplemental Data, version {}".format(data["version"])
+ language_mappings = data["languageMappings"]
+ complex_language_mappings = data["complexLanguageMappings"]
+ script_mappings = data["scriptMappings"]
+ region_mappings = data["regionMappings"]
+ complex_region_mappings = data["complexRegionMappings"]
+ likely_subtags = data["likelySubtags"]
+
+ def bcp47(tag):
+ (language, script, region) = tag
+ return "{}{}{}".format(
+ language, "-" + script if script else "", "-" + region if region else ""
+ )
+
+ def canonical(tag):
+ (language, script, region) = tag
+
+ # Map deprecated language subtags.
+ if language in language_mappings:
+ language = language_mappings[language]
+ elif language in complex_language_mappings:
+ (language2, script2, region2) = complex_language_mappings[language]
+ (language, script, region) = (
+ language2,
+ script if script else script2,
+ region if region else region2,
+ )
+
+ # Map deprecated script subtags.
+ if script in script_mappings:
+ script = script_mappings[script]
+
+ # Map deprecated region subtags.
+ if region in region_mappings:
+ region = region_mappings[region]
+ else:
+ # Assume no complex region mappings are needed for now.
+ assert (
+ region not in complex_region_mappings
+ ), "unexpected region with complex mappings: {}".format(region)
+
+ return (language, script, region)
+
+ # https://unicode.org/reports/tr35/#Likely_Subtags
+
+ def addLikelySubtags(tag):
+ # Step 1: Canonicalize.
+ (language, script, region) = canonical(tag)
+ if script == "Zzzz":
+ script = None
+ if region == "ZZ":
+ region = None
+
+ # Step 2: Lookup.
+ searches = (
+ (language, script, region),
+ (language, None, region),
+ (language, script, None),
+ (language, None, None),
+ ("und", script, None),
+ )
+ search = next(search for search in searches if search in likely_subtags)
+
+ (language_s, script_s, region_s) = search
+ (language_m, script_m, region_m) = likely_subtags[search]
+
+ # Step 3: Return.
+ return (
+ language if language != language_s else language_m,
+ script if script != script_s else script_m,
+ region if region != region_s else region_m,
+ )
+
+ # https://unicode.org/reports/tr35/#Likely_Subtags
+ def removeLikelySubtags(tag):
+ # Step 1: Add likely subtags.
+ max = addLikelySubtags(tag)
+
+ # Step 2: Remove variants (doesn't apply here).
+
+ # Step 3: Find a match.
+ (language, script, region) = max
+ for trial in (
+ (language, None, None),
+ (language, None, region),
+ (language, script, None),
+ ):
+ if addLikelySubtags(trial) == max:
+ return trial
+
+ # Step 4: Return maximized if no match found.
+ return max
+
+ def likely_canonical(from_tag, to_tag):
+ # Canonicalize the input tag.
+ from_tag = canonical(from_tag)
+
+ # Update the expected result if necessary.
+ if from_tag in likely_subtags:
+ to_tag = likely_subtags[from_tag]
+
+ # Canonicalize the expected output.
+ to_canonical = canonical(to_tag)
+
+ # Sanity check: This should match the result of |addLikelySubtags|.
+ assert to_canonical == addLikelySubtags(from_tag)
+
+ return to_canonical
+
+ # |likely_subtags| contains non-canonicalized tags, so canonicalize it first.
+ likely_subtags_canonical = {
+ k: likely_canonical(k, v) for (k, v) in likely_subtags.items()
+ }
+
+ # Add test data for |Intl.Locale.prototype.maximize()|.
+ writeMappingsVar(
+ println,
+ {bcp47(k): bcp47(v) for (k, v) in likely_subtags_canonical.items()},
+ "maxLikelySubtags",
+ "Extracted from likelySubtags.xml.",
+ source,
+ url,
+ )
+
+ # Use the maximalized tags as the input for the remove likely-subtags test.
+ minimized = {
+ tag: removeLikelySubtags(tag) for tag in likely_subtags_canonical.values()
+ }
+
+ # Add test data for |Intl.Locale.prototype.minimize()|.
+ writeMappingsVar(
+ println,
+ {bcp47(k): bcp47(v) for (k, v) in minimized.items()},
+ "minLikelySubtags",
+ "Extracted from likelySubtags.xml.",
+ source,
+ url,
+ )
+
+ println(
+ """
+for (let [tag, maximal] of Object.entries(maxLikelySubtags)) {
+ assertEq(new Intl.Locale(tag).maximize().toString(), maximal);
+}"""
+ )
+
+ println(
+ """
+for (let [tag, minimal] of Object.entries(minLikelySubtags)) {
+ assertEq(new Intl.Locale(tag).minimize().toString(), minimal);
+}"""
+ )
+
+ println(
+ """
+if (typeof reportCompare === "function")
+ reportCompare(0, 0);"""
+ )
+
+
+def readCLDRVersionFromICU():
+ icuDir = os.path.join(topsrcdir, "intl/icu/source")
+ if not os.path.isdir(icuDir):
+ raise RuntimeError("not a directory: {}".format(icuDir))
+
+ reVersion = re.compile(r'\s*cldrVersion\{"(\d+(?:\.\d+)?)"\}')
+
+ for line in flines(os.path.join(icuDir, "data/misc/supplementalData.txt")):
+ m = reVersion.match(line)
+ if m:
+ version = m.group(1)
+ break
+
+ if version is None:
+ raise RuntimeError("can't resolve CLDR version")
+
+ return version
+
+
+def updateCLDRLangTags(args):
+ """Update the LanguageTagGenerated.cpp file."""
+ version = args.version
+ url = args.url
+ out = args.out
+ filename = args.file
+
+ # Determine current CLDR version from ICU.
+ if version is None:
+ version = readCLDRVersionFromICU()
+
+ url = url.replace("<VERSION>", version)
+
+ print("Arguments:")
+ print("\tCLDR version: %s" % version)
+ print("\tDownload url: %s" % url)
+ if filename is not None:
+ print("\tLocal CLDR core.zip file: %s" % filename)
+ print("\tOutput file: %s" % out)
+ print("")
+
+ data = {
+ "version": version,
+ }
+
+ def readFiles(cldr_file):
+ with ZipFile(cldr_file) as zip_file:
+ data.update(readSupplementalData(zip_file))
+ data.update(readUnicodeExtensions(zip_file))
+
+ print("Processing CLDR data...")
+ if filename is not None:
+ print("Always make sure you have the newest CLDR core.zip!")
+ with open(filename, "rb") as cldr_file:
+ readFiles(cldr_file)
+ else:
+ print("Downloading CLDR core.zip...")
+ with closing(urlopen(url)) as cldr_file:
+ cldr_data = io.BytesIO(cldr_file.read())
+ readFiles(cldr_data)
+
+ print("Writing Intl data...")
+ with io.open(out, mode="w", encoding="utf-8", newline="") as f:
+ println = partial(print, file=f)
+
+ writeCLDRLanguageTagData(println, data, url)
+
+ print("Writing Intl test data...")
+ js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__))
+ test_file = os.path.join(
+ js_src_builtin_intl_dir,
+ "../../tests/non262/Intl/Locale/likely-subtags-generated.js",
+ )
+ with io.open(test_file, mode="w", encoding="utf-8", newline="") as f:
+ println = partial(print, file=f)
+
+ println("// |reftest| skip-if(!this.hasOwnProperty('Intl'))")
+ writeCLDRLanguageTagLikelySubtagsTest(println, data, url)
+
+
+def flines(filepath, encoding="utf-8"):
+ """Open filepath and iterate over its content."""
+ with io.open(filepath, mode="r", encoding=encoding) as f:
+ for line in f:
+ yield line
+
+
+@total_ordering
+class Zone(object):
+ """Time zone with optional file name."""
+
+ def __init__(self, name, filename=""):
+ self.name = name
+ self.filename = filename
+
+ def __eq__(self, other):
+ return hasattr(other, "name") and self.name == other.name
+
+ def __lt__(self, other):
+ return self.name < other.name
+
+ def __hash__(self):
+ return hash(self.name)
+
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return self.name
+
+
+class TzDataDir(object):
+ """tzdata source from a directory."""
+
+ def __init__(self, obj):
+ self.name = partial(os.path.basename, obj)
+ self.resolve = partial(os.path.join, obj)
+ self.basename = os.path.basename
+ self.isfile = os.path.isfile
+ self.listdir = partial(os.listdir, obj)
+ self.readlines = flines
+
+
+class TzDataFile(object):
+ """tzdata source from a file (tar or gzipped)."""
+
+ def __init__(self, obj):
+ self.name = lambda: os.path.splitext(
+ os.path.splitext(os.path.basename(obj))[0]
+ )[0]
+ self.resolve = obj.getmember
+ self.basename = attrgetter("name")
+ self.isfile = tarfile.TarInfo.isfile
+ self.listdir = obj.getnames
+ self.readlines = partial(self._tarlines, obj)
+
+ def _tarlines(self, tar, m):
+ with closing(tar.extractfile(m)) as f:
+ for line in f:
+ yield line.decode("utf-8")
+
+
+def validateTimeZones(zones, links):
+ """Validate the zone and link entries."""
+ linkZones = set(links.keys())
+ intersect = linkZones.intersection(zones)
+ if intersect:
+ raise RuntimeError("Links also present in zones: %s" % intersect)
+
+ zoneNames = {z.name for z in zones}
+ linkTargets = set(links.values())
+ if not linkTargets.issubset(zoneNames):
+ raise RuntimeError(
+ "Link targets not found: %s" % linkTargets.difference(zoneNames)
+ )
+
+
+def partition(iterable, *predicates):
+ def innerPartition(pred, it):
+ it1, it2 = tee(it)
+ return (filter(pred, it1), filterfalse(pred, it2))
+
+ if len(predicates) == 0:
+ return iterable
+ (left, right) = innerPartition(predicates[0], iterable)
+ if len(predicates) == 1:
+ return (left, right)
+ return tuple([left] + list(partition(right, *predicates[1:])))
+
+
+def listIANAFiles(tzdataDir):
+ def isTzFile(d, m, f):
+ return m(f) and d.isfile(d.resolve(f))
+
+ return filter(
+ partial(isTzFile, tzdataDir, re.compile("^[a-z0-9]+$").match),
+ tzdataDir.listdir(),
+ )
+
+
+def readIANAFiles(tzdataDir, files):
+ """Read all IANA time zone files from the given iterable."""
+ nameSyntax = "[\w/+\-]+"
+ pZone = re.compile(r"Zone\s+(?P<name>%s)\s+.*" % nameSyntax)
+ pLink = re.compile(
+ r"Link\s+(?P<target>%s)\s+(?P<name>%s)(?:\s+#.*)?" % (nameSyntax, nameSyntax)
+ )
+
+ def createZone(line, fname):
+ match = pZone.match(line)
+ name = match.group("name")
+ return Zone(name, fname)
+
+ def createLink(line, fname):
+ match = pLink.match(line)
+ (name, target) = match.group("name", "target")
+ return (Zone(name, fname), target)
+
+ zones = set()
+ links = dict()
+ for filename in files:
+ filepath = tzdataDir.resolve(filename)
+ for line in tzdataDir.readlines(filepath):
+ if line.startswith("Zone"):
+ zones.add(createZone(line, filename))
+ if line.startswith("Link"):
+ (link, target) = createLink(line, filename)
+ links[link] = target
+
+ return (zones, links)
+
+
+def readIANATimeZones(tzdataDir, ignoreBackzone, ignoreFactory):
+ """Read the IANA time zone information from `tzdataDir`."""
+
+ backzoneFiles = {"backzone"}
+ (bkfiles, tzfiles) = partition(listIANAFiles(tzdataDir), backzoneFiles.__contains__)
+
+ # Read zone and link infos.
+ (zones, links) = readIANAFiles(tzdataDir, tzfiles)
+ (backzones, backlinks) = readIANAFiles(tzdataDir, bkfiles)
+
+ # Remove the placeholder time zone "Factory".
+ if ignoreFactory:
+ zones.remove(Zone("Factory"))
+
+ # Merge with backzone data.
+ if not ignoreBackzone:
+ zones |= backzones
+ links = {
+ name: target for name, target in links.items() if name not in backzones
+ }
+ links.update(backlinks)
+
+ validateTimeZones(zones, links)
+
+ return (zones, links)
+
+
+def readICUResourceFile(filename):
+ """Read an ICU resource file.
+
+ Yields (<table-name>, <startOrEnd>, <value>) for each table.
+ """
+
+ numberValue = r"-?\d+"
+ stringValue = r'".+?"'
+
+ def asVector(val):
+ return r"%s(?:\s*,\s*%s)*" % (val, val)
+
+ numberVector = asVector(numberValue)
+ stringVector = asVector(stringValue)
+
+ reNumberVector = re.compile(numberVector)
+ reStringVector = re.compile(stringVector)
+ reNumberValue = re.compile(numberValue)
+ reStringValue = re.compile(stringValue)
+
+ def parseValue(value):
+ m = reNumberVector.match(value)
+ if m:
+ return [int(v) for v in reNumberValue.findall(value)]
+ m = reStringVector.match(value)
+ if m:
+ return [v[1:-1] for v in reStringValue.findall(value)]
+ raise RuntimeError("unknown value type: %s" % value)
+
+ def extractValue(values):
+ if len(values) == 0:
+ return None
+ if len(values) == 1:
+ return values[0]
+ return values
+
+ def line(*args):
+ maybeMultiComments = r"(?:/\*[^*]*\*/)*"
+ maybeSingleComment = r"(?://.*)?"
+ lineStart = "^%s" % maybeMultiComments
+ lineEnd = "%s\s*%s$" % (maybeMultiComments, maybeSingleComment)
+ return re.compile(r"\s*".join(chain([lineStart], args, [lineEnd])))
+
+ tableName = r'(?P<quote>"?)(?P<name>.+?)(?P=quote)'
+ tableValue = r"(?P<value>%s|%s)" % (numberVector, stringVector)
+
+ reStartTable = line(tableName, r"\{")
+ reEndTable = line(r"\}")
+ reSingleValue = line(r",?", tableValue, r",?")
+ reCompactTable = line(tableName, r"\{", tableValue, r"\}")
+ reEmptyLine = line()
+
+ tables = []
+
+ def currentTable():
+ return "|".join(tables)
+
+ values = []
+ for line in flines(filename, "utf-8-sig"):
+ line = line.strip()
+ if line == "":
+ continue
+
+ m = reEmptyLine.match(line)
+ if m:
+ continue
+
+ m = reStartTable.match(line)
+ if m:
+ assert len(values) == 0
+ tables.append(m.group("name"))
+ continue
+
+ m = reEndTable.match(line)
+ if m:
+ yield (currentTable(), extractValue(values))
+ tables.pop()
+ values = []
+ continue
+
+ m = reCompactTable.match(line)
+ if m:
+ assert len(values) == 0
+ tables.append(m.group("name"))
+ yield (currentTable(), extractValue(parseValue(m.group("value"))))
+ tables.pop()
+ continue
+
+ m = reSingleValue.match(line)
+ if m and tables:
+ values.extend(parseValue(m.group("value")))
+ continue
+
+ raise RuntimeError("unknown entry: %s" % line)
+
+
+def readICUTimeZonesFromTimezoneTypes(icuTzDir):
+ """Read the ICU time zone information from `icuTzDir`/timezoneTypes.txt
+ and returns the tuple (zones, links).
+ """
+ typeMapTimeZoneKey = "timezoneTypes:table(nofallback)|typeMap|timezone|"
+ typeAliasTimeZoneKey = "timezoneTypes:table(nofallback)|typeAlias|timezone|"
+
+ def toTimeZone(name):
+ return Zone(name.replace(":", "/"))
+
+ zones = set()
+ links = dict()
+
+ for name, value in readICUResourceFile(os.path.join(icuTzDir, "timezoneTypes.txt")):
+ if name.startswith(typeMapTimeZoneKey):
+ zones.add(toTimeZone(name[len(typeMapTimeZoneKey) :]))
+ if name.startswith(typeAliasTimeZoneKey):
+ links[toTimeZone(name[len(typeAliasTimeZoneKey) :])] = value
+
+ validateTimeZones(zones, links)
+
+ return (zones, links)
+
+
+def readICUTimeZonesFromZoneInfo(icuTzDir):
+ """Read the ICU time zone information from `icuTzDir`/zoneinfo64.txt
+ and returns the tuple (zones, links).
+ """
+ zoneKey = "zoneinfo64:table(nofallback)|Zones:array|:table"
+ linkKey = "zoneinfo64:table(nofallback)|Zones:array|:int"
+ namesKey = "zoneinfo64:table(nofallback)|Names"
+
+ tzId = 0
+ tzLinks = dict()
+ tzNames = []
+
+ for name, value in readICUResourceFile(os.path.join(icuTzDir, "zoneinfo64.txt")):
+ if name == zoneKey:
+ tzId += 1
+ elif name == linkKey:
+ tzLinks[tzId] = int(value)
+ tzId += 1
+ elif name == namesKey:
+ tzNames.extend(value)
+
+ links = {Zone(tzNames[zone]): tzNames[target] for (zone, target) in tzLinks.items()}
+ zones = {Zone(v) for v in tzNames if Zone(v) not in links}
+
+ validateTimeZones(zones, links)
+
+ return (zones, links)
+
+
+def readICUTimeZones(icuDir, icuTzDir, ignoreFactory):
+ # zoneinfo64.txt contains the supported time zones by ICU. This data is
+ # generated from tzdata files, it doesn't include "backzone" in stock ICU.
+ (zoneinfoZones, zoneinfoLinks) = readICUTimeZonesFromZoneInfo(icuTzDir)
+
+ # timezoneTypes.txt contains the canonicalization information for ICU. This
+ # data is generated from CLDR files. It includes data about time zones from
+ # tzdata's "backzone" file.
+ (typesZones, typesLinks) = readICUTimeZonesFromTimezoneTypes(icuTzDir)
+
+ # Remove the placeholder time zone "Factory".
+ # See also <https://github.com/eggert/tz/blob/master/factory>.
+ if ignoreFactory:
+ zoneinfoZones.remove(Zone("Factory"))
+
+ # Remove the ICU placeholder time zone "Etc/Unknown".
+ # See also <https://unicode.org/reports/tr35/#Time_Zone_Identifiers>.
+ for zones in (zoneinfoZones, typesZones):
+ zones.remove(Zone("Etc/Unknown"))
+
+ # Remove any outdated ICU links.
+ for links in (zoneinfoLinks, typesLinks):
+ for zone in otherICULegacyLinks().keys():
+ if zone not in links:
+ raise KeyError(f"Can't remove non-existent link from '{zone}'")
+ del links[zone]
+
+ # Information in zoneinfo64 should be a superset of timezoneTypes.
+ def inZoneInfo64(zone):
+ return zone in zoneinfoZones or zone in zoneinfoLinks
+
+ notFoundInZoneInfo64 = [zone for zone in typesZones if not inZoneInfo64(zone)]
+ if notFoundInZoneInfo64:
+ raise RuntimeError(
+ "Missing time zones in zoneinfo64.txt: %s" % notFoundInZoneInfo64
+ )
+
+ notFoundInZoneInfo64 = [
+ zone for zone in typesLinks.keys() if not inZoneInfo64(zone)
+ ]
+ if notFoundInZoneInfo64:
+ raise RuntimeError(
+ "Missing time zones in zoneinfo64.txt: %s" % notFoundInZoneInfo64
+ )
+
+ # zoneinfo64.txt only defines the supported time zones by ICU, the canonicalization
+ # rules are defined through timezoneTypes.txt. Merge both to get the actual zones
+ # and links used by ICU.
+ icuZones = set(
+ chain(
+ (zone for zone in zoneinfoZones if zone not in typesLinks),
+ (zone for zone in typesZones),
+ )
+ )
+ icuLinks = dict(
+ chain(
+ (
+ (zone, target)
+ for (zone, target) in zoneinfoLinks.items()
+ if zone not in typesZones
+ ),
+ ((zone, target) for (zone, target) in typesLinks.items()),
+ )
+ )
+
+ return (icuZones, icuLinks)
+
+
+def readICULegacyZones(icuDir):
+ """Read the ICU legacy time zones from `icuTzDir`/tools/tzcode/icuzones
+ and returns the tuple (zones, links).
+ """
+ tzdir = TzDataDir(os.path.join(icuDir, "tools/tzcode"))
+
+ # Per spec we must recognize only IANA time zones and links, but ICU
+ # recognizes various legacy, non-IANA time zones and links. Compute these
+ # non-IANA time zones and links.
+
+ # Most legacy, non-IANA time zones and links are in the icuzones file.
+ (zones, links) = readIANAFiles(tzdir, ["icuzones"])
+
+ # Remove the ICU placeholder time zone "Etc/Unknown".
+ # See also <https://unicode.org/reports/tr35/#Time_Zone_Identifiers>.
+ zones.remove(Zone("Etc/Unknown"))
+
+ # A handful of non-IANA zones/links are not in icuzones and must be added
+ # manually so that we won't invoke ICU with them.
+ for (zone, target) in otherICULegacyLinks().items():
+ if zone in links:
+ if links[zone] != target:
+ raise KeyError(
+ f"Can't overwrite link '{zone} -> {links[zone]}' with '{target}'"
+ )
+ else:
+ print(
+ f"Info: Link '{zone} -> {target}' can be removed from otherICULegacyLinks()"
+ )
+ links[zone] = target
+
+ return (zones, links)
+
+
+def otherICULegacyLinks():
+ """The file `icuTzDir`/tools/tzcode/icuzones contains all ICU legacy time
+ zones with the exception of time zones which are removed by IANA after an
+ ICU release.
+
+ For example ICU 67 uses tzdata2018i, but tzdata2020b removed the link from
+ "US/Pacific-New" to "America/Los_Angeles". ICU standalone tzdata updates
+ don't include modified icuzones files, so we must manually record any IANA
+ modifications here.
+
+ After an ICU update, we can remove any no longer needed entries from this
+ function by checking if the relevant entries are now included in icuzones.
+ """
+
+ return {
+ # Current ICU is up-to-date with IANA, so this dict is empty.
+ }
+
+
+def icuTzDataVersion(icuTzDir):
+ """Read the ICU time zone version from `icuTzDir`/zoneinfo64.txt."""
+
+ def searchInFile(pattern, f):
+ p = re.compile(pattern)
+ for line in flines(f, "utf-8-sig"):
+ m = p.search(line)
+ if m:
+ return m.group(1)
+ return None
+
+ zoneinfo = os.path.join(icuTzDir, "zoneinfo64.txt")
+ if not os.path.isfile(zoneinfo):
+ raise RuntimeError("file not found: %s" % zoneinfo)
+ version = searchInFile("^//\s+tz version:\s+([0-9]{4}[a-z])$", zoneinfo)
+ if version is None:
+ raise RuntimeError(
+ "%s does not contain a valid tzdata version string" % zoneinfo
+ )
+ return version
+
+
+def findIncorrectICUZones(ianaZones, ianaLinks, icuZones, icuLinks, ignoreBackzone):
+ """Find incorrect ICU zone entries."""
+
+ def isIANATimeZone(zone):
+ return zone in ianaZones or zone in ianaLinks
+
+ def isICUTimeZone(zone):
+ return zone in icuZones or zone in icuLinks
+
+ def isICULink(zone):
+ return zone in icuLinks
+
+ # All IANA zones should be present in ICU.
+ missingTimeZones = [zone for zone in ianaZones if not isICUTimeZone(zone)]
+ # Normally zones in backzone are also present as links in one of the other
+ # time zone files. The only exception to this rule is the Asia/Hanoi time
+ # zone, this zone is only present in the backzone file.
+ expectedMissing = [] if ignoreBackzone else [Zone("Asia/Hanoi")]
+ if missingTimeZones != expectedMissing:
+ raise RuntimeError(
+ "Not all zones are present in ICU, did you forget "
+ "to run intl/update-tzdata.sh? %s" % missingTimeZones
+ )
+
+ # Zones which are only present in ICU?
+ additionalTimeZones = [zone for zone in icuZones if not isIANATimeZone(zone)]
+ if additionalTimeZones:
+ raise RuntimeError(
+ "Additional zones present in ICU, did you forget "
+ "to run intl/update-tzdata.sh? %s" % additionalTimeZones
+ )
+
+ # Zones which are marked as links in ICU.
+ result = ((zone, icuLinks[zone]) for zone in ianaZones if isICULink(zone))
+
+ # Remove unnecessary UTC mappings.
+ utcnames = ["Etc/UTC", "Etc/UCT", "Etc/GMT"]
+ result = ((zone, target) for (zone, target) in result if zone.name not in utcnames)
+
+ return sorted(result, key=itemgetter(0))
+
+
+def findIncorrectICULinks(ianaZones, ianaLinks, icuZones, icuLinks):
+ """Find incorrect ICU link entries."""
+
+ def isIANATimeZone(zone):
+ return zone in ianaZones or zone in ianaLinks
+
+ def isICUTimeZone(zone):
+ return zone in icuZones or zone in icuLinks
+
+ def isICULink(zone):
+ return zone in icuLinks
+
+ def isICUZone(zone):
+ return zone in icuZones
+
+ # All links should be present in ICU.
+ missingTimeZones = [zone for zone in ianaLinks.keys() if not isICUTimeZone(zone)]
+ if missingTimeZones:
+ raise RuntimeError(
+ "Not all zones are present in ICU, did you forget "
+ "to run intl/update-tzdata.sh? %s" % missingTimeZones
+ )
+
+ # Links which are only present in ICU?
+ additionalTimeZones = [zone for zone in icuLinks.keys() if not isIANATimeZone(zone)]
+ if additionalTimeZones:
+ raise RuntimeError(
+ "Additional links present in ICU, did you forget "
+ "to run intl/update-tzdata.sh? %s" % additionalTimeZones
+ )
+
+ result = chain(
+ # IANA links which have a different target in ICU.
+ (
+ (zone, target, icuLinks[zone])
+ for (zone, target) in ianaLinks.items()
+ if isICULink(zone) and target != icuLinks[zone]
+ ),
+ # IANA links which are zones in ICU.
+ (
+ (zone, target, zone.name)
+ for (zone, target) in ianaLinks.items()
+ if isICUZone(zone)
+ ),
+ )
+
+ # Remove unnecessary UTC mappings.
+ utcnames = ["Etc/UTC", "Etc/UCT", "Etc/GMT"]
+ result = (
+ (zone, target, icuTarget)
+ for (zone, target, icuTarget) in result
+ if target not in utcnames or icuTarget not in utcnames
+ )
+
+ return sorted(result, key=itemgetter(0))
+
+
+generatedFileWarning = "// Generated by make_intl_data.py. DO NOT EDIT."
+tzdataVersionComment = "// tzdata version = {0}"
+
+
+def processTimeZones(
+ tzdataDir, icuDir, icuTzDir, version, ignoreBackzone, ignoreFactory, out
+):
+ """Read the time zone info and create a new time zone cpp file."""
+ print("Processing tzdata mapping...")
+ (ianaZones, ianaLinks) = readIANATimeZones(tzdataDir, ignoreBackzone, ignoreFactory)
+ (icuZones, icuLinks) = readICUTimeZones(icuDir, icuTzDir, ignoreFactory)
+ (legacyZones, legacyLinks) = readICULegacyZones(icuDir)
+
+ # Remove all legacy ICU time zones.
+ icuZones = {zone for zone in icuZones if zone not in legacyZones}
+ icuLinks = {
+ zone: target for (zone, target) in icuLinks.items() if zone not in legacyLinks
+ }
+
+ incorrectZones = findIncorrectICUZones(
+ ianaZones, ianaLinks, icuZones, icuLinks, ignoreBackzone
+ )
+ if not incorrectZones:
+ print("<<< No incorrect ICU time zones found, please update Intl.js! >>>")
+ print("<<< Maybe https://ssl.icu-project.org/trac/ticket/12044 was fixed? >>>")
+
+ incorrectLinks = findIncorrectICULinks(ianaZones, ianaLinks, icuZones, icuLinks)
+ if not incorrectLinks:
+ print("<<< No incorrect ICU time zone links found, please update Intl.js! >>>")
+ print("<<< Maybe https://ssl.icu-project.org/trac/ticket/12044 was fixed? >>>")
+
+ print("Writing Intl tzdata file...")
+ with io.open(out, mode="w", encoding="utf-8", newline="") as f:
+ println = partial(print, file=f)
+
+ println(generatedFileWarning)
+ println(tzdataVersionComment.format(version))
+ println("")
+
+ println("#ifndef builtin_intl_TimeZoneDataGenerated_h")
+ println("#define builtin_intl_TimeZoneDataGenerated_h")
+ println("")
+
+ println("namespace js {")
+ println("namespace timezone {")
+ println("")
+
+ println("// Format:")
+ println('// "ZoneName" // ICU-Name [time zone file]')
+ println("const char* const ianaZonesTreatedAsLinksByICU[] = {")
+ for (zone, icuZone) in incorrectZones:
+ println(' "%s", // %s [%s]' % (zone, icuZone, zone.filename))
+ println("};")
+ println("")
+
+ println("// Format:")
+ println('// "LinkName", "Target" // ICU-Target [time zone file]')
+ println("struct LinkAndTarget")
+ println("{")
+ println(" const char* const link;")
+ println(" const char* const target;")
+ println("};")
+ println("")
+ println("const LinkAndTarget ianaLinksCanonicalizedDifferentlyByICU[] = {")
+ for (zone, target, icuTarget) in incorrectLinks:
+ println(
+ ' { "%s", "%s" }, // %s [%s]'
+ % (zone, target, icuTarget, zone.filename)
+ )
+ println("};")
+ println("")
+
+ println(
+ "// Legacy ICU time zones, these are not valid IANA time zone names. We also"
+ )
+ println("// disallow the old and deprecated System V time zones.")
+ println(
+ "// https://ssl.icu-project.org/repos/icu/trunk/icu4c/source/tools/tzcode/icuzones"
+ ) # NOQA: E501
+ println("const char* const legacyICUTimeZones[] = {")
+ for zone in chain(sorted(legacyLinks.keys()), sorted(legacyZones)):
+ println(' "%s",' % zone)
+ println("};")
+ println("")
+
+ println("} // namespace timezone")
+ println("} // namespace js")
+ println("")
+ println("#endif /* builtin_intl_TimeZoneDataGenerated_h */")
+
+
+def updateBackzoneLinks(tzdataDir, links):
+ def withZone(fn):
+ return lambda zone_target: fn(zone_target[0])
+
+ (backzoneZones, backzoneLinks) = readIANAFiles(tzdataDir, ["backzone"])
+ (stableZones, updatedLinks, updatedZones) = partition(
+ links.items(),
+ # Link not changed in backzone.
+ withZone(lambda zone: zone not in backzoneLinks and zone not in backzoneZones),
+ # Link has a new target.
+ withZone(lambda zone: zone in backzoneLinks),
+ )
+ # Keep stable zones and links with updated target.
+ return dict(
+ chain(
+ stableZones,
+ map(withZone(lambda zone: (zone, backzoneLinks[zone])), updatedLinks),
+ )
+ )
+
+
+def generateTzDataLinkTestContent(testDir, version, fileName, description, links):
+ with io.open(
+ os.path.join(testDir, fileName), mode="w", encoding="utf-8", newline=""
+ ) as f:
+ println = partial(print, file=f)
+
+ println('// |reftest| skip-if(!this.hasOwnProperty("Intl"))')
+ println("")
+ println(generatedFileWarning)
+ println(tzdataVersionComment.format(version))
+ println(
+ """
+const tzMapper = [
+ x => x,
+ x => x.toUpperCase(),
+ x => x.toLowerCase(),
+];
+"""
+ )
+
+ println(description)
+ println("const links = {")
+ for (zone, target) in sorted(links, key=itemgetter(0)):
+ println(' "%s": "%s",' % (zone, target))
+ println("};")
+
+ println(
+ """
+for (let [linkName, target] of Object.entries(links)) {
+ if (target === "Etc/UTC" || target === "Etc/GMT")
+ target = "UTC";
+
+ for (let map of tzMapper) {
+ let dtf = new Intl.DateTimeFormat(undefined, {timeZone: map(linkName)});
+ let resolvedTimeZone = dtf.resolvedOptions().timeZone;
+ assertEq(resolvedTimeZone, target, `${linkName} -> ${target}`);
+ }
+}
+"""
+ )
+ println(
+ """
+if (typeof reportCompare === "function")
+ reportCompare(0, 0, "ok");
+"""
+ )
+
+
+def generateTzDataTestBackwardLinks(tzdataDir, version, ignoreBackzone, testDir):
+ (zones, links) = readIANAFiles(tzdataDir, ["backward"])
+ assert len(zones) == 0
+
+ if not ignoreBackzone:
+ links = updateBackzoneLinks(tzdataDir, links)
+
+ generateTzDataLinkTestContent(
+ testDir,
+ version,
+ "timeZone_backward_links.js",
+ "// Link names derived from IANA Time Zone Database, backward file.",
+ links.items(),
+ )
+
+
+def generateTzDataTestNotBackwardLinks(tzdataDir, version, ignoreBackzone, testDir):
+ tzfiles = filterfalse(
+ {"backward", "backzone"}.__contains__, listIANAFiles(tzdataDir)
+ )
+ (zones, links) = readIANAFiles(tzdataDir, tzfiles)
+
+ if not ignoreBackzone:
+ links = updateBackzoneLinks(tzdataDir, links)
+
+ generateTzDataLinkTestContent(
+ testDir,
+ version,
+ "timeZone_notbackward_links.js",
+ "// Link names derived from IANA Time Zone Database, excluding backward file.",
+ links.items(),
+ )
+
+
+def generateTzDataTestBackzone(tzdataDir, version, ignoreBackzone, testDir):
+ backzoneFiles = {"backzone"}
+ (bkfiles, tzfiles) = partition(listIANAFiles(tzdataDir), backzoneFiles.__contains__)
+
+ # Read zone and link infos.
+ (zones, links) = readIANAFiles(tzdataDir, tzfiles)
+ (backzones, backlinks) = readIANAFiles(tzdataDir, bkfiles)
+
+ if not ignoreBackzone:
+ comment = """\
+// This file was generated with historical, pre-1970 backzone information
+// respected. Therefore, every zone key listed below is its own Zone, not
+// a Link to a modern-day target as IANA ignoring backzones would say.
+
+"""
+ else:
+ comment = """\
+// This file was generated while ignoring historical, pre-1970 backzone
+// information. Therefore, every zone key listed below is part of a Link
+// whose target is the corresponding value.
+
+"""
+
+ generateTzDataLinkTestContent(
+ testDir,
+ version,
+ "timeZone_backzone.js",
+ comment + "// Backzone zones derived from IANA Time Zone Database.",
+ (
+ (zone, zone if not ignoreBackzone else links[zone])
+ for zone in backzones
+ if zone in links
+ ),
+ )
+
+
+def generateTzDataTestBackzoneLinks(tzdataDir, version, ignoreBackzone, testDir):
+ backzoneFiles = {"backzone"}
+ (bkfiles, tzfiles) = partition(listIANAFiles(tzdataDir), backzoneFiles.__contains__)
+
+ # Read zone and link infos.
+ (zones, links) = readIANAFiles(tzdataDir, tzfiles)
+ (backzones, backlinks) = readIANAFiles(tzdataDir, bkfiles)
+
+ if not ignoreBackzone:
+ comment = """\
+// This file was generated with historical, pre-1970 backzone information
+// respected. Therefore, every zone key listed below points to a target
+// in the backzone file and not to its modern-day target as IANA ignoring
+// backzones would say.
+
+"""
+ else:
+ comment = """\
+// This file was generated while ignoring historical, pre-1970 backzone
+// information. Therefore, every zone key listed below is part of a Link
+// whose target is the corresponding value ignoring any backzone entries.
+
+"""
+
+ generateTzDataLinkTestContent(
+ testDir,
+ version,
+ "timeZone_backzone_links.js",
+ comment + "// Backzone links derived from IANA Time Zone Database.",
+ (
+ (zone, target if not ignoreBackzone else links[zone])
+ for (zone, target) in backlinks.items()
+ ),
+ )
+
+
+def generateTzDataTestVersion(tzdataDir, version, testDir):
+ fileName = "timeZone_version.js"
+
+ with io.open(
+ os.path.join(testDir, fileName), mode="w", encoding="utf-8", newline=""
+ ) as f:
+ println = partial(print, file=f)
+
+ println('// |reftest| skip-if(!this.hasOwnProperty("Intl"))')
+ println("")
+ println(generatedFileWarning)
+ println(tzdataVersionComment.format(version))
+ println("""const tzdata = "{0}";""".format(version))
+
+ println(
+ """
+if (typeof getICUOptions === "undefined") {
+ var getICUOptions = SpecialPowers.Cu.getJSTestingFunctions().getICUOptions;
+}
+
+var options = getICUOptions();
+
+assertEq(options.tzdata, tzdata);
+
+if (typeof reportCompare === "function")
+ reportCompare(0, 0, "ok");
+"""
+ )
+
+
+def generateTzDataTestCanonicalZones(
+ tzdataDir, version, ignoreBackzone, ignoreFactory, testDir
+):
+ fileName = "supportedValuesOf-timeZones-canonical.js"
+
+ # Read zone and link infos.
+ (ianaZones, _) = readIANATimeZones(tzdataDir, ignoreBackzone, ignoreFactory)
+
+ # Replace Etc/GMT and Etc/UTC with UTC.
+ ianaZones.remove(Zone("Etc/GMT"))
+ ianaZones.remove(Zone("Etc/UTC"))
+ ianaZones.add(Zone("UTC"))
+
+ # See findIncorrectICUZones() for why Asia/Hanoi has to be special-cased.
+ ianaZones.remove(Zone("Asia/Hanoi"))
+
+ if not ignoreBackzone:
+ comment = """\
+// This file was generated with historical, pre-1970 backzone information
+// respected.
+"""
+ else:
+ comment = """\
+// This file was generated while ignoring historical, pre-1970 backzone
+// information.
+"""
+
+ with io.open(
+ os.path.join(testDir, fileName), mode="w", encoding="utf-8", newline=""
+ ) as f:
+ println = partial(print, file=f)
+
+ println('// |reftest| skip-if(!this.hasOwnProperty("Intl"))')
+ println("")
+ println(generatedFileWarning)
+ println(tzdataVersionComment.format(version))
+ println("")
+ println(comment)
+
+ println("const zones = [")
+ for zone in sorted(ianaZones):
+ println(f' "{zone}",')
+ println("];")
+
+ println(
+ """
+let supported = Intl.supportedValuesOf("timeZone");
+
+assertEqArray(supported, zones);
+
+if (typeof reportCompare === "function")
+ reportCompare(0, 0, "ok");
+"""
+ )
+
+
+def generateTzDataTests(tzdataDir, version, ignoreBackzone, ignoreFactory, testDir):
+ dtfTestDir = os.path.join(testDir, "DateTimeFormat")
+ if not os.path.isdir(dtfTestDir):
+ raise RuntimeError("not a directory: %s" % dtfTestDir)
+
+ generateTzDataTestBackwardLinks(tzdataDir, version, ignoreBackzone, dtfTestDir)
+ generateTzDataTestNotBackwardLinks(tzdataDir, version, ignoreBackzone, dtfTestDir)
+ generateTzDataTestBackzone(tzdataDir, version, ignoreBackzone, dtfTestDir)
+ generateTzDataTestBackzoneLinks(tzdataDir, version, ignoreBackzone, dtfTestDir)
+ generateTzDataTestVersion(tzdataDir, version, dtfTestDir)
+ generateTzDataTestCanonicalZones(
+ tzdataDir, version, ignoreBackzone, ignoreFactory, testDir
+ )
+
+
+def updateTzdata(topsrcdir, args):
+ """Update the time zone cpp file."""
+
+ icuDir = os.path.join(topsrcdir, "intl/icu/source")
+ if not os.path.isdir(icuDir):
+ raise RuntimeError("not a directory: %s" % icuDir)
+
+ icuTzDir = os.path.join(topsrcdir, "intl/tzdata/source")
+ if not os.path.isdir(icuTzDir):
+ raise RuntimeError("not a directory: %s" % icuTzDir)
+
+ intlTestDir = os.path.join(topsrcdir, "js/src/tests/non262/Intl")
+ if not os.path.isdir(intlTestDir):
+ raise RuntimeError("not a directory: %s" % intlTestDir)
+
+ tzDir = args.tz
+ if tzDir is not None and not (os.path.isdir(tzDir) or os.path.isfile(tzDir)):
+ raise RuntimeError("not a directory or file: %s" % tzDir)
+ ignoreBackzone = args.ignore_backzone
+ # TODO: Accept or ignore the placeholder time zone "Factory"?
+ ignoreFactory = False
+ out = args.out
+
+ version = icuTzDataVersion(icuTzDir)
+ url = (
+ "https://www.iana.org/time-zones/repository/releases/tzdata%s.tar.gz" % version
+ )
+
+ print("Arguments:")
+ print("\ttzdata version: %s" % version)
+ print("\ttzdata URL: %s" % url)
+ print("\ttzdata directory|file: %s" % tzDir)
+ print("\tICU directory: %s" % icuDir)
+ print("\tICU timezone directory: %s" % icuTzDir)
+ print("\tIgnore backzone file: %s" % ignoreBackzone)
+ print("\tOutput file: %s" % out)
+ print("")
+
+ def updateFrom(f):
+ if os.path.isfile(f) and tarfile.is_tarfile(f):
+ with tarfile.open(f, "r:*") as tar:
+ processTimeZones(
+ TzDataFile(tar),
+ icuDir,
+ icuTzDir,
+ version,
+ ignoreBackzone,
+ ignoreFactory,
+ out,
+ )
+ generateTzDataTests(
+ TzDataFile(tar), version, ignoreBackzone, ignoreFactory, intlTestDir
+ )
+ elif os.path.isdir(f):
+ processTimeZones(
+ TzDataDir(f),
+ icuDir,
+ icuTzDir,
+ version,
+ ignoreBackzone,
+ ignoreFactory,
+ out,
+ )
+ generateTzDataTests(
+ TzDataDir(f), version, ignoreBackzone, ignoreFactory, intlTestDir
+ )
+ else:
+ raise RuntimeError("unknown format")
+
+ if tzDir is None:
+ print("Downloading tzdata file...")
+ with closing(urlopen(url)) as tzfile:
+ fname = urlsplit(tzfile.geturl()).path.split("/")[-1]
+ with tempfile.NamedTemporaryFile(suffix=fname) as tztmpfile:
+ print("File stored in %s" % tztmpfile.name)
+ tztmpfile.write(tzfile.read())
+ tztmpfile.flush()
+ updateFrom(tztmpfile.name)
+ else:
+ updateFrom(tzDir)
+
+
+def readCurrencyFile(tree):
+ reCurrency = re.compile(r"^[A-Z]{3}$")
+ reIntMinorUnits = re.compile(r"^\d+$")
+
+ for country in tree.iterfind(".//CcyNtry"):
+ # Skip entry if no currency information is available.
+ currency = country.findtext("Ccy")
+ if currency is None:
+ continue
+ assert reCurrency.match(currency)
+
+ minorUnits = country.findtext("CcyMnrUnts")
+ assert minorUnits is not None
+
+ # Skip all entries without minorUnits or which use the default minorUnits.
+ if reIntMinorUnits.match(minorUnits) and int(minorUnits) != 2:
+ currencyName = country.findtext("CcyNm")
+ countryName = country.findtext("CtryNm")
+ yield (currency, int(minorUnits), currencyName, countryName)
+
+
+def writeCurrencyFile(published, currencies, out):
+ with io.open(out, mode="w", encoding="utf-8", newline="") as f:
+ println = partial(print, file=f)
+
+ println(generatedFileWarning)
+ println("// Version: {}".format(published))
+
+ println(
+ """
+/**
+ * Mapping from currency codes to the number of decimal digits used for them.
+ * Default is 2 digits.
+ *
+ * Spec: ISO 4217 Currency and Funds Code List.
+ * http://www.currency-iso.org/en/home/tables/table-a1.html
+ */"""
+ )
+ println("var currencyDigits = {")
+ for (currency, entries) in groupby(
+ sorted(currencies, key=itemgetter(0)), itemgetter(0)
+ ):
+ for (_, minorUnits, currencyName, countryName) in entries:
+ println(" // {} ({})".format(currencyName, countryName))
+ println(" {}: {},".format(currency, minorUnits))
+ println("};")
+
+
+def updateCurrency(topsrcdir, args):
+ """Update the CurrencyDataGenerated.js file."""
+ import xml.etree.ElementTree as ET
+ from random import randint
+
+ url = args.url
+ out = args.out
+ filename = args.file
+
+ print("Arguments:")
+ print("\tDownload url: %s" % url)
+ print("\tLocal currency file: %s" % filename)
+ print("\tOutput file: %s" % out)
+ print("")
+
+ def updateFrom(currencyFile):
+ print("Processing currency code list file...")
+ tree = ET.parse(currencyFile)
+ published = tree.getroot().attrib["Pblshd"]
+ currencies = readCurrencyFile(tree)
+
+ print("Writing CurrencyData file...")
+ writeCurrencyFile(published, currencies, out)
+
+ if filename is not None:
+ print("Always make sure you have the newest currency code list file!")
+ updateFrom(filename)
+ else:
+ print("Downloading currency & funds code list...")
+ request = UrlRequest(url)
+ request.add_header(
+ "User-agent",
+ "Mozilla/5.0 (Mobile; rv:{0}.0) Gecko/{0}.0 Firefox/{0}.0".format(
+ randint(1, 999)
+ ),
+ )
+ with closing(urlopen(request)) as currencyFile:
+ fname = urlsplit(currencyFile.geturl()).path.split("/")[-1]
+ with tempfile.NamedTemporaryFile(suffix=fname) as currencyTmpFile:
+ print("File stored in %s" % currencyTmpFile.name)
+ currencyTmpFile.write(currencyFile.read())
+ currencyTmpFile.flush()
+ updateFrom(currencyTmpFile.name)
+
+
+def writeUnicodeExtensionsMappings(println, mapping, extension):
+ println(
+ """
+template <size_t Length>
+static inline bool Is{0}Key(mozilla::Span<const char> key, const char (&str)[Length]) {{
+ static_assert(Length == {0}KeyLength + 1,
+ "{0} extension key is two characters long");
+ return memcmp(key.data(), str, Length - 1) == 0;
+}}
+
+template <size_t Length>
+static inline bool Is{0}Type(mozilla::Span<const char> type, const char (&str)[Length]) {{
+ static_assert(Length > {0}KeyLength + 1,
+ "{0} extension type contains more than two characters");
+ return type.size() == (Length - 1) &&
+ memcmp(type.data(), str, Length - 1) == 0;
+}}
+""".format(
+ extension
+ ).rstrip(
+ "\n"
+ )
+ )
+
+ linear_search_max_length = 4
+
+ needs_binary_search = any(
+ len(replacements.items()) > linear_search_max_length
+ for replacements in mapping.values()
+ )
+
+ if needs_binary_search:
+ println(
+ """
+static int32_t Compare{0}Type(const char* a, mozilla::Span<const char> b) {{
+ MOZ_ASSERT(!std::char_traits<char>::find(b.data(), b.size(), '\\0'),
+ "unexpected null-character in string");
+
+ using UnsignedChar = unsigned char;
+ for (size_t i = 0; i < b.size(); i++) {{
+ // |a| is zero-terminated and |b| doesn't contain a null-terminator. So if
+ // we've reached the end of |a|, the below if-statement will always be true.
+ // That ensures we don't read past the end of |a|.
+ if (int32_t r = UnsignedChar(a[i]) - UnsignedChar(b[i])) {{
+ return r;
+ }}
+ }}
+
+ // Return zero if both strings are equal or a positive number if |b| is a
+ // prefix of |a|.
+ return int32_t(UnsignedChar(a[b.size()]));
+}}
+
+template <size_t Length>
+static inline const char* Search{0}Replacement(
+ const char* (&types)[Length], const char* (&aliases)[Length],
+ mozilla::Span<const char> type) {{
+
+ auto p = std::lower_bound(std::begin(types), std::end(types), type,
+ [](const auto& a, const auto& b) {{
+ return Compare{0}Type(a, b) < 0;
+ }});
+ if (p != std::end(types) && Compare{0}Type(*p, type) == 0) {{
+ return aliases[std::distance(std::begin(types), p)];
+ }}
+ return nullptr;
+}}
+""".format(
+ extension
+ ).rstrip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+/**
+ * Mapping from deprecated BCP 47 {0} extension types to their preferred
+ * values.
+ *
+ * Spec: https://www.unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files
+ * Spec: https://www.unicode.org/reports/tr35/#t_Extension
+ */
+const char* mozilla::intl::Locale::Replace{0}ExtensionType(
+ mozilla::Span<const char> key, mozilla::Span<const char> type) {{
+ MOZ_ASSERT(key.size() == {0}KeyLength);
+ MOZ_ASSERT(IsCanonicallyCased{0}Key(key));
+
+ MOZ_ASSERT(type.size() > {0}KeyLength);
+ MOZ_ASSERT(IsCanonicallyCased{0}Type(type));
+""".format(
+ extension
+ )
+ )
+
+ def to_hash_key(replacements):
+ return str(sorted(replacements.items()))
+
+ def write_array(subtags, name, length):
+ max_entries = (80 - len(" ")) // (length + len('"", '))
+
+ println(" static const char* {}[{}] = {{".format(name, len(subtags)))
+
+ for entries in grouper(subtags, max_entries):
+ entries = (
+ '"{}"'.format(tag).center(length + 2)
+ for tag in entries
+ if tag is not None
+ )
+ println(" {},".format(", ".join(entries)))
+
+ println(" };")
+
+ # Merge duplicate keys.
+ key_aliases = {}
+ for (key, replacements) in sorted(mapping.items(), key=itemgetter(0)):
+ hash_key = to_hash_key(replacements)
+ if hash_key not in key_aliases:
+ key_aliases[hash_key] = []
+ else:
+ key_aliases[hash_key].append(key)
+
+ first_key = True
+ for (key, replacements) in sorted(mapping.items(), key=itemgetter(0)):
+ hash_key = to_hash_key(replacements)
+ if key in key_aliases[hash_key]:
+ continue
+
+ cond = (
+ 'Is{}Key(key, "{}")'.format(extension, k)
+ for k in [key] + key_aliases[hash_key]
+ )
+
+ if_kind = "if" if first_key else "else if"
+ cond = (" ||\n" + " " * (2 + len(if_kind) + 2)).join(cond)
+ println(
+ """
+ {} ({}) {{""".format(
+ if_kind, cond
+ ).strip(
+ "\n"
+ )
+ )
+ first_key = False
+
+ replacements = sorted(replacements.items(), key=itemgetter(0))
+
+ if len(replacements) > linear_search_max_length:
+ types = [t for (t, _) in replacements]
+ preferred = [r for (_, r) in replacements]
+ max_len = max(len(k) for k in types + preferred)
+
+ write_array(types, "types", max_len)
+ write_array(preferred, "aliases", max_len)
+ println(
+ """
+ return Search{}Replacement(types, aliases, type);
+""".format(
+ extension
+ ).strip(
+ "\n"
+ )
+ )
+ else:
+ for (type, replacement) in replacements:
+ println(
+ """
+ if (Is{}Type(type, "{}")) {{
+ return "{}";
+ }}""".format(
+ extension, type, replacement
+ ).strip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+ }""".lstrip(
+ "\n"
+ )
+ )
+
+ println(
+ """
+ return nullptr;
+}
+""".strip(
+ "\n"
+ )
+ )
+
+
+def readICUUnitResourceFile(filepath):
+ """Return a set of unit descriptor pairs where the first entry denotes the unit type and the
+ second entry the unit name.
+
+ Example:
+
+ root{
+ units{
+ compound{
+ }
+ coordinate{
+ }
+ length{
+ meter{
+ }
+ }
+ }
+ unitsNarrow:alias{"/LOCALE/unitsShort"}
+ unitsShort{
+ duration{
+ day{
+ }
+ day-person:alias{"/LOCALE/unitsShort/duration/day"}
+ }
+ length{
+ meter{
+ }
+ }
+ }
+ }
+
+ Returns {("length", "meter"), ("duration", "day"), ("duration", "day-person")}
+ """
+
+ start_table_re = re.compile(r"^([\w\-%:\"]+)\{$")
+ end_table_re = re.compile(r"^\}$")
+ table_entry_re = re.compile(r"^([\w\-%:\"]+)\{\"(.*?)\"\}$")
+
+ # The current resource table.
+ table = {}
+
+ # List of parent tables when parsing.
+ parents = []
+
+ # Track multi-line comments state.
+ in_multiline_comment = False
+
+ for line in flines(filepath, "utf-8-sig"):
+ # Remove leading and trailing whitespace.
+ line = line.strip()
+
+ # Skip over comments.
+ if in_multiline_comment:
+ if line.endswith("*/"):
+ in_multiline_comment = False
+ continue
+
+ if line.startswith("//"):
+ continue
+
+ if line.startswith("/*"):
+ in_multiline_comment = True
+ continue
+
+ # Try to match the start of a table, e.g. `length{` or `meter{`.
+ match = start_table_re.match(line)
+ if match:
+ parents.append(table)
+ table_name = match.group(1)
+ new_table = {}
+ table[table_name] = new_table
+ table = new_table
+ continue
+
+ # Try to match the end of a table.
+ match = end_table_re.match(line)
+ if match:
+ table = parents.pop()
+ continue
+
+ # Try to match a table entry, e.g. `dnam{"meter"}`.
+ match = table_entry_re.match(line)
+ if match:
+ entry_key = match.group(1)
+ entry_value = match.group(2)
+ table[entry_key] = entry_value
+ continue
+
+ raise Exception("unexpected line: '{}' in {}".format(line, filepath))
+
+ assert len(parents) == 0, "Not all tables closed"
+ assert len(table) == 1, "More than one root table"
+
+ # Remove the top-level language identifier table.
+ (_, unit_table) = table.popitem()
+
+ # Add all units for the three display formats "units", "unitsNarrow", and "unitsShort".
+ # But exclude the pseudo-units "compound" and "ccoordinate".
+ return {
+ (unit_type, unit_name if not unit_name.endswith(":alias") else unit_name[:-6])
+ for unit_display in ("units", "unitsNarrow", "unitsShort")
+ if unit_display in unit_table
+ for (unit_type, unit_names) in unit_table[unit_display].items()
+ if unit_type != "compound" and unit_type != "coordinate"
+ for unit_name in unit_names.keys()
+ }
+
+
+def computeSupportedUnits(all_units, sanctioned_units):
+ """Given the set of all possible ICU unit identifiers and the set of sanctioned unit
+ identifiers, compute the set of effectively supported ICU unit identifiers.
+ """
+
+ def find_match(unit):
+ unit_match = [
+ (unit_type, unit_name)
+ for (unit_type, unit_name) in all_units
+ if unit_name == unit
+ ]
+ if unit_match:
+ assert len(unit_match) == 1
+ return unit_match[0]
+ return None
+
+ def compound_unit_identifiers():
+ for numerator in sanctioned_units:
+ for denominator in sanctioned_units:
+ yield "{}-per-{}".format(numerator, denominator)
+
+ supported_simple_units = {find_match(unit) for unit in sanctioned_units}
+ assert None not in supported_simple_units
+
+ supported_compound_units = {
+ unit_match
+ for unit_match in (find_match(unit) for unit in compound_unit_identifiers())
+ if unit_match
+ }
+
+ return supported_simple_units | supported_compound_units
+
+
+def readICUDataFilterForUnits(data_filter_file):
+ with io.open(data_filter_file, mode="r", encoding="utf-8") as f:
+ data_filter = json.load(f)
+
+ # Find the rule set for the "unit_tree".
+ unit_tree_rules = [
+ entry["rules"]
+ for entry in data_filter["resourceFilters"]
+ if entry["categories"] == ["unit_tree"]
+ ]
+ assert len(unit_tree_rules) == 1
+
+ # Compute the list of included units from that rule set. The regular expression must match
+ # "+/*/length/meter" and mustn't match either "-/*" or "+/*/compound".
+ included_unit_re = re.compile(r"^\+/\*/(.+?)/(.+)$")
+ filtered_units = (included_unit_re.match(unit) for unit in unit_tree_rules[0])
+
+ return {(unit.group(1), unit.group(2)) for unit in filtered_units if unit}
+
+
+def writeSanctionedSimpleUnitIdentifiersFiles(all_units, sanctioned_units):
+ js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__))
+ intl_components_src_dir = os.path.join(
+ js_src_builtin_intl_dir, "../../../../intl/components/src"
+ )
+
+ def find_unit_type(unit):
+ result = [
+ unit_type for (unit_type, unit_name) in all_units if unit_name == unit
+ ]
+ assert result and len(result) == 1
+ return result[0]
+
+ sanctioned_js_file = os.path.join(
+ js_src_builtin_intl_dir, "SanctionedSimpleUnitIdentifiersGenerated.js"
+ )
+ with io.open(sanctioned_js_file, mode="w", encoding="utf-8", newline="") as f:
+ println = partial(print, file=f)
+
+ sanctioned_units_object = json.dumps(
+ {unit: True for unit in sorted(sanctioned_units)},
+ sort_keys=True,
+ indent=2,
+ separators=(",", ": "),
+ )
+
+ println(generatedFileWarning)
+
+ println(
+ """
+/**
+ * The list of currently supported simple unit identifiers.
+ *
+ * Intl.NumberFormat Unified API Proposal
+ */"""
+ )
+
+ println("/* eslint-disable prettier/prettier */")
+ println(
+ "var sanctionedSimpleUnitIdentifiers = {};".format(sanctioned_units_object)
+ )
+ println("/* eslint-enable prettier/prettier */")
+
+ sanctioned_h_file = os.path.join(intl_components_src_dir, "MeasureUnitGenerated.h")
+ with io.open(sanctioned_h_file, mode="w", encoding="utf-8", newline="") as f:
+ println = partial(print, file=f)
+
+ println(generatedFileWarning)
+
+ println(
+ """
+#ifndef intl_components_MeasureUnitGenerated_h
+#define intl_components_MeasureUnitGenerated_h
+
+namespace mozilla::intl {
+
+struct SimpleMeasureUnit {
+ const char* const type;
+ const char* const name;
+};
+
+/**
+ * The list of currently supported simple unit identifiers.
+ *
+ * The list must be kept in alphabetical order of |name|.
+ */
+inline constexpr SimpleMeasureUnit simpleMeasureUnits[] = {
+ // clang-format off"""
+ )
+
+ for unit_name in sorted(sanctioned_units):
+ println(' {{"{}", "{}"}},'.format(find_unit_type(unit_name), unit_name))
+
+ println(
+ """
+ // clang-format on
+};
+
+} // namespace mozilla::intl
+
+#endif
+""".strip(
+ "\n"
+ )
+ )
+
+ writeUnitTestFiles(all_units, sanctioned_units)
+
+
+def writeUnitTestFiles(all_units, sanctioned_units):
+ """Generate test files for unit number formatters."""
+
+ js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__))
+ test_dir = os.path.join(
+ js_src_builtin_intl_dir, "../../tests/non262/Intl/NumberFormat"
+ )
+
+ def write_test(file_name, test_content, indent=4):
+ file_path = os.path.join(test_dir, file_name)
+ with io.open(file_path, mode="w", encoding="utf-8", newline="") as f:
+ println = partial(print, file=f)
+
+ println('// |reftest| skip-if(!this.hasOwnProperty("Intl"))')
+ println("")
+ println(generatedFileWarning)
+ println("")
+
+ sanctioned_units_array = json.dumps(
+ [unit for unit in sorted(sanctioned_units)],
+ indent=indent,
+ separators=(",", ": "),
+ )
+
+ println(
+ "const sanctionedSimpleUnitIdentifiers = {};".format(
+ sanctioned_units_array
+ )
+ )
+
+ println(test_content)
+
+ println(
+ """
+if (typeof reportCompare === "function")
+{}reportCompare(true, true);""".format(
+ " " * indent
+ )
+ )
+
+ write_test(
+ "unit-compound-combinations.js",
+ """
+// Test all simple unit identifier combinations are allowed.
+
+for (const numerator of sanctionedSimpleUnitIdentifiers) {
+ for (const denominator of sanctionedSimpleUnitIdentifiers) {
+ const unit = `${numerator}-per-${denominator}`;
+ const nf = new Intl.NumberFormat("en", {style: "unit", unit});
+
+ assertEq(nf.format(1), nf.formatToParts(1).map(p => p.value).join(""));
+ }
+}""",
+ )
+
+ all_units_array = json.dumps(
+ ["-".join(unit) for unit in sorted(all_units)], indent=4, separators=(",", ": ")
+ )
+
+ write_test(
+ "unit-well-formed.js",
+ """
+const allUnits = {};
+""".format(
+ all_units_array
+ )
+ + """
+// Test only sanctioned unit identifiers are allowed.
+
+for (const typeAndUnit of allUnits) {
+ const [_, type, unit] = typeAndUnit.match(/(\w+)-(.+)/);
+
+ let allowed;
+ if (unit.includes("-per-")) {
+ const [numerator, denominator] = unit.split("-per-");
+ allowed = sanctionedSimpleUnitIdentifiers.includes(numerator) &&
+ sanctionedSimpleUnitIdentifiers.includes(denominator);
+ } else {
+ allowed = sanctionedSimpleUnitIdentifiers.includes(unit);
+ }
+
+ if (allowed) {
+ const nf = new Intl.NumberFormat("en", {style: "unit", unit});
+ assertEq(nf.format(1), nf.formatToParts(1).map(p => p.value).join(""));
+ } else {
+ assertThrowsInstanceOf(() => new Intl.NumberFormat("en", {style: "unit", unit}),
+ RangeError, `Missing error for "${typeAndUnit}"`);
+ }
+}""",
+ )
+
+ write_test(
+ "unit-formatToParts-has-unit-field.js",
+ """
+// Test only English and Chinese to keep the overall runtime reasonable.
+//
+// Chinese is included because it contains more than one "unit" element for
+// certain unit combinations.
+const locales = ["en", "zh"];
+
+// Plural rules for English only differentiate between "one" and "other". Plural
+// rules for Chinese only use "other". That means we only need to test two values
+// per unit.
+const values = [0, 1];
+
+// Ensure unit formatters contain at least one "unit" element.
+
+for (const locale of locales) {
+ for (const unit of sanctionedSimpleUnitIdentifiers) {
+ const nf = new Intl.NumberFormat(locale, {style: "unit", unit});
+
+ for (const value of values) {
+ assertEq(nf.formatToParts(value).some(e => e.type === "unit"), true,
+ `locale=${locale}, unit=${unit}`);
+ }
+ }
+
+ for (const numerator of sanctionedSimpleUnitIdentifiers) {
+ for (const denominator of sanctionedSimpleUnitIdentifiers) {
+ const unit = `${numerator}-per-${denominator}`;
+ const nf = new Intl.NumberFormat(locale, {style: "unit", unit});
+
+ for (const value of values) {
+ assertEq(nf.formatToParts(value).some(e => e.type === "unit"), true,
+ `locale=${locale}, unit=${unit}`);
+ }
+ }
+ }
+}""",
+ indent=2,
+ )
+
+
+def updateUnits(topsrcdir, args):
+ js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__))
+ icu_path = os.path.join(topsrcdir, "intl", "icu")
+ icu_unit_path = os.path.join(icu_path, "source", "data", "unit")
+
+ with io.open(
+ os.path.join(js_src_builtin_intl_dir, "SanctionedSimpleUnitIdentifiers.yaml"),
+ mode="r",
+ encoding="utf-8",
+ ) as f:
+ sanctioned_units = yaml.safe_load(f)
+
+ # Read all possible ICU unit identifiers from the "unit/root.txt" resource.
+ unit_root_file = os.path.join(icu_unit_path, "root.txt")
+ all_units = readICUUnitResourceFile(unit_root_file)
+
+ # Compute the set of effectively supported ICU unit identifiers.
+ supported_units = computeSupportedUnits(all_units, sanctioned_units)
+
+ # Read the list of units we're including into the ICU data file.
+ data_filter_file = os.path.join(icu_path, "data_filter.json")
+ filtered_units = readICUDataFilterForUnits(data_filter_file)
+
+ # Both sets must match to avoid resource loading errors at runtime.
+ if supported_units != filtered_units:
+
+ def units_to_string(units):
+ return ", ".join("/".join(u) for u in units)
+
+ missing = supported_units - filtered_units
+ if missing:
+ raise RuntimeError("Missing units: {}".format(units_to_string(missing)))
+
+ # Not exactly an error, but we currently don't have a use case where we need to support
+ # more units than required by ECMA-402.
+ extra = filtered_units - supported_units
+ if extra:
+ raise RuntimeError("Unnecessary units: {}".format(units_to_string(extra)))
+
+ writeSanctionedSimpleUnitIdentifiersFiles(all_units, sanctioned_units)
+
+
+def readICUNumberingSystemsResourceFile(filepath):
+ """Returns a dictionary of numbering systems where the key denotes the numbering system name
+ and the value a dictionary with additional numbering system data.
+
+ Example:
+
+ numberingSystems:table(nofallback){
+ numberingSystems{
+ latn{
+ algorithmic:int{0}
+ desc{"0123456789"}
+ radix:int{10}
+ }
+ roman{
+ algorithmic:int{1}
+ desc{"%roman-upper"}
+ radix:int{10}
+ }
+ }
+ }
+
+ Returns {"latn": {"digits": "0123456789", "algorithmic": False},
+ "roman": {"algorithmic": True}}
+ """
+
+ start_table_re = re.compile(r"^(\w+)(?:\:[\w\(\)]+)?\{$")
+ end_table_re = re.compile(r"^\}$")
+ table_entry_re = re.compile(r"^(\w+)(?:\:[\w\(\)]+)?\{(?:(?:\"(.*?)\")|(\d+))\}$")
+
+ # The current resource table.
+ table = {}
+
+ # List of parent tables when parsing.
+ parents = []
+
+ # Track multi-line comments state.
+ in_multiline_comment = False
+
+ for line in flines(filepath, "utf-8-sig"):
+ # Remove leading and trailing whitespace.
+ line = line.strip()
+
+ # Skip over comments.
+ if in_multiline_comment:
+ if line.endswith("*/"):
+ in_multiline_comment = False
+ continue
+
+ if line.startswith("//"):
+ continue
+
+ if line.startswith("/*"):
+ in_multiline_comment = True
+ continue
+
+ # Try to match the start of a table, e.g. `latn{`.
+ match = start_table_re.match(line)
+ if match:
+ parents.append(table)
+ table_name = match.group(1)
+ new_table = {}
+ table[table_name] = new_table
+ table = new_table
+ continue
+
+ # Try to match the end of a table.
+ match = end_table_re.match(line)
+ if match:
+ table = parents.pop()
+ continue
+
+ # Try to match a table entry, e.g. `desc{"0123456789"}`.
+ match = table_entry_re.match(line)
+ if match:
+ entry_key = match.group(1)
+ entry_value = (
+ match.group(2) if match.group(2) is not None else int(match.group(3))
+ )
+ table[entry_key] = entry_value
+ continue
+
+ raise Exception("unexpected line: '{}' in {}".format(line, filepath))
+
+ assert len(parents) == 0, "Not all tables closed"
+ assert len(table) == 1, "More than one root table"
+
+ # Remove the two top-level "numberingSystems" tables.
+ (_, numbering_systems) = table.popitem()
+ (_, numbering_systems) = numbering_systems.popitem()
+
+ # Assert all numbering systems use base 10.
+ assert all(ns["radix"] == 10 for ns in numbering_systems.values())
+
+ # Return the numbering systems.
+ return {
+ key: {"digits": value["desc"], "algorithmic": False}
+ if not bool(value["algorithmic"])
+ else {"algorithmic": True}
+ for (key, value) in numbering_systems.items()
+ }
+
+
+def writeNumberingSystemFiles(numbering_systems):
+ js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__))
+
+ numbering_systems_js_file = os.path.join(
+ js_src_builtin_intl_dir, "NumberingSystemsGenerated.h"
+ )
+ with io.open(
+ numbering_systems_js_file, mode="w", encoding="utf-8", newline=""
+ ) as f:
+ println = partial(print, file=f)
+
+ println(generatedFileWarning)
+
+ println(
+ """
+/**
+ * The list of numbering systems with simple digit mappings.
+ */
+
+#ifndef builtin_intl_NumberingSystemsGenerated_h
+#define builtin_intl_NumberingSystemsGenerated_h
+"""
+ )
+
+ simple_numbering_systems = sorted(
+ name
+ for (name, value) in numbering_systems.items()
+ if not value["algorithmic"]
+ )
+
+ println("// clang-format off")
+ println("#define NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS \\")
+ println(
+ "{}".format(
+ ", \\\n".join(
+ ' "{}"'.format(name) for name in simple_numbering_systems
+ )
+ )
+ )
+ println("// clang-format on")
+ println("")
+
+ println("#endif // builtin_intl_NumberingSystemsGenerated_h")
+
+ js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__))
+ test_dir = os.path.join(js_src_builtin_intl_dir, "../../tests/non262/Intl")
+
+ intl_shell_js_file = os.path.join(test_dir, "shell.js")
+
+ with io.open(intl_shell_js_file, mode="w", encoding="utf-8", newline="") as f:
+ println = partial(print, file=f)
+
+ println(generatedFileWarning)
+
+ println(
+ """
+// source: CLDR file common/bcp47/number.xml; version CLDR {}.
+// https://github.com/unicode-org/cldr/blob/master/common/bcp47/number.xml
+// https://github.com/unicode-org/cldr/blob/master/common/supplemental/numberingSystems.xml
+""".format(
+ readCLDRVersionFromICU()
+ ).rstrip()
+ )
+
+ numbering_systems_object = json.dumps(
+ numbering_systems,
+ indent=2,
+ separators=(",", ": "),
+ sort_keys=True,
+ ensure_ascii=False,
+ )
+ println("const numberingSystems = {};".format(numbering_systems_object))
+
+
+def updateNumberingSystems(topsrcdir, args):
+ js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__))
+ icu_path = os.path.join(topsrcdir, "intl", "icu")
+ icu_misc_path = os.path.join(icu_path, "source", "data", "misc")
+
+ with io.open(
+ os.path.join(js_src_builtin_intl_dir, "NumberingSystems.yaml"),
+ mode="r",
+ encoding="utf-8",
+ ) as f:
+ numbering_systems = yaml.safe_load(f)
+
+ # Read all possible ICU unit identifiers from the "misc/numberingSystems.txt" resource.
+ misc_ns_file = os.path.join(icu_misc_path, "numberingSystems.txt")
+ all_numbering_systems = readICUNumberingSystemsResourceFile(misc_ns_file)
+
+ all_numbering_systems_simple_digits = {
+ name
+ for (name, value) in all_numbering_systems.items()
+ if not value["algorithmic"]
+ }
+
+ # Assert ICU includes support for all required numbering systems. If this assertion fails,
+ # something is broken in ICU.
+ assert all_numbering_systems_simple_digits.issuperset(
+ numbering_systems
+ ), "{}".format(numbering_systems.difference(all_numbering_systems_simple_digits))
+
+ # Assert the spec requires support for all numbering systems with simple digit mappings. If
+ # this assertion fails, file a PR at <https://github.com/tc39/ecma402> to include any new
+ # numbering systems.
+ assert all_numbering_systems_simple_digits.issubset(numbering_systems), "{}".format(
+ all_numbering_systems_simple_digits.difference(numbering_systems)
+ )
+
+ writeNumberingSystemFiles(all_numbering_systems)
+
+
+if __name__ == "__main__":
+ import argparse
+
+ # This script must reside in js/src/builtin/intl to work correctly.
+ (thisDir, thisFile) = os.path.split(os.path.abspath(__file__))
+ dirPaths = os.path.normpath(thisDir).split(os.sep)
+ if "/".join(dirPaths[-4:]) != "js/src/builtin/intl":
+ raise RuntimeError("%s must reside in js/src/builtin/intl" % __file__)
+ topsrcdir = "/".join(dirPaths[:-4])
+
+ def EnsureHttps(v):
+ if not v.startswith("https:"):
+ raise argparse.ArgumentTypeError("URL protocol must be https: " % v)
+ return v
+
+ parser = argparse.ArgumentParser(description="Update intl data.")
+ subparsers = parser.add_subparsers(help="Select update mode")
+
+ parser_cldr_tags = subparsers.add_parser(
+ "langtags", help="Update CLDR language tags data"
+ )
+ parser_cldr_tags.add_argument(
+ "--version", metavar="VERSION", help="CLDR version number"
+ )
+ parser_cldr_tags.add_argument(
+ "--url",
+ metavar="URL",
+ default="https://unicode.org/Public/cldr/<VERSION>/core.zip",
+ type=EnsureHttps,
+ help="Download url CLDR data (default: %(default)s)",
+ )
+ parser_cldr_tags.add_argument(
+ "--out",
+ default=os.path.join(
+ topsrcdir, "intl", "components", "src", "LocaleGenerated.cpp"
+ ),
+ help="Output file (default: %(default)s)",
+ )
+ parser_cldr_tags.add_argument(
+ "file", nargs="?", help="Local cldr-core.zip file, if omitted uses <URL>"
+ )
+ parser_cldr_tags.set_defaults(func=updateCLDRLangTags)
+
+ parser_tz = subparsers.add_parser("tzdata", help="Update tzdata")
+ parser_tz.add_argument(
+ "--tz",
+ help="Local tzdata directory or file, if omitted downloads tzdata "
+ "distribution from https://www.iana.org/time-zones/",
+ )
+ # ICU doesn't include the backzone file by default, but we still like to
+ # use the backzone time zone names to avoid user confusion. This does lead
+ # to formatting "historic" dates (pre-1970 era) with the wrong time zone,
+ # but that's probably acceptable for now.
+ parser_tz.add_argument(
+ "--ignore-backzone",
+ action="store_true",
+ help="Ignore tzdata's 'backzone' file. Can be enabled to generate more "
+ "accurate time zone canonicalization reflecting the actual time "
+ "zones as used by ICU.",
+ )
+ parser_tz.add_argument(
+ "--out",
+ default=os.path.join(thisDir, "TimeZoneDataGenerated.h"),
+ help="Output file (default: %(default)s)",
+ )
+ parser_tz.set_defaults(func=partial(updateTzdata, topsrcdir))
+
+ parser_currency = subparsers.add_parser(
+ "currency", help="Update currency digits mapping"
+ )
+ parser_currency.add_argument(
+ "--url",
+ metavar="URL",
+ default="https://www.currency-iso.org/dam/downloads/lists/list_one.xml", # NOQA: E501
+ type=EnsureHttps,
+ help="Download url for the currency & funds code list (default: "
+ "%(default)s)",
+ )
+ parser_currency.add_argument(
+ "--out",
+ default=os.path.join(thisDir, "CurrencyDataGenerated.js"),
+ help="Output file (default: %(default)s)",
+ )
+ parser_currency.add_argument(
+ "file", nargs="?", help="Local currency code list file, if omitted uses <URL>"
+ )
+ parser_currency.set_defaults(func=partial(updateCurrency, topsrcdir))
+
+ parser_units = subparsers.add_parser(
+ "units", help="Update sanctioned unit identifiers mapping"
+ )
+ parser_units.set_defaults(func=partial(updateUnits, topsrcdir))
+
+ parser_numbering_systems = subparsers.add_parser(
+ "numbering", help="Update numbering systems with simple digit mappings"
+ )
+ parser_numbering_systems.set_defaults(
+ func=partial(updateNumberingSystems, topsrcdir)
+ )
+
+ args = parser.parse_args()
+ args.func(args)