/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* Implementation of the Intl object and its non-constructor properties. */ #include "builtin/intl/IntlObject.h" #include "mozilla/Assertions.h" #include "mozilla/intl/Calendar.h" #include "mozilla/intl/Collator.h" #include "mozilla/intl/Currency.h" #include "mozilla/intl/MeasureUnitGenerated.h" #include "mozilla/intl/TimeZone.h" #include #include #include #include #include #include "builtin/Array.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/LocaleNegotiation.h" #include "builtin/intl/NumberingSystemsGenerated.h" #include "builtin/intl/SharedIntlData.h" #include "ds/Sort.h" #include "js/Class.h" #include "js/experimental/Intl.h" #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/GCAPI.h" #include "js/GCVector.h" #include "js/PropertyAndElement.h" #include "js/PropertySpec.h" #include "vm/GlobalObject.h" #include "vm/JSAtomUtils.h" // ClassName #include "vm/JSContext.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 namespace js::intl; /******************** Mozilla Intl extensions ********************/ /** * Returns a plain object with calendar information for a single valid locale * (callers must perform this validation). The object will have these * properties: * * firstDayOfWeek * an integer in the range 1=Monday to 7=Sunday indicating the day * considered the first day of the week in calendars, e.g. 7 for en-US, * 1 for en-GB, 7 for bn-IN * minDays * an integer in the range of 1 to 7 indicating the minimum number * of days required in the first week of the year, e.g. 1 for en-US, * 4 for de * weekend * an array with values in the range 1=Monday to 7=Sunday indicating the * days of the week considered as part of the weekend, e.g. [6, 7] for en-US * and en-GB, [7] for bn-IN (note that "weekend" is *not* necessarily two * days) * * NOTE: "calendar" and "locale" properties are *not* added to the object. */ static PlainObject* GetCalendarInfo(JSContext* cx, Handle loc) { auto locale = EncodeLocale(cx, loc); if (!locale) { return nullptr; } auto result = mozilla::intl::Calendar::TryCreate(locale.get()); if (result.isErr()) { ReportInternalError(cx, result.unwrapErr()); return nullptr; } auto calendar = result.unwrap(); Rooted properties(cx, cx); if (!properties.emplaceBack(NameToId(cx->names().locale), StringValue(loc))) { return nullptr; } auto type = calendar->GetBcp47Type(); if (type.isErr()) { ReportInternalError(cx, type.unwrapErr()); return nullptr; } auto* calendarType = NewStringCopy(cx, type.unwrap()); if (!calendarType) { return nullptr; } if (!properties.emplaceBack(NameToId(cx->names().calendar), StringValue(calendarType))) { return nullptr; } if (!properties.emplaceBack( NameToId(cx->names().firstDayOfWeek), Int32Value(static_cast(calendar->GetFirstDayOfWeek())))) { return nullptr; } if (!properties.emplaceBack( NameToId(cx->names().minDays), Int32Value(calendar->GetMinimalDaysInFirstWeek()))) { return nullptr; } auto weekend = calendar->GetWeekend(); if (weekend.isErr()) { ReportInternalError(cx, weekend.unwrapErr()); return nullptr; } auto weekendSet = weekend.unwrap(); auto* weekendArray = NewDenseFullyAllocatedArray(cx, weekendSet.size()); if (!weekendArray) { return nullptr; } weekendArray->setDenseInitializedLength(weekendSet.size()); size_t index = 0; for (auto day : weekendSet) { weekendArray->initDenseElement(index++, Int32Value(static_cast(day))); } MOZ_ASSERT(index == weekendSet.size()); if (!properties.emplaceBack(NameToId(cx->names().weekend), ObjectValue(*weekendArray))) { return nullptr; } return NewPlainObjectWithUniqueNames(cx, properties); } /** * This function is a custom function in the style of the standard Intl.* * functions, that isn't part of any spec or proposal yet. * * Returns an object with the following properties: * locale: * The actual resolved locale. * * calendar: * The default calendar of the resolved locale. * * firstDayOfWeek: * The first day of the week for the resolved locale. * * minDays: * The minimum number of days in a week for the resolved locale. * * weekend: * The days of the week considered as the weekend for the resolved locale. * * Days are encoded as integers in the range 1=Monday to 7=Sunday. */ static bool intl_getCalendarInfo(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales). Rooted requestedLocales( cx, CanonicalizeLocaleList(cx, args.get(0))); if (!requestedLocales) { return false; } // 2. Let localeOptions be a new Record. // 3. Set localeOptions.[[localeMatcher]] to "best fit". Rooted localeOptions(cx); // 4. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], // requestedLocales, localeOpt). auto localeData = LocaleData::Default; mozilla::EnumSet relevantExtensionKeys{ UnicodeExtensionKey::Calendar, }; Rooted resolved(cx); if (!ResolveLocale(cx, AvailableLocaleKind::DateTimeFormat, requestedLocales, localeOptions, relevantExtensionKeys, localeData, &resolved)) { return false; } Rooted locale(cx, resolved.toLocale(cx)); if (!locale) { return false; } // 5. Let result be GetCalendarInfo(r.[[locale]]). auto* result = GetCalendarInfo(cx, locale); if (!result) { return false; } // 6. Return result. args.rval().setObject(*result); return true; } static const JSFunctionSpec intl_extensions[] = { JS_FN("getCalendarInfo", intl_getCalendarInfo, 1, 0), JS_FS_END, }; bool JS::AddMozGetCalendarInfo(JSContext* cx, Handle intl) { return JS_DefineFunctions(cx, intl, intl_extensions); } /******************** Intl ********************/ using StringList = GCVector; /** * Create a sorted array from a list of strings. */ static ArrayObject* CreateArrayFromList(JSContext* cx, MutableHandle list) { // Reserve scratch space for MergeSort(). size_t initialLength = list.length(); if (!list.growBy(initialLength)) { return nullptr; } // Sort all strings in alphabetical order. MOZ_ALWAYS_TRUE( MergeSort(list.begin(), initialLength, list.begin() + initialLength, [](const auto* a, const auto* b, bool* lessOrEqual) { *lessOrEqual = CompareStrings(a, b) <= 0; return true; })); // Ensure we don't add duplicate entries to the array. auto* end = std::unique( list.begin(), list.begin() + initialLength, [](const auto* a, const auto* b) { return EqualStrings(a, b); }); // std::unique leaves the elements after |end| with an unspecified value, so // remove them first. And also delete the elements in the scratch space. list.shrinkBy(std::distance(end, list.end())); // And finally copy the strings into the result array. auto* array = NewDenseFullyAllocatedArray(cx, list.length()); if (!array) { return nullptr; } array->setDenseInitializedLength(list.length()); for (size_t i = 0; i < list.length(); ++i) { array->initDenseElement(i, StringValue(list[i])); } return array; } /** * Create an array from a sorted list of strings. */ template static ArrayObject* CreateArrayFromSortedList( JSContext* cx, const std::array& list) { // Ensure the list is sorted and doesn't contain duplicates. MOZ_ASSERT(std::adjacent_find(std::begin(list), std::end(list), [](const auto& a, const auto& b) { return std::strcmp(a, b) >= 0; }) == std::end(list)); size_t length = std::size(list); Rooted array(cx, NewDenseFullyAllocatedArray(cx, length)); if (!array) { return nullptr; } array->ensureDenseInitializedLength(0, length); for (size_t i = 0; i < length; ++i) { auto* str = NewStringCopyZ(cx, list[i]); if (!str) { return nullptr; } array->initDenseElement(i, StringValue(str)); } return array; } /** * Create an array from an intl::Enumeration. */ template static bool EnumerationIntoList(JSContext* cx, auto values, MutableHandle list) { for (auto value : values) { if (value.isErr()) { ReportInternalError(cx); return false; } auto span = value.unwrap(); // Skip over known, unsupported values. std::string_view sv(span.data(), span.size()); if (std::any_of(std::begin(unsupported), std::end(unsupported), [sv](const auto& e) { return sv == e; })) { continue; } auto* string = NewStringCopy(cx, span); if (!string) { return false; } if (!list.append(string)) { return false; } } return true; } /** * Create an array from an intl::ICU4XEnumeration. */ static bool ICU4XEnumerationIntoList(JSContext* cx, auto& values, MutableHandle list) { for (mozilla::Span value : values) { auto* string = NewStringCopy(cx, value); if (!string) { return false; } if (!list.append(string)) { return false; } } return true; } /** * Returns the list of calendar types which mustn't be returned by * |Intl.supportedValuesOf()|. */ static constexpr auto UnsupportedCalendars() { return std::array{ "islamic", "islamic-rgsa", }; } /** * AvailableCalendars ( ) */ static ArrayObject* AvailableCalendars(JSContext* cx) { Rooted list(cx, StringList(cx)); { // Hazard analysis complains that the mozilla::Result destructor calls a // GC function, which is unsound when returning an unrooted value. Work // around this issue by restricting the lifetime of |keywords| to a // separate block. auto keywords = mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale(""); if (keywords.isErr()) { ReportInternalError(cx, keywords.unwrapErr()); return nullptr; } static constexpr auto unsupported = UnsupportedCalendars(); if (!EnumerationIntoList(cx, keywords.unwrap(), &list)) { return nullptr; } } return CreateArrayFromList(cx, &list); } /** * AvailableCollations ( ) */ static ArrayObject* AvailableCollations(JSContext* cx) { Rooted list(cx, StringList(cx)); auto keywords = mozilla::intl::Collator::GetBcp47KeywordValues(); if (!ICU4XEnumerationIntoList(cx, keywords, &list)) { return nullptr; } return CreateArrayFromList(cx, &list); } /** * Returns a list of known, unsupported currencies which are returned by * |Currency::GetISOCurrencies()|. */ static constexpr auto UnsupportedCurrencies() { // "MVP" is also marked with "questionable, remove?" in ucurr.cpp, but only // this single currency code isn't supported by |Intl.DisplayNames| and // therefore must be excluded by |Intl.supportedValuesOf|. return std::array{ "LSM", // https://unicode-org.atlassian.net/browse/ICU-21687 }; } /** * AvailableCurrencies ( ) */ static ArrayObject* AvailableCurrencies(JSContext* cx) { Rooted list(cx, StringList(cx)); { // Hazard analysis complains that the mozilla::Result destructor calls a // GC function, which is unsound when returning an unrooted value. Work // around this issue by restricting the lifetime of |currencies| to a // separate block. auto currencies = mozilla::intl::Currency::GetISOCurrencies(); if (currencies.isErr()) { ReportInternalError(cx, currencies.unwrapErr()); return nullptr; } static constexpr auto unsupported = UnsupportedCurrencies(); if (!EnumerationIntoList(cx, currencies.unwrap(), &list)) { return nullptr; } } return CreateArrayFromList(cx, &list); } /** * AvailableNumberingSystems ( ) */ static ArrayObject* AvailableNumberingSystems(JSContext* cx) { static constexpr std::array numberingSystems = { NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS}; return CreateArrayFromSortedList(cx, numberingSystems); } /** * AvailableTimeZones ( ) */ static ArrayObject* AvailableTimeZones(JSContext* cx) { // Unsorted list of canonical time zone names, possibly containing duplicates. Rooted timeZones(cx, StringList(cx)); auto& sharedIntlData = cx->runtime()->sharedIntlData.ref(); auto iterResult = sharedIntlData.availableTimeZonesIteration(cx); if (iterResult.isErr()) { return nullptr; } auto iter = iterResult.unwrap(); Rooted validatedTimeZone(cx); for (; !iter.done(); iter.next()) { validatedTimeZone = iter.get(); // Canonicalize the time zone before adding it to the result array. auto* timeZone = sharedIntlData.canonicalizeTimeZone(cx, validatedTimeZone); if (!timeZone) { return nullptr; } if (!timeZones.append(timeZone)) { return nullptr; } } return CreateArrayFromList(cx, &timeZones); } template constexpr auto MeasurementUnitNames( const mozilla::intl::SimpleMeasureUnit (&units)[N]) { std::array array = {}; for (size_t i = 0; i < N; ++i) { array[i] = units[i].name; } return array; } /** * AvailableUnits ( ) */ static ArrayObject* AvailableUnits(JSContext* cx) { static constexpr auto simpleMeasureUnitNames = MeasurementUnitNames(mozilla::intl::simpleMeasureUnits); return CreateArrayFromSortedList(cx, simpleMeasureUnitNames); } /** * Intl.getCanonicalLocales ( locales ) */ static bool intl_getCanonicalLocales(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Steps 1-2. auto* array = CanonicalizeLocaleList(cx, args.get(0)); if (!array) { return false; } args.rval().setObject(*array); return true; } /** * Intl.supportedValuesOf ( key ) */ static bool intl_supportedValuesOf(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1. auto* key = ToString(cx, args.get(0)); if (!key) { return false; } auto* linearKey = key->ensureLinear(cx); if (!linearKey) { return false; } // Steps 2-8. ArrayObject* list; if (StringEqualsLiteral(linearKey, "calendar")) { list = AvailableCalendars(cx); } else if (StringEqualsLiteral(linearKey, "collation")) { list = AvailableCollations(cx); } else if (StringEqualsLiteral(linearKey, "currency")) { list = AvailableCurrencies(cx); } else if (StringEqualsLiteral(linearKey, "numberingSystem")) { list = AvailableNumberingSystems(cx); } else if (StringEqualsLiteral(linearKey, "timeZone")) { list = AvailableTimeZones(cx); } else if (StringEqualsLiteral(linearKey, "unit")) { list = AvailableUnits(cx); } else { if (UniqueChars chars = QuoteString(cx, linearKey, '"')) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, chars.get()); } return false; } if (!list) { return false; } // Step 9. args.rval().setObject(*list); return true; } static bool intl_toSource(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); args.rval().setString(cx->names().Intl); return true; } static const JSFunctionSpec intl_static_methods[] = { JS_FN("toSource", intl_toSource, 0, 0), JS_FN("getCanonicalLocales", intl_getCanonicalLocales, 1, 0), JS_FN("supportedValuesOf", intl_supportedValuesOf, 1, 0), JS_FS_END, }; static const JSPropertySpec intl_static_properties[] = { JS_STRING_SYM_PS(toStringTag, "Intl", JSPROP_READONLY), JS_PS_END, }; static JSObject* CreateIntlObject(JSContext* cx, JSProtoKey key) { Rooted proto(cx, &cx->global()->getObjectPrototype()); // The |Intl| object is just a plain object with some "static" function // properties and some constructor properties. return NewTenuredObjectWithGivenProto(cx, &IntlClass, proto); } /** * Initializes the Intl Object and its standard built-in properties. * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1 */ static bool IntlClassFinish(JSContext* cx, Handle intl, Handle proto) { // Add the constructor properties. Rooted ctorId(cx); Rooted ctorValue(cx); for (const auto& protoKey : { JSProto_Collator, JSProto_DateTimeFormat, JSProto_DisplayNames, JSProto_DurationFormat, JSProto_ListFormat, JSProto_Locale, JSProto_NumberFormat, JSProto_PluralRules, JSProto_RelativeTimeFormat, JSProto_Segmenter, }) { if (GlobalObject::skipDeselectedConstructor(cx, protoKey)) { continue; } 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::intl::IntlClass = { "Intl", JSCLASS_HAS_CACHED_PROTO(JSProto_Intl), JS_NULL_CLASS_OPS, &IntlClassSpec, };