diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/builtin/intl/IntlObject.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/builtin/intl/IntlObject.cpp')
-rw-r--r-- | js/src/builtin/intl/IntlObject.cpp | 846 |
1 files changed, 846 insertions, 0 deletions
diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp new file mode 100644 index 0000000000..35bb4035b3 --- /dev/null +++ b/js/src/builtin/intl/IntlObject.cpp @@ -0,0 +1,846 @@ +/* -*- 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/Likely.h" +#include "mozilla/Range.h" + +#include <algorithm> +#include <iterator> + +#include "jsapi.h" + +#include "builtin/Array.h" +#include "builtin/intl/Collator.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/DateTimeFormat.h" +#include "builtin/intl/LanguageTag.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/CharacterEncoding.h" +#include "js/Class.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "js/Result.h" +#include "js/StableStringChars.h" +#include "unicode/ucal.h" +#include "unicode/udat.h" +#include "unicode/udatpg.h" +#include "unicode/uloc.h" +#include "unicode/utypes.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/StringType.h" + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::Range; +using mozilla::RangedPtr; + +using JS::AutoStableStringChars; + +using js::intl::CallICU; +using js::intl::DateTimeFormatOptions; +using js::intl::IcuLocale; + +/******************** 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; + } + + UErrorCode status = U_ZERO_ERROR; + const UChar* uTimeZone = nullptr; + int32_t uTimeZoneLength = 0; + UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, locale.get(), + UCAL_DEFAULT, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject<UCalendar, ucal_close> toClose(cal); + + RootedObject info(cx, NewBuiltinClassInstance<PlainObject>(cx)); + if (!info) { + return false; + } + + RootedValue v(cx); + int32_t firstDayOfWeek = ucal_getAttribute(cal, UCAL_FIRST_DAY_OF_WEEK); + v.setInt32(firstDayOfWeek); + + if (!DefineDataProperty(cx, info, cx->names().firstDayOfWeek, v)) { + return false; + } + + int32_t minDays = ucal_getAttribute(cal, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK); + v.setInt32(minDays); + if (!DefineDataProperty(cx, info, cx->names().minDays, v)) { + return false; + } + + UCalendarWeekdayType prevDayType = + ucal_getDayOfWeekType(cal, UCAL_SATURDAY, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + RootedValue weekendStart(cx), weekendEnd(cx); + + for (int i = UCAL_SUNDAY; i <= UCAL_SATURDAY; i++) { + UCalendarDaysOfWeek dayOfWeek = static_cast<UCalendarDaysOfWeek>(i); + UCalendarWeekdayType type = ucal_getDayOfWeekType(cal, dayOfWeek, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + if (prevDayType != type) { + switch (type) { + case UCAL_WEEKDAY: + // If the first Weekday after Weekend is Sunday (1), + // then the last Weekend day is Saturday (7). + // Otherwise we'll just take the previous days number. + weekendEnd.setInt32(i == 1 ? 7 : i - 1); + break; + case UCAL_WEEKEND: + weekendStart.setInt32(i); + break; + case UCAL_WEEKEND_ONSET: + case UCAL_WEEKEND_CEASE: + // At the time this code was added, ICU apparently never behaves this + // way, so just throw, so that users will report a bug and we can + // decide what to do. + intl::ReportInternalError(cx); + return false; + default: + break; + } + } + + prevDayType = type; + } + + MOZ_ASSERT(weekendStart.isInt32()); + MOZ_ASSERT(weekendEnd.isInt32()); + + if (!DefineDataProperty(cx, info, cx->names().weekendStart, weekendStart)) { + return false; + } + + if (!DefineDataProperty(cx, info, cx->names().weekendEnd, weekendEnd)) { + return false; + } + + args.rval().setObject(*info); + return true; +} + +static void ReportBadKey(JSContext* cx, HandleString key) { + if (UniqueChars chars = QuoteString(cx, key, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, + chars.get()); + } +} + +template <typename ConstChar> +static bool MatchPart(RangedPtr<ConstChar> iter, const RangedPtr<ConstChar> end, + const char* part, size_t partlen) { + for (size_t i = 0; i < partlen; iter++, i++) { + if (iter == end || *iter != part[i]) { + return false; + } + } + + return true; +} + +template <typename ConstChar, size_t N> +inline bool MatchPart(RangedPtr<ConstChar>* iter, + const RangedPtr<ConstChar> end, const char (&part)[N]) { + if (!MatchPart(*iter, end, part, N - 1)) { + return false; + } + + *iter += N - 1; + return true; +} + +enum class DisplayNameStyle { + Narrow, + Short, + Long, +}; + +template <typename ConstChar> +static JSString* ComputeSingleDisplayName(JSContext* cx, UDateFormat* fmt, + UDateTimePatternGenerator* dtpg, + DisplayNameStyle style, + const Range<ConstChar>& pattern, + HandleString patternString) { + RangedPtr<ConstChar> iter = pattern.begin(); + const RangedPtr<ConstChar> end = pattern.end(); + + auto MatchSlash = [cx, patternString, &iter, end]() { + if (MOZ_LIKELY(iter != end && *iter == '/')) { + iter++; + return true; + } + + ReportBadKey(cx, patternString); + return false; + }; + + if (!MatchPart(&iter, end, "dates")) { + ReportBadKey(cx, patternString); + return nullptr; + } + + if (!MatchSlash()) { + return nullptr; + } + + if (MatchPart(&iter, end, "fields")) { + if (!MatchSlash()) { + return nullptr; + } + + UDateTimePatternField fieldType; + + if (MatchPart(&iter, end, "year")) { + fieldType = UDATPG_YEAR_FIELD; + } else if (MatchPart(&iter, end, "month")) { + fieldType = UDATPG_MONTH_FIELD; + } else if (MatchPart(&iter, end, "week")) { + fieldType = UDATPG_WEEK_OF_YEAR_FIELD; + } else if (MatchPart(&iter, end, "day")) { + fieldType = UDATPG_DAY_FIELD; + } else { + ReportBadKey(cx, patternString); + return nullptr; + } + + // This part must be the final part with no trailing data. + if (iter != end) { + ReportBadKey(cx, patternString); + return nullptr; + } + + int32_t resultSize; + const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize); + MOZ_ASSERT(resultSize >= 0); + + return NewStringCopyN<CanGC>(cx, value, size_t(resultSize)); + } + + if (MatchPart(&iter, end, "gregorian")) { + if (!MatchSlash()) { + return nullptr; + } + + UDateFormatSymbolType symbolType; + int32_t index; + + if (MatchPart(&iter, end, "months")) { + if (!MatchSlash()) { + return nullptr; + } + + switch (style) { + case DisplayNameStyle::Narrow: + symbolType = UDAT_STANDALONE_NARROW_MONTHS; + break; + + case DisplayNameStyle::Short: + symbolType = UDAT_STANDALONE_SHORT_MONTHS; + break; + + case DisplayNameStyle::Long: + symbolType = UDAT_STANDALONE_MONTHS; + break; + } + + if (MatchPart(&iter, end, "january")) { + index = UCAL_JANUARY; + } else if (MatchPart(&iter, end, "february")) { + index = UCAL_FEBRUARY; + } else if (MatchPart(&iter, end, "march")) { + index = UCAL_MARCH; + } else if (MatchPart(&iter, end, "april")) { + index = UCAL_APRIL; + } else if (MatchPart(&iter, end, "may")) { + index = UCAL_MAY; + } else if (MatchPart(&iter, end, "june")) { + index = UCAL_JUNE; + } else if (MatchPart(&iter, end, "july")) { + index = UCAL_JULY; + } else if (MatchPart(&iter, end, "august")) { + index = UCAL_AUGUST; + } else if (MatchPart(&iter, end, "september")) { + index = UCAL_SEPTEMBER; + } else if (MatchPart(&iter, end, "october")) { + index = UCAL_OCTOBER; + } else if (MatchPart(&iter, end, "november")) { + index = UCAL_NOVEMBER; + } else if (MatchPart(&iter, end, "december")) { + index = UCAL_DECEMBER; + } else { + ReportBadKey(cx, patternString); + return nullptr; + } + } else if (MatchPart(&iter, end, "weekdays")) { + if (!MatchSlash()) { + return nullptr; + } + + switch (style) { + case DisplayNameStyle::Narrow: + symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS; + break; + + case DisplayNameStyle::Short: + symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS; + break; + + case DisplayNameStyle::Long: + symbolType = UDAT_STANDALONE_WEEKDAYS; + break; + } + + if (MatchPart(&iter, end, "monday")) { + index = UCAL_MONDAY; + } else if (MatchPart(&iter, end, "tuesday")) { + index = UCAL_TUESDAY; + } else if (MatchPart(&iter, end, "wednesday")) { + index = UCAL_WEDNESDAY; + } else if (MatchPart(&iter, end, "thursday")) { + index = UCAL_THURSDAY; + } else if (MatchPart(&iter, end, "friday")) { + index = UCAL_FRIDAY; + } else if (MatchPart(&iter, end, "saturday")) { + index = UCAL_SATURDAY; + } else if (MatchPart(&iter, end, "sunday")) { + index = UCAL_SUNDAY; + } else { + ReportBadKey(cx, patternString); + return nullptr; + } + } else if (MatchPart(&iter, end, "dayperiods")) { + if (!MatchSlash()) { + return nullptr; + } + + symbolType = UDAT_AM_PMS; + + if (MatchPart(&iter, end, "am")) { + index = UCAL_AM; + } else if (MatchPart(&iter, end, "pm")) { + index = UCAL_PM; + } else { + ReportBadKey(cx, patternString); + return nullptr; + } + } else { + ReportBadKey(cx, patternString); + return nullptr; + } + + // This part must be the final part with no trailing data. + if (iter != end) { + ReportBadKey(cx, patternString); + return nullptr; + } + + return CallICU(cx, [fmt, symbolType, index](UChar* chars, int32_t size, + UErrorCode* status) { + return udat_getSymbols(fmt, symbolType, index, chars, size, status); + }); + } + + ReportBadKey(cx, patternString); + return nullptr; +} + +bool js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + // 1. Assert: locale is a string. + UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); + if (!locale) { + return false; + } + + // 2. Assert: style is a string. + DisplayNameStyle dnStyle; + { + JSLinearString* style = args[1].toString()->ensureLinear(cx); + if (!style) { + return false; + } + + if (StringEqualsLiteral(style, "narrow")) { + dnStyle = DisplayNameStyle::Narrow; + } else if (StringEqualsLiteral(style, "short")) { + dnStyle = DisplayNameStyle::Short; + } else { + MOZ_ASSERT(StringEqualsLiteral(style, "long")); + dnStyle = DisplayNameStyle::Long; + } + } + + // 3. Assert: keys is an Array. + RootedArrayObject keys(cx, &args[2].toObject().as<ArrayObject>()); + if (!keys) { + return false; + } + + // 4. Let result be ArrayCreate(0). + RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, keys->length())); + if (!result) { + return false; + } + result->ensureDenseInitializedLength(0, keys->length()); + + UErrorCode status = U_ZERO_ERROR; + + UDateFormat* fmt = + udat_open(UDAT_DEFAULT, UDAT_DEFAULT, IcuLocale(locale.get()), nullptr, 0, + nullptr, 0, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject<UDateFormat, udat_close> datToClose(fmt); + + // UDateTimePatternGenerator will be needed for translations of date and + // time fields like "month", "week", "day" etc. + UDateTimePatternGenerator* dtpg = + udatpg_open(IcuLocale(locale.get()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject<UDateTimePatternGenerator, udatpg_close> datPgToClose(dtpg); + + // 5. For each element of keys, + RootedString keyValStr(cx); + RootedValue v(cx); + for (uint32_t i = 0; i < keys->length(); i++) { + if (!GetElement(cx, keys, keys, i, &v)) { + return false; + } + + keyValStr = v.toString(); + + AutoStableStringChars stablePatternChars(cx); + if (!stablePatternChars.init(cx, keyValStr)) { + return false; + } + + // 5.a. Perform an implementation dependent algorithm to map a key to a + // corresponding display name. + JSString* displayName = + stablePatternChars.isLatin1() + ? ComputeSingleDisplayName(cx, fmt, dtpg, dnStyle, + stablePatternChars.latin1Range(), + keyValStr) + : ComputeSingleDisplayName(cx, fmt, dtpg, dnStyle, + stablePatternChars.twoByteRange(), + keyValStr); + if (!displayName) { + return false; + } + + // 5.b. Append the result string to result. + result->setDenseElement(i, StringValue(displayName)); + } + + // 6. Return result. + args.rval().setObject(*result); + return true; +} + +bool js::intl_GetLocaleInfo(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; + } + + RootedObject info(cx, NewBuiltinClassInstance<PlainObject>(cx)); + if (!info) { + return false; + } + + if (!DefineDataProperty(cx, info, cx->names().locale, args[0])) { + return false; + } + + bool rtl = uloc_isRightToLeft(IcuLocale(locale.get())); + + RootedValue dir(cx, StringValue(rtl ? cx->names().rtl : cx->names().ltr)); + + if (!DefineDataProperty(cx, info, cx->names().direction, dir)) { + return false; + } + + args.rval().setObject(*info); + return true; +} + +using SupportedLocaleKind = js::intl::SharedIntlData::SupportedLocaleKind; + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ) +static JS::Result<JSString*> 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->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. + 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 (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; + } + } + + 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"); + + MOZ_ASSERT(!tag.unicodeExtension(), + "locale must contain no Unicode extensions"); + + if (!tag.canonicalize(cx)) { + 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<CanGC>(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } else { + if (!tag.canonicalize(cx)) { + 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<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. + // + // Note: We don't test the supported locales of either Intl.ListFormat, + // Intl.PluralRules, 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<CanGC>(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } + + args.rval().setString(candidate); + 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_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) { + Handle<GlobalObject*> global = cx->global(); + RootedObject proto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global)); + if (!proto) { + return nullptr; + } + + // 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}; |