commit 065c332bc2a7ff545544eec9d4eeabe3690c2791
parent fb163b93205386cc920b2f77761dcdea6c4c94b7
Author: André Bargull <andre.bargull@gmail.com>
Date: Fri, 12 Dec 2025 17:38:15 +0000
Bug 2005531 - Part 4: Add separate LocaleNegotiation files. r=spidermonkey-reviewers,dminor
Add "LocaleNegotiation.{h,cpp}" for locale negotation [1] related operations. Later
patches will add more functions to this file.
[1] https://tc39.es/ecma402/#locale-and-parameter-negotiation
Differential Revision: https://phabricator.services.mozilla.com/D276021
Diffstat:
6 files changed, 360 insertions(+), 282 deletions(-)
diff --git a/js/src/builtin/intl/GlobalIntlData.cpp b/js/src/builtin/intl/GlobalIntlData.cpp
@@ -13,7 +13,7 @@
#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/DateTimeFormat.h"
#include "builtin/intl/FormatBuffer.h"
-#include "builtin/intl/IntlObject.h"
+#include "builtin/intl/LocaleNegotiation.h"
#include "builtin/intl/NumberFormat.h"
#include "builtin/temporal/TimeZone.h"
#include "gc/Tracer.h"
diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp
@@ -12,7 +12,6 @@
#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"
@@ -24,18 +23,15 @@
#include "builtin/Array.h"
#include "builtin/intl/CommonFunctions.h"
-#include "builtin/intl/FormatBuffer.h"
+#include "builtin/intl/LocaleNegotiation.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/JSAtomUtils.h" // ClassName
#include "vm/JSContext.h"
@@ -116,103 +112,6 @@ static void ReportBadKey(JSContext* cx, JSString* key) {
}
}
-static bool SameOrParentLocale(const JSLinearString* locale,
- const 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 AvailableLocaleKind = js::intl::AvailableLocaleKind;
-
-// 9.2.2 BestAvailableLocale ( availableLocales, locale )
-static JS::Result<JSLinearString*> BestAvailableLocale(
- JSContext* cx, AvailableLocaleKind 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.isAvailableLocale(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
@@ -221,6 +120,8 @@ bool js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 3);
+ using AvailableLocaleKind = js::intl::AvailableLocaleKind;
+
AvailableLocaleKind kind;
{
JSLinearString* typeStr = args[0].toString()->ensureLinear(cx);
@@ -255,54 +156,6 @@ bool js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) {
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;
- }
-
- MOZ_ASSERT(StringEqualsAscii(locale, buffer.data(), buffer.length()),
- "locale is a canonicalized language tag");
- }
-#endif
-
MOZ_ASSERT(args[2].isNull() || args[2].isString());
Rooted<JSLinearString*> defaultLocale(cx);
@@ -313,10 +166,10 @@ bool js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) {
}
}
- JSString* result;
- JS_TRY_VAR_OR_RETURN_FALSE(
- cx, result, BestAvailableLocale(cx, kind, locale, defaultLocale));
-
+ Rooted<JSLinearString*> result(cx);
+ if (!intl::BestAvailableLocale(cx, kind, locale, defaultLocale, &result)) {
+ return false;
+ }
if (result) {
args.rval().setString(result);
} else {
@@ -325,122 +178,6 @@ bool js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) {
return true;
}
-JSLinearString* js::intl::ComputeDefaultLocale(JSContext* cx) {
- const char* locale = cx->realm()->getLocale();
- if (!locale) {
- ReportOutOfMemory(cx);
- return nullptr;
- }
-
- auto span = mozilla::MakeStringSpan(locale);
-
- mozilla::intl::Locale tag;
- bool canParseLocale =
- mozilla::intl::LocaleParser::TryParse(span, tag).isOk() &&
- tag.Canonicalize().isOk();
-
- Rooted<JSLinearString*> candidate(cx);
- if (!canParseLocale) {
- candidate = NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale());
- if (!candidate) {
- return nullptr;
- }
- } 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 nullptr;
- }
-
- candidate = buffer.toAsciiString(cx);
- if (!candidate) {
- return nullptr;
- }
-
- // 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 nullptr;
- }
- 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_NULL(
- cx, supportedCollator,
- BestAvailableLocale(cx, AvailableLocaleKind::Collator, candidate,
- nullptr));
-
- Rooted<JSLinearString*> supportedDateTimeFormat(cx);
- JS_TRY_VAR_OR_RETURN_NULL(
- cx, supportedDateTimeFormat,
- BestAvailableLocale(cx, AvailableLocaleKind::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 : {
- AvailableLocaleKind::DisplayNames,
- AvailableLocaleKind::DurationFormat,
- AvailableLocaleKind::ListFormat,
- AvailableLocaleKind::NumberFormat,
- AvailableLocaleKind::PluralRules,
- AvailableLocaleKind::RelativeTimeFormat,
- AvailableLocaleKind::Segmenter,
- }) {
- JSLinearString* supported;
- JS_TRY_VAR_OR_RETURN_NULL(
- 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)) {
- return supportedDateTimeFormat;
- }
- return supportedCollator;
- }
-
- // Return the last ditch locale if the candidate locale isn't supported.
- return NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale());
-}
-
using StringList = GCVector<JSLinearString*>;
/**
diff --git a/js/src/builtin/intl/IntlObject.h b/js/src/builtin/intl/IntlObject.h
@@ -67,17 +67,6 @@ extern const JSClass IntlClass;
*/
[[nodiscard]] extern bool intl_SupportedValuesOf(JSContext* cx, unsigned argc,
JS::Value* vp);
-
-namespace intl {
-
-/**
- * Return the supported locale for the default locale if ICU supports that
- * default locale (perhaps via fallback, e.g. supporting "de-CH" through "de"
- * support implied by a "de-DE" locale). Otherwise uses the last-ditch locale.
- */
-JSLinearString* ComputeDefaultLocale(JSContext* cx);
-
-} // namespace intl
} // namespace js
#endif /* builtin_intl_IntlObject_h */
diff --git a/js/src/builtin/intl/LocaleNegotiation.cpp b/js/src/builtin/intl/LocaleNegotiation.cpp
@@ -0,0 +1,304 @@
+/* -*- 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/LocaleNegotiation.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/Locale.h"
+
+#include <algorithm>
+#include <iterator>
+
+#include "builtin/intl/FormatBuffer.h"
+#include "builtin/intl/SharedIntlData.h"
+#include "builtin/intl/StringAsciiChars.h"
+#include "js/Result.h"
+#include "vm/JSContext.h"
+#include "vm/StringType.h"
+
+using namespace js;
+using namespace js::intl;
+
+static bool SameOrParentLocale(const JSLinearString* locale,
+ const 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;
+}
+
+// 9.2.2 BestAvailableLocale ( availableLocales, locale )
+static JS::Result<JSLinearString*> BestAvailableLocale(
+ JSContext* cx, AvailableLocaleKind availableLocales,
+ 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.isAvailableLocale(cx, availableLocales, 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,
+ AvailableLocaleKind availableLocales,
+ Handle<JSLinearString*> locale,
+ Handle<JSLinearString*> defaultLocale,
+ MutableHandle<JSLinearString*> result) {
+#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;
+ }
+
+ MOZ_ASSERT(StringEqualsAscii(locale, buffer.data(), buffer.length()),
+ "locale is a canonicalized language tag");
+ }
+#endif
+
+ JSLinearString* res;
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, res,
+ BestAvailableLocale(cx, availableLocales, locale, defaultLocale));
+ if (res) {
+ result.set(res);
+ } else {
+ result.set(nullptr);
+ }
+ return true;
+}
+
+JSLinearString* js::intl::ComputeDefaultLocale(JSContext* cx) {
+ const char* locale = cx->realm()->getLocale();
+ if (!locale) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+
+ auto span = mozilla::MakeStringSpan(locale);
+
+ mozilla::intl::Locale tag;
+ bool canParseLocale =
+ mozilla::intl::LocaleParser::TryParse(span, tag).isOk() &&
+ tag.Canonicalize().isOk();
+
+ Rooted<JSLinearString*> candidate(cx);
+ if (!canParseLocale) {
+ candidate = NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale());
+ if (!candidate) {
+ return nullptr;
+ }
+ } 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 nullptr;
+ }
+
+ candidate = buffer.toAsciiString(cx);
+ if (!candidate) {
+ return nullptr;
+ }
+
+ // 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 nullptr;
+ }
+ 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_NULL(
+ cx, supportedCollator,
+ BestAvailableLocale(cx, AvailableLocaleKind::Collator, candidate,
+ nullptr));
+
+ Rooted<JSLinearString*> supportedDateTimeFormat(cx);
+ JS_TRY_VAR_OR_RETURN_NULL(
+ cx, supportedDateTimeFormat,
+ BestAvailableLocale(cx, AvailableLocaleKind::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 : {
+ AvailableLocaleKind::DisplayNames,
+ AvailableLocaleKind::DurationFormat,
+ AvailableLocaleKind::ListFormat,
+ AvailableLocaleKind::NumberFormat,
+ AvailableLocaleKind::PluralRules,
+ AvailableLocaleKind::RelativeTimeFormat,
+ AvailableLocaleKind::Segmenter,
+ }) {
+ JSLinearString* supported;
+ JS_TRY_VAR_OR_RETURN_NULL(
+ 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)) {
+ return supportedDateTimeFormat;
+ }
+ return supportedCollator;
+ }
+
+ // Return the last ditch locale if the candidate locale isn't supported.
+ return NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale());
+}
diff --git a/js/src/builtin/intl/LocaleNegotiation.h b/js/src/builtin/intl/LocaleNegotiation.h
@@ -0,0 +1,47 @@
+/* -*- 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_LocaleNegotiation_h
+#define builtin_intl_LocaleNegotiation_h
+
+#include "js/RootingAPI.h"
+#include "js/TypeDecls.h"
+
+class JSLinearString;
+
+namespace js::intl {
+
+enum class AvailableLocaleKind;
+
+/**
+ * Compares a BCP 47 language tag against the locales in availableLocales and
+ * returns the best available match -- or |nullptr| 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 |defaultLocale|; otherwise pass |nullptr|.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.2.
+ * Spec: RFC 4647, section 3.4.
+ */
+bool BestAvailableLocale(JSContext* cx, AvailableLocaleKind availableLocales,
+ JS::Handle<JSLinearString*> locale,
+ JS::Handle<JSLinearString*> defaultLocale,
+ JS::MutableHandle<JSLinearString*> result);
+
+/**
+ * Return the supported locale for the default locale if ICU supports that
+ * default locale (perhaps via fallback, e.g. supporting "de-CH" through "de"
+ * support implied by a "de-DE" locale). Otherwise uses the last-ditch locale.
+ */
+JSLinearString* ComputeDefaultLocale(JSContext* cx);
+
+} // namespace js::intl
+
+#endif /* builtin_intl_LocaleNegotiation_h */
diff --git a/js/src/builtin/intl/moz.build b/js/src/builtin/intl/moz.build
@@ -27,6 +27,7 @@ UNIFIED_SOURCES += [
"LanguageTag.cpp",
"ListFormat.cpp",
"Locale.cpp",
+ "LocaleNegotiation.cpp",
"NumberFormat.cpp",
"PluralRules.cpp",
"RelativeTimeFormat.cpp",