From 4ad4a82a10c86e0e147d9ca784fcd9e58d652b76 Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 29 Jun 2023 23:08:11 +0200 Subject: [PATCH] Issue #2259 - Performance improvements in Locale - Move BestAvailableLocale function to C++ - Move default locale computation to C++ - Add available-locales set to SharedIntlData - Remove separate sets for DateFormat and NumberFormat available locales. Based-on: m-c 1373089 --- js/src/builtin/intl/Collator.cpp | 14 -- js/src/builtin/intl/Collator.h | 11 - js/src/builtin/intl/Collator.js | 22 +- js/src/builtin/intl/CommonFunctions.cpp | 35 +-- js/src/builtin/intl/CommonFunctions.h | 45 ++-- js/src/builtin/intl/CommonFunctions.js | 159 +----------- js/src/builtin/intl/DateTimeFormat.cpp | 14 -- js/src/builtin/intl/DateTimeFormat.h | 11 - js/src/builtin/intl/DateTimeFormat.js | 16 +- js/src/builtin/intl/ICUHeader.h | 1 + js/src/builtin/intl/IntlObject.cpp | 276 ++++++++++++++++++++- js/src/builtin/intl/IntlObject.h | 29 +++ js/src/builtin/intl/IntlObject.js | 12 +- js/src/builtin/intl/LanguageTag.cpp | 18 +- js/src/builtin/intl/LanguageTag.h | 12 +- js/src/builtin/intl/Locale.cpp | 21 +- js/src/builtin/intl/NumberFormat.cpp | 14 -- js/src/builtin/intl/NumberFormat.h | 11 - js/src/builtin/intl/NumberFormat.js | 16 +- js/src/builtin/intl/PluralRules.cpp | 16 -- js/src/builtin/intl/PluralRules.h | 11 - js/src/builtin/intl/PluralRules.js | 17 +- js/src/builtin/intl/RelativeTimeFormat.cpp | 17 -- js/src/builtin/intl/RelativeTimeFormat.h | 11 - js/src/builtin/intl/RelativeTimeFormat.js | 17 +- js/src/builtin/intl/SharedIntlData.cpp | 182 ++++++++++++++ js/src/builtin/intl/SharedIntlData.h | 93 +++++-- js/src/vm/SelfHosting.cpp | 7 +- 28 files changed, 647 insertions(+), 461 deletions(-) diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp index 446ffe7369..a351cf967d 100644 --- a/js/src/builtin/intl/Collator.cpp +++ b/js/src/builtin/intl/Collator.cpp @@ -24,7 +24,6 @@ #include "jsobjinlines.h" using namespace js; -using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::ReportInternalError; using js::intl::SharedIntlData; @@ -185,19 +184,6 @@ js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle Intl, extern MOZ_MUST_USE bool intl_Collator(JSContext* cx, unsigned argc, Value* vp); -/** - * Returns an object indicating the supported locales for collation - * by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_Collator_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp); - /** * Returns an array with the collation type identifiers per Unicode * Technical Standard 35, Unicode Locale Data Markup Language, for the diff --git a/js/src/builtin/intl/Collator.js b/js/src/builtin/intl/Collator.js index eb4f4b106e..bae7ee76c9 100644 --- a/js/src/builtin/intl/Collator.js +++ b/js/src/builtin/intl/Collator.js @@ -30,7 +30,7 @@ function resolveCollatorInternals(lazyCollatorData) var relevantExtensionKeys = Collator.relevantExtensionKeys; // Step 17. - var r = ResolveLocale(callFunction(Collator.availableLocales, Collator), + var r = ResolveLocale("Collator", lazyCollatorData.requestedLocales, lazyCollatorData.opt, relevantExtensionKeys, @@ -203,8 +203,7 @@ function Intl_Collator_supportedLocalesOf(locales /*, options*/) { var options = arguments.length > 1 ? arguments[1] : undefined; // Step 1. - var availableLocales = callFunction(collatorInternalProperties.availableLocales, - collatorInternalProperties); + var availableLocales = "Collator"; // Step 2. var requestedLocales = CanonicalizeLocaleList(locales); @@ -222,17 +221,6 @@ function Intl_Collator_supportedLocalesOf(locales /*, options*/) { var collatorInternalProperties = { sortLocaleData: collatorSortLocaleData, searchLocaleData: collatorSearchLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_Collator_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, relevantExtensionKeys: ["co", "kn", "kf"] }; @@ -245,10 +233,8 @@ function collatorActualLocale(locale) { // 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 BestAvailableLocaleHelper. - var availableLocales = callFunction(collatorInternalProperties.availableLocales, - collatorInternalProperties); - return BestAvailableLocaleIgnoringDefault(availableLocales, locale); + // can call intl_isUpperCaseFirst. Also see intl_BestAvailableLocale. + return BestAvailableLocaleIgnoringDefault("Collator", locale); } diff --git a/js/src/builtin/intl/CommonFunctions.cpp b/js/src/builtin/intl/CommonFunctions.cpp index c904d94a49..3afb3ef55e 100644 --- a/js/src/builtin/intl/CommonFunctions.cpp +++ b/js/src/builtin/intl/CommonFunctions.cpp @@ -83,33 +83,10 @@ js::intl::ReportInternalError(JSContext* cx) JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); } -bool -js::intl::GetAvailableLocales(JSContext* cx, CountAvailable countAvailable, - GetAvailable getAvailable, MutableHandleValue result) -{ - RootedObject locales(cx, NewObjectWithGivenProto(cx, nullptr)); - if (!locales) - return false; +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"}, +}; - uint32_t count = countAvailable(); - for (uint32_t i = 0; i < count; i++) { - const char* locale = getAvailable(i); - auto lang = DuplicateString(cx, locale); - if (!lang) - return false; - char* p; - while ((p = strchr(lang.get(), '_'))) - *p = '-'; - RootedAtom a(cx, Atomize(cx, lang.get(), strlen(lang.get()))); - if (!a) - return false; - if (!DefineProperty(cx, locales, a->asPropertyName(), TrueHandleValue, nullptr, nullptr, - JSPROP_ENUMERATE)) - { - return false; - } - } - - result.setObject(*locales); - return true; -} diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h index 03ed385f3c..66312ebc69 100644 --- a/js/src/builtin/intl/CommonFunctions.h +++ b/js/src/builtin/intl/CommonFunctions.h @@ -71,6 +71,32 @@ StringsAreEqual(JSAutoByteString& s1, const char* s2) return !strcmp(s1.ptr(), s2); } +/** + * 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]; + static inline const char* IcuLocale(const char* locale) { @@ -129,25 +155,6 @@ CallICU(JSContext* cx, const ICUStringFunction& strFn) return NewStringCopyN(cx, chars.begin(), size_t(size)); } -// 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); - -/** - * Return an object whose own property names are the locales indicated as - * available by |countAvailable| that provides an overall count, and by - * |getAvailable| that when called passing a number less than that count, - * returns the corresponding locale as a borrowed string. For example: - * - * RootedValue v(cx); - * if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &v)) - * return false; - */ -extern bool -GetAvailableLocales(JSContext* cx, CountAvailable countAvailable, GetAvailable getAvailable, - JS::MutableHandle result); - } // namespace intl } // namespace js diff --git a/js/src/builtin/intl/CommonFunctions.js b/js/src/builtin/intl/CommonFunctions.js index 9fad595979..effabf020f 100644 --- a/js/src/builtin/intl/CommonFunctions.js +++ b/js/src/builtin/intl/CommonFunctions.js @@ -138,78 +138,12 @@ function IsASCIIAlphaString(s) { return true; } -// 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. -function lastDitchLocale() { - // Per bug 1177929, strings don't clone out of self-hosted code as atoms, - // breaking IonBuilder::constant. Put this in a function for now. - 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. -var 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", -}; - - -var localeCandidateCache = { - runtimeDefaultLocale: undefined, - candidateDefaultLocale: undefined, -}; - - var localeCache = { runtimeDefaultLocale: undefined, defaultLocale: undefined, }; -/** - * Compute the candidate default locale: the locale *requested* to be used as - * the default locale. We'll use it if and only if ICU provides support (maybe - * fallback support, e.g. supporting "de-ZA" through "de" support implied by a - * "de-DE" locale). - */ -function DefaultLocaleIgnoringAvailableLocales() { - const runtimeDefaultLocale = RuntimeDefaultLocale(); - if (runtimeDefaultLocale === localeCandidateCache.runtimeDefaultLocale) - return localeCandidateCache.candidateDefaultLocale; - - // If we didn't get a cache hit, compute the candidate default locale and - // cache it. Fall back on the last-ditch locale when necessary. - var candidate = intl_TryValidateAndCanonicalizeLanguageTag(runtimeDefaultLocale); - if (candidate === null) { - candidate = lastDitchLocale(); - } 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. - candidate = removeUnicodeExtensions(candidate); - - if (hasOwn(candidate, oldStyleLanguageTagMappings)) - candidate = oldStyleLanguageTagMappings[candidate]; - } - - // Cache the candidate locale until the runtime default locale changes. - localeCandidateCache.candidateDefaultLocale = candidate; - localeCandidateCache.runtimeDefaultLocale = runtimeDefaultLocale; - - assertIsValidAndCanonicalLanguageTag(candidate, "the candidate"); - - return candidate; -} - - /** * Returns the BCP 47 language tag for the host environment's current locale. * @@ -221,29 +155,13 @@ function DefaultLocale() { return localeCache.defaultLocale; // If we didn't have a cache hit, compute the candidate default locale. - // Then use it as the actual default locale if ICU supports that locale - // (perhaps via fallback). Otherwise use the last-ditch locale. - var candidate = DefaultLocaleIgnoringAvailableLocales(); - var locale; - if (BestAvailableLocaleIgnoringDefault(callFunction(collatorInternalProperties.availableLocales, - collatorInternalProperties), - candidate) && - BestAvailableLocaleIgnoringDefault(callFunction(numberFormatInternalProperties.availableLocales, - numberFormatInternalProperties), - candidate) && - BestAvailableLocaleIgnoringDefault(callFunction(dateTimeFormatInternalProperties.availableLocales, - dateTimeFormatInternalProperties), - candidate)) - { - locale = candidate; - } else { - locale = lastDitchLocale(); - } + 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; @@ -251,31 +169,6 @@ function DefaultLocale() { } -/** - * Add old-style language tags without script code for locales that in current - * usage would include a script subtag. Also 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). - */ -function addSpecialMissingLanguageTags(availableLocales) { - // 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. - var oldStyleLocales = std_Object_getOwnPropertyNames(oldStyleLanguageTagMappings); - for (var i = 0; i < oldStyleLocales.length; i++) { - var oldStyleLocale = oldStyleLocales[i]; - if (availableLocales[oldStyleLanguageTagMappings[oldStyleLocale]]) - availableLocales[oldStyleLocale] = true; - } - - // Also forcibly provide the last-ditch locale. - var lastDitch = lastDitchLocale(); - assert(lastDitch === "en-GB" && availableLocales["en"], - "shouldn't be a need to add every locale implied by the last-" + - "ditch locale, merely just the last-ditch locale"); - availableLocales[lastDitch] = true; -} - - /** * Canonicalizes a locale list. * @@ -336,49 +229,6 @@ function CanonicalizeLocaleList(locales) { } -function BestAvailableLocaleHelper(availableLocales, locale, considerDefaultLocale) { - assertIsValidAndCanonicalLanguageTag(locale, "BestAvailableLocale locale"); - assert(startOfUnicodeExtensions(locale) < 0, "locale must contain no Unicode extensions"); - - // 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. - - var defaultLocale; - if (considerDefaultLocale) - defaultLocale = DefaultLocale(); - - var candidate = locale; - while (true) { - if (availableLocales[candidate]) - return candidate; - - if (considerDefaultLocale && candidate.length <= defaultLocale.length) { - if (candidate === defaultLocale) - return candidate; - if (callFunction(std_String_startsWith, defaultLocale, candidate + "-")) - return candidate; - } - - var pos = callFunction(std_String_lastIndexOf, candidate, "-"); - if (pos === -1) - return undefined; - - if (pos >= 2 && candidate[pos - 2] === "-") - pos -= 2; - - candidate = callFunction(String_substring, candidate, 0, pos); - } -} - - /** * Compares a BCP 47 language tag against the locales in availableLocales * and returns the best available match. Uses the fallback @@ -388,16 +238,15 @@ function BestAvailableLocaleHelper(availableLocales, locale, considerDefaultLoca * Spec: RFC 4647, section 3.4. */ function BestAvailableLocale(availableLocales, locale) { - return BestAvailableLocaleHelper(availableLocales, locale, true); + return intl_BestAvailableLocale(availableLocales, locale, DefaultLocale()); } - /** * Identical to BestAvailableLocale, but does not consider the default locale * during computation. */ function BestAvailableLocaleIgnoringDefault(availableLocales, locale) { - return BestAvailableLocaleHelper(availableLocales, locale, false); + return intl_BestAvailableLocale(availableLocales, locale, null); } /** diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp index 449456d30f..170cde9b50 100644 --- a/js/src/builtin/intl/DateTimeFormat.cpp +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -34,7 +34,6 @@ using JS::TimeClip; using js::intl::CallICU; using js::intl::DateTimeFormatOptions; -using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::SharedIntlData; @@ -228,19 +227,6 @@ js::AddMozDateTimeFormatConstructor(JSContext* cx, JS::Handle intl) return mozDateTimeFormatProto != nullptr; } -bool -js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - if (!GetAvailableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - static bool DefaultCalendar(JSContext* cx, const JSAutoByteString& locale, MutableHandleValue rval) { diff --git a/js/src/builtin/intl/DateTimeFormat.h b/js/src/builtin/intl/DateTimeFormat.h index 8173bb6662..7680975b6c 100644 --- a/js/src/builtin/intl/DateTimeFormat.h +++ b/js/src/builtin/intl/DateTimeFormat.h @@ -54,17 +54,6 @@ CreateDateTimeFormatPrototype(JSContext* cx, JS::Handle Intl, extern MOZ_MUST_USE bool intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); -/** - * Returns an object indicating the supported locales for date and time - * formatting by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_DateTimeFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - /** * Returns an array with the calendar type identifiers per Unicode * Technical Standard 35, Unicode Locale Data Markup Language, for the diff --git a/js/src/builtin/intl/DateTimeFormat.js b/js/src/builtin/intl/DateTimeFormat.js index 9d2dd06329..07b732451f 100644 --- a/js/src/builtin/intl/DateTimeFormat.js +++ b/js/src/builtin/intl/DateTimeFormat.js @@ -62,7 +62,7 @@ function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { var localeData = DateTimeFormat.localeData; // Step 11. - var r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + var r = ResolveLocale("DateTimeFormat", lazyDateTimeFormatData.requestedLocales, lazyDateTimeFormatData.localeOpt, DateTimeFormat.relevantExtensionKeys, @@ -852,8 +852,7 @@ function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { var options = arguments.length > 1 ? arguments[1] : undefined; // Step 1. - var availableLocales = callFunction(dateTimeFormatInternalProperties.availableLocales, - dateTimeFormatInternalProperties); + var availableLocales = "DateTimeFormat"; // Step 2. var requestedLocales = CanonicalizeLocaleList(locales); @@ -870,17 +869,6 @@ function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { */ var dateTimeFormatInternalProperties = { localeData: dateTimeFormatLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_DateTimeFormat_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, relevantExtensionKeys: ["ca", "nu", "hc"] }; diff --git a/js/src/builtin/intl/ICUHeader.h b/js/src/builtin/intl/ICUHeader.h index 57a353c8c9..c75df47402 100644 --- a/js/src/builtin/intl/ICUHeader.h +++ b/js/src/builtin/intl/ICUHeader.h @@ -13,6 +13,7 @@ #include "unicode/udatpg.h" #include "unicode/udisplaycontext.h" #include "unicode/uenum.h" +#include "unicode/uloc.h" #include "unicode/unum.h" #include "unicode/unumsys.h" #include "unicode/upluralrules.h" diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp index 1c1a8d2477..9caa2709a8 100644 --- a/js/src/builtin/intl/IntlObject.cpp +++ b/js/src/builtin/intl/IntlObject.cpp @@ -11,6 +11,9 @@ #include "mozilla/Likely.h" #include "mozilla/Range.h" +#include +#include + #include "jsapi.h" #include "jscntxt.h" #include "jsobj.h" @@ -18,12 +21,15 @@ #include "builtin/intl/Collator.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/DateTimeFormat.h" +#include "builtin/intl/LanguageTag.h" #include "builtin/intl/ICUHeader.h" #include "builtin/intl/Locale.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/PluralRules.h" #include "builtin/intl/RelativeTimeFormat.h" #include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "js/Result.h" #include "vm/GlobalObject.h" #include "jsobjinlines.h" @@ -32,7 +38,6 @@ using namespace js; using js::intl::CallICU; using js::intl::DateTimeFormatOptions; -using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; @@ -407,6 +412,275 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) return true; } +using SupportedLocaleKind = js::intl::SharedIntlData::SupportedLocaleKind; + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ) +static JS::Result +BestAvailableLocale(JSContext* cx, SupportedLocaleKind kind, HandleLinearString locale, + HandleLinearString 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->sharedIntlData; + + 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 corresponding forward iterator value, because reserve iterators point + // to one element past the forward iterator value. + return r - 1; + }; + + // Step 1. + RootedLinearString 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 && candidate->length() <= defaultLocale->length()) { + if (EqualStrings(candidate, defaultLocale)) { + return candidate.get(); + } + + if (candidate->length() < defaultLocale->length() && + HasSubstringAt(defaultLocale, candidate, 0) && + defaultLocale->latin1OrTwoByteChar(candidate->length()) == '-') { + 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 (StringEqualsAscii(typeStr, "Collator")) { + kind = SupportedLocaleKind::Collator; + } else if (StringEqualsAscii(typeStr, "DateTimeFormat")) { + kind = SupportedLocaleKind::DateTimeFormat; + } else if (StringEqualsAscii(typeStr, "NumberFormat")) { + kind = SupportedLocaleKind::NumberFormat; + } else if (StringEqualsAscii(typeStr, "PluralRules")) { + kind = SupportedLocaleKind::PluralRules; + } else { + MOZ_ASSERT(StringEqualsAscii(typeStr, "RelativeTimeFormat")); + kind = SupportedLocaleKind::RelativeTimeFormat; + } + } + + RootedLinearString locale(cx, args[1].toString()->ensureLinear(cx)); + if (!locale) { + return false; + } + + #ifdef DEBUG + { + intl::LanguageTag tag(cx); + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE(cx, ok, intl::LanguageTagParser::tryParse(cx, locale, tag)); + MOZ_ASSERT(ok, "locale is a structurally valid language tag"); + + auto isUnicodeExtension = [](const auto& extension) { + return extension[0] == 'u'; + }; + MOZ_ASSERT(std::none_of(tag.extensions().begin(), tag.extensions().end(), isUnicodeExtension), + "locale must contain no Unicode extensions"); + + if (!tag.canonicalize(cx, intl::LanguageTag::UnicodeExtensionCanonicalForm::No)) { + return false; + } + + JSString* tagStr = tag.toString(cx); + if (!tagStr) { + return false; + } + + bool canonical; + if (!EqualStrings(cx, locale, tagStr, &canonical)) { + return false; + } + MOZ_ASSERT(canonical, "locale is a canonicalized language tag"); + } + #endif + + MOZ_ASSERT(args[2].isNull() || args[2].isString()); + + RootedLinearString 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); + + RootedLinearString locale(cx, args[0].toString()->ensureLinear(cx)); + if (!locale) { + return false; + } + + intl::LanguageTag tag(cx); + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE(cx, ok, intl::LanguageTagParser::tryParse(cx, locale, tag)); + + RootedLinearString candidate(cx); + if (!ok) { + candidate = NewStringCopyZ(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } else { + if (!tag.canonicalize(cx, intl::LanguageTag::UnicodeExtensionCanonicalForm::No)) { + return false; + } + + // 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(); + + JSString* canonical = tag.toString(cx); + if (!canonical) { + return false; + } + + candidate = canonical->ensureLinear(cx); + if (!candidate) { + return false; + } + + for (const auto& mapping : js::intl::oldStyleLanguageTagMappings) { + const char* oldStyle = mapping.oldStyle; + const char* modernStyle = mapping.modernStyle; + + if (StringEqualsAscii(candidate, oldStyle)) { + candidate = NewStringCopyZ(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. + // + // Note: We don't test the supported locales of either Intl.PluralRules or + // Intl.RelativeTimeFormat, because ICU doesn't provide the necessary API to + // return actual set of supported locales for these constructors. Instead it + // returns the complete set of available locales for ULocale, which is a + // superset of the locales supported by Collator, NumberFormat, and + // DateTimeFormat. + bool isSupported = true; + for (auto kind : {SupportedLocaleKind::Collator, SupportedLocaleKind::DateTimeFormat, + SupportedLocaleKind::NumberFormat}) { + JSString* supported; + JS_TRY_VAR_OR_RETURN_FALSE(cx, supported, BestAvailableLocale(cx, kind, candidate, nullptr)); + + if (!supported) { + isSupported = false; + break; + } + } + + if (!isSupported) { + candidate = NewStringCopyZ(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } + + args.rval().setString(candidate); + return true; +} + const Class js::IntlClass = { js_Object_str, JSCLASS_HAS_CACHED_PROTO(JSProto_Intl) diff --git a/js/src/builtin/intl/IntlObject.h b/js/src/builtin/intl/IntlObject.h index 1f0b26c545..eb5b4c69f8 100644 --- a/js/src/builtin/intl/IntlObject.h +++ b/js/src/builtin/intl/IntlObject.h @@ -95,6 +95,35 @@ intl_GetCalendarInfo(JSContext* cx, unsigned argc, JS::Value* vp); extern MOZ_MUST_USE bool intl_ComputeDisplayNames(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) + */ +extern MOZ_MUST_USE bool +intl_BestAvailableLocale(JSContext* cx, unsigned argc, JS::Value* vp); + +/** + * Returns the input locale in its canonicalized form if ICU supports that + * locale (perhaps via fallback, e.g. supporting "de-ZA" through "de" support + * implied by a "de-DE" locale). Otherwise uses the last-ditch locale. + * + * Usage: result = intl_supportedLocaleOrFallback(locale) + */ +extern MOZ_MUST_USE bool +intl_supportedLocaleOrFallback(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 index 4baef4eca4..54e4699dd9 100644 --- a/js/src/builtin/intl/IntlObject.js +++ b/js/src/builtin/intl/IntlObject.js @@ -58,7 +58,7 @@ function Intl_getCalendarInfo(locales) { // 5. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], // requestedLocales, localeOpt, // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). - const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + const r = ResolveLocale("DateTimeFormat", requestedLocales, localeOpt, DateTimeFormat.relevantExtensionKeys, @@ -129,11 +129,11 @@ function Intl_getDisplayNames(locales, options) { // 7. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], requestedLocales, localeOpt, // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). - const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), - requestedLocales, - localeOpt, - DateTimeFormat.relevantExtensionKeys, - localeData); + const r = ResolveLocale("DateTimeFormat", + requestedLocales, + localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); // 8. Let style be ? GetOption(options, "style", "string", « "long", "short", "narrow" », "long"). const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); diff --git a/js/src/builtin/intl/LanguageTag.cpp b/js/src/builtin/intl/LanguageTag.cpp index 1f5c1fa110..e675e19686 100644 --- a/js/src/builtin/intl/LanguageTag.cpp +++ b/js/src/builtin/intl/LanguageTag.cpp @@ -199,6 +199,15 @@ bool LanguageTag::setUnicodeExtension(UniqueChars extension) { return extensions_.append(std::move(extension)); } +void LanguageTag::clearUnicodeExtension() { + auto p = std::find_if(extensions().begin(), extensions().end(), + [](const auto& ext) { return ext[0] == 'u'; }); + if (p != extensions().end()) { + size_t index = std::distance(extensions().begin(), p); + extensions_.erase(extensions_.begin() + index); + } +} + template static bool SortAlphabetically(JSContext* cx, Vector& subtags) { @@ -754,8 +763,13 @@ bool LanguageTag::canonicalizeTransformExtension( return true; } -bool LanguageTag::appendTo(JSContext* cx, StringBuffer& sb) const { - return LanguageTagToString(cx, *this, sb); +JSString* LanguageTag::toString(JSContext* cx) const { + StringBuffer sb(cx); + if (!LanguageTagToString(cx, *this, sb)) { + return nullptr; + } + + return sb.finishString(); } // Zero-terminated ICU Locale ID. diff --git a/js/src/builtin/intl/LanguageTag.h b/js/src/builtin/intl/LanguageTag.h index 5f190757b8..657458cecb 100644 --- a/js/src/builtin/intl/LanguageTag.h +++ b/js/src/builtin/intl/LanguageTag.h @@ -34,8 +34,6 @@ class JSString; namespace js { -class StringBuffer; - namespace intl { #ifdef DEBUG @@ -315,6 +313,11 @@ class MOZ_STACK_CLASS LanguageTag final { */ bool setUnicodeExtension(JS::UniqueChars extension); + /** + * Remove any Unicode extension subtag if present. + */ + void clearUnicodeExtension(); + /** * Set the private-use subtag. The input must be a valid, case-normalized * private-use subtag or the empty string. @@ -374,10 +377,9 @@ class MOZ_STACK_CLASS LanguageTag final { } /** - * Append the string representation of this language tag to the given - * string buffer. + * Return the string representation of this language tag. */ - bool appendTo(JSContext* cx, StringBuffer& sb) const; + JSString* toString(JSContext* cx) const; /** * Add likely-subtags to the language tag. diff --git a/js/src/builtin/intl/Locale.cpp b/js/src/builtin/intl/Locale.cpp index 2bd93732c2..7e26add298 100644 --- a/js/src/builtin/intl/Locale.cpp +++ b/js/src/builtin/intl/Locale.cpp @@ -106,12 +106,7 @@ static LocaleObject* CreateLocaleObject(JSContext* cx, HandleObject prototype, } } - StringBuffer sb(cx); - if (!tag.appendTo(cx, sb)) { - return nullptr; - } - - RootedString tagStr(cx, sb.finishString()); + RootedString tagStr(cx, tag.toString(cx)); if (!tagStr) { return nullptr; } @@ -1320,12 +1315,7 @@ bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc, return false; } - StringBuffer sb(cx); - if (!tag.appendTo(cx, sb)) { - return false; - } - - JSString* resultStr = sb.finishString(); + JSString* resultStr = tag.toString(cx); if (!resultStr) { return false; } @@ -1358,12 +1348,7 @@ bool js::intl_TryValidateAndCanonicalizeLanguageTag(JSContext* cx, return false; } - StringBuffer sb(cx); - if (!tag.appendTo(cx, sb)) { - return false; - } - - JSString* resultStr = sb.finishString(); + JSString* resultStr = tag.toString(cx); if (!resultStr) { return false; } diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp index b921fd780e..4f84fc16d2 100644 --- a/js/src/builtin/intl/NumberFormat.cpp +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -36,7 +36,6 @@ using mozilla::IsNaN; using mozilla::IsNegativeZero; using js::intl::CallICU; using js::intl::DateTimeFormatOptions; -using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; @@ -203,19 +202,6 @@ js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle 1 ? arguments[1] : undefined; // Step 1. - var availableLocales = callFunction(numberFormatInternalProperties.availableLocales, - numberFormatInternalProperties); + var availableLocales = "NumberFormat"; // Step 2. var requestedLocales = CanonicalizeLocaleList(locales); diff --git a/js/src/builtin/intl/PluralRules.cpp b/js/src/builtin/intl/PluralRules.cpp index c6058370fd..e6ad27577b 100644 --- a/js/src/builtin/intl/PluralRules.cpp +++ b/js/src/builtin/intl/PluralRules.cpp @@ -28,7 +28,6 @@ using namespace js; using mozilla::AssertedCast; using js::intl::CallICU; -using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; @@ -180,21 +179,6 @@ js::CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handle Intl, extern MOZ_MUST_USE bool intl_PluralRules(JSContext* cx, unsigned argc, Value* vp); -/** - * Returns an object indicating the supported locales for plural rules - * by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_PluralRules_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp); - /** * Returns a plural rule for the number x according to the effective * locale and the formatting options of the given PluralRules. diff --git a/js/src/builtin/intl/PluralRules.js b/js/src/builtin/intl/PluralRules.js index 73d0ffb9f5..4522b5c4a3 100644 --- a/js/src/builtin/intl/PluralRules.js +++ b/js/src/builtin/intl/PluralRules.js @@ -11,17 +11,6 @@ */ var pluralRulesInternalProperties = { localeData: pluralRulesLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_PluralRules_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, relevantExtensionKeys: [], }; @@ -50,7 +39,7 @@ function resolvePluralRulesInternals(lazyPluralRulesData) { var localeData = PluralRules.localeData; // Step 11. - const r = ResolveLocale(callFunction(PluralRules.availableLocales, PluralRules), + const r = ResolveLocale("PluralRules", lazyPluralRulesData.requestedLocales, lazyPluralRulesData.opt, PluralRules.relevantExtensionKeys, @@ -187,8 +176,8 @@ function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) { var options = arguments.length > 1 ? arguments[1] : undefined; // Step 1. - var availableLocales = callFunction(pluralRulesInternalProperties.availableLocales, - pluralRulesInternalProperties); + var availableLocales = "PluralRules"; + // Step 2. let requestedLocales = CanonicalizeLocaleList(locales); diff --git a/js/src/builtin/intl/RelativeTimeFormat.cpp b/js/src/builtin/intl/RelativeTimeFormat.cpp index bb66c30cc9..277baae4c4 100644 --- a/js/src/builtin/intl/RelativeTimeFormat.cpp +++ b/js/src/builtin/intl/RelativeTimeFormat.cpp @@ -26,7 +26,6 @@ using mozilla::Range; using mozilla::RangedPtr; using js::intl::CallICU; -using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; @@ -164,22 +163,6 @@ js::CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle Intl, JS::Handle global); -/** - * Returns an object indicating the supported locales for relative time format - * by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_RelativeTimeFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - /** * Returns a relative time as a string formatted according to the effective * locale and the formatting options of the given RelativeTimeFormat. diff --git a/js/src/builtin/intl/RelativeTimeFormat.js b/js/src/builtin/intl/RelativeTimeFormat.js index 92af7b0dca..fe021c47e0 100644 --- a/js/src/builtin/intl/RelativeTimeFormat.js +++ b/js/src/builtin/intl/RelativeTimeFormat.js @@ -11,17 +11,6 @@ */ var relativeTimeFormatInternalProperties = { localeData: relativeTimeFormatLocaleData, - _availableLocales: null, - availableLocales: function() // eslint-disable-line object-shorthand - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_RelativeTimeFormat_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, relevantExtensionKeys: [], }; @@ -41,7 +30,7 @@ function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) { var RelativeTimeFormat = relativeTimeFormatInternalProperties; // Steps 7-8. - const r = ResolveLocale(callFunction(RelativeTimeFormat.availableLocales, RelativeTimeFormat), + const r = ResolveLocale("RelativeTimeFormat", lazyRelativeTimeFormatData.requestedLocales, lazyRelativeTimeFormatData.opt, RelativeTimeFormat.relevantExtensionKeys, @@ -159,8 +148,8 @@ function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) { var options = arguments.length > 1 ? arguments[1] : undefined; // Step 1. - var availableLocales = callFunction(relativeTimeFormatInternalProperties.availableLocales, - relativeTimeFormatInternalProperties); + var availableLocales = "RelativeTimeFormat"; + // Step 2. let requestedLocales = CanonicalizeLocaleList(locales); diff --git a/js/src/builtin/intl/SharedIntlData.cpp b/js/src/builtin/intl/SharedIntlData.cpp index 4c0f5a5b03..7426646192 100644 --- a/js/src/builtin/intl/SharedIntlData.cpp +++ b/js/src/builtin/intl/SharedIntlData.cpp @@ -10,6 +10,7 @@ #include "mozilla/Assertions.h" #include "mozilla/HashFunctions.h" +#include #include #include "jsatom.h" @@ -21,6 +22,7 @@ #include "builtin/intl/ScopedICUObject.h" #include "builtin/intl/TimeZoneDataGenerated.h" #include "js/Utility.h" +#include "js/Vector.h" using js::HashNumber; using js::intl::StringsAreEqual; @@ -278,6 +280,13 @@ js::intl::SharedIntlData::LocaleHasher::Lookup::Lookup(JSLinearString* locale) 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) { @@ -297,6 +306,173 @@ js::intl::SharedIntlData::LocaleHasher::match(Locale key, const Lookup& lookup) return EqualChars(keyChars, lookup.twoByteChars, lookup.length); } +bool +js::intl::SharedIntlData::getAvailableLocales(JSContext* cx, LocaleSet& locales, + CountAvailable countAvailable, + GetAvailable getAvailable) +{ + 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 lang(cx); + + int32_t count = countAvailable(); + for (int32_t i = 0; i < count; i++) { + const char* locale = getAvailable(i); + size_t length = strlen(locale); + + lang.clear(); + if (!lang.append(locale, length)) + return false; + + std::replace(lang.begin(), lang.end(), '_', '-'); + + if (!addLocale(lang.begin(), length)) + return false; + } + + // Add old-style language tags without script code for locales that in current + // usage would include a script subtag. Also 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). + + // 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; + + LocaleHasher::Lookup lookup(modernStyle, strlen(modernStyle)); + if (locales.has(lookup)) { + if (!addLocale(oldStyle, strlen(oldStyle))) + return false; + } + } + + // Also forcibly provide the last-ditch locale. + { + 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 +static bool +IsSameAvailableLocales(CountAvailable countAvailable1, + GetAvailable getAvailable1, + CountAvailable countAvailable2, + GetAvailable getAvailable2) +{ + int32_t count = countAvailable1(); + if (count != countAvailable2()) { + return false; + } + for (int32_t i = 0; i < count; i++) { + if (getAvailable1(i) != getAvailable2(i)) { + return false; + } + } + return true; +} +#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. + if (supportedLocales.initialized()) + supportedLocales.finish(); + if (collatorSupportedLocales.initialized()) + collatorSupportedLocales.finish(); + if (!supportedLocales.init() || + !collatorSupportedLocales.init()) { + ReportOutOfMemory(cx); + return false; + } + + if (!getAvailableLocales(cx, supportedLocales, uloc_countAvailable, uloc_getAvailable)) + return false; + + if (!getAvailableLocales(cx, collatorSupportedLocales, ucol_countAvailable, ucol_getAvailable)) + return false; + + MOZ_ASSERT(IsSameAvailableLocales(uloc_countAvailable, uloc_getAvailable, + udat_countAvailable, udat_getAvailable)); + + MOZ_ASSERT(IsSameAvailableLocales(uloc_countAvailable, uloc_getAvailable, + unum_countAvailable, unum_getAvailable)); + + 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; + + + RootedLinearString 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::NumberFormat: + case SupportedLocaleKind::PluralRules: + case SupportedLocaleKind::RelativeTimeFormat: + *supported = supportedLocales.has(lookup); + return true; + } + MOZ_CRASH("Invalid Intl constructor"); + + return true; +} + bool js::intl::SharedIntlData::ensureUpperCaseFirstLocales(JSContext* cx) { @@ -393,6 +569,8 @@ js::intl::SharedIntlData::destroyInstance() availableTimeZones.finish(); ianaZonesTreatedAsLinksByICU.finish(); ianaLinksCanonicalizedDifferentlyByICU.finish(); + supportedLocales.finish(); + collatorSupportedLocales.finish(); upperCaseFirstLocales.finish(); } @@ -404,6 +582,8 @@ js::intl::SharedIntlData::trace(JSTracer* trc) availableTimeZones.trace(trc); ianaZonesTreatedAsLinksByICU.trace(trc); ianaLinksCanonicalizedDifferentlyByICU.trace(trc); + supportedLocales.trace(trc); + collatorSupportedLocales.trace(trc); upperCaseFirstLocales.trace(trc); } } @@ -414,5 +594,7 @@ js::intl::SharedIntlData::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf return availableTimeZones.sizeOfExcludingThis(mallocSizeOf) + ianaZonesTreatedAsLinksByICU.sizeOfExcludingThis(mallocSizeOf) + ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf) + + supportedLocales.sizeOfExcludingThis(mallocSizeOf) + + collatorSupportedLocales.sizeOfExcludingThis(mallocSizeOf) + upperCaseFirstLocales.sizeOfExcludingThis(mallocSizeOf); } diff --git a/js/src/builtin/intl/SharedIntlData.h b/js/src/builtin/intl/SharedIntlData.h index 2d5662f947..81834804a1 100644 --- a/js/src/builtin/intl/SharedIntlData.h +++ b/js/src/builtin/intl/SharedIntlData.h @@ -49,6 +49,12 @@ class SharedIntlData else twoByteChars = string->twoByteChars(nogc); } + + LinearStringLookup(const char* chars, size_t length) + : isLatin1(true), length(length) + { + latin1Chars = reinterpret_cast(chars); + } }; private: @@ -161,6 +167,76 @@ class SharedIntlData */ bool tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, JS::HandleString timeZone, JS::MutableHandleString result); + 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; + + // 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. + // + // 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); + + static bool getAvailableLocales(JSContext* cx, LocaleSet& locales, + CountAvailable countAvailable, + GetAvailable getAvailable); + + /** + * Precomputes the available locales sets. + */ + bool ensureSupportedLocales(JSContext* cx); + + public: + enum class SupportedLocaleKind { + Collator, + DateTimeFormat, + NumberFormat, + PluralRules, + RelativeTimeFormat + }; + + /** + * Sets |supported| to true if |locale| is supported by the requested Intl + * service constructor. Otherwise sets |supported| to false. + */ + MOZ_MUST_USE bool isSupportedLocale(JSContext* cx, SupportedLocaleKind kind, + JS::Handle locale, + bool* supported); + private: /** * The case first parameter (BCP47 key "kf") allows to switch the order of @@ -178,23 +254,6 @@ class SharedIntlData * track locales which use upper-case first as their default setting. */ - using Locale = JSAtom*; - - struct LocaleHasher - { - struct Lookup : LinearStringLookup - { - explicit Lookup(JSLinearString* locale); - }; - - static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } - static bool match(Locale key, const Lookup& lookup); - }; - - using LocaleSet = js::GCHashSet; - LocaleSet upperCaseFirstLocales; bool upperCaseFirstInitialized = false; diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index cce912759d..ef007a69db 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -2462,12 +2462,12 @@ static const JSFunctionSpec intrinsic_functions[] = { // See builtin/intl/*.h for descriptions of the intl_* functions. JS_FN("intl_availableCalendars", intl_availableCalendars, 1,0), JS_FN("intl_availableCollations", intl_availableCollations, 1,0), + JS_FN("intl_BestAvailableLocale", intl_BestAvailableLocale, 3, 0), + JS_FN("intl_supportedLocaleOrFallback", intl_supportedLocaleOrFallback, 1, 0), JS_FN("intl_canonicalizeTimeZone", intl_canonicalizeTimeZone, 1,0), JS_FN("intl_Collator", intl_Collator, 2,0), - JS_FN("intl_Collator_availableLocales", intl_Collator_availableLocales, 0,0), JS_FN("intl_CompareStrings", intl_CompareStrings, 3,0), JS_FN("intl_DateTimeFormat", intl_DateTimeFormat, 2,0), - JS_FN("intl_DateTimeFormat_availableLocales", intl_DateTimeFormat_availableLocales, 0,0), JS_FN("intl_defaultCalendar", intl_defaultCalendar, 1,0), JS_FN("intl_defaultTimeZone", intl_defaultTimeZone, 0,0), JS_FN("intl_defaultTimeZoneOffset", intl_defaultTimeZoneOffset, 0,0), @@ -2478,18 +2478,15 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("intl_isUpperCaseFirst", intl_isUpperCaseFirst, 1,0), JS_FN("intl_IsValidTimeZoneName", intl_IsValidTimeZoneName, 1,0), JS_FN("intl_NumberFormat", intl_NumberFormat, 2,0), - JS_FN("intl_NumberFormat_availableLocales", intl_NumberFormat_availableLocales, 0,0), JS_FN("intl_numberingSystem", intl_numberingSystem, 1,0), JS_FN("intl_patternForSkeleton", intl_patternForSkeleton, 2,0), JS_FN("intl_patternForStyle", intl_patternForStyle, 3,0), - JS_FN("intl_PluralRules_availableLocales", intl_PluralRules_availableLocales, 0,0), JS_FN("intl_GetPluralCategories", intl_GetPluralCategories, 2, 0), JS_FN("intl_SelectPluralRule", intl_SelectPluralRule, 2,0), JS_FN("intl_toLocaleLowerCase", intl_toLocaleLowerCase, 2,0), JS_FN("intl_toLocaleUpperCase", intl_toLocaleUpperCase, 2,0), JS_FN("intl_ValidateAndCanonicalizeLanguageTag", intl_ValidateAndCanonicalizeLanguageTag, 2, 0), JS_FN("intl_TryValidateAndCanonicalizeLanguageTag", intl_TryValidateAndCanonicalizeLanguageTag, 1, 0), - JS_FN("intl_RelativeTimeFormat_availableLocales", intl_RelativeTimeFormat_availableLocales, 0,0), JS_FN("intl_FormatRelativeTime", intl_FormatRelativeTime, 3,0), JS_INLINABLE_FN("IsCollator",