diff options
Diffstat (limited to 'js/src/builtin/intl/DisplayNames.cpp')
-rw-r--r-- | js/src/builtin/intl/DisplayNames.cpp | 1076 |
1 files changed, 1076 insertions, 0 deletions
diff --git a/js/src/builtin/intl/DisplayNames.cpp b/js/src/builtin/intl/DisplayNames.cpp new file mode 100644 index 0000000000..06e4de67f2 --- /dev/null +++ b/js/src/builtin/intl/DisplayNames.cpp @@ -0,0 +1,1076 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Intl.DisplayNames implementation. */ + +#include "builtin/intl/DisplayNames.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> +#include <iterator> + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "jsnum.h" +#include "jspubtd.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "builtin/String.h" +#include "gc/AllocKind.h" +#include "gc/FreeOp.h" +#include "gc/Rooting.h" +#include "js/CallArgs.h" +#include "js/Class.h" +#include "js/experimental/Intl.h" // JS::AddMozDisplayNamesConstructor +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/GCVector.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/Result.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Utility.h" +#include "unicode/ucal.h" +#include "unicode/ucurr.h" +#include "unicode/udat.h" +#include "unicode/udatpg.h" +#include "unicode/udisplaycontext.h" +#include "unicode/uldnames.h" +#include "unicode/uloc.h" +#include "unicode/umachine.h" +#include "unicode/utypes.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/List.h" +#include "vm/Printer.h" +#include "vm/Runtime.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" +#include "vm/StringType.h" + +#include "vm/JSObject-inl.h" +#include "vm/List-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using js::intl::CallICU; +using js::intl::IcuLocale; + +const JSClassOps DisplayNamesObject::classOps_ = {nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* enumerate */ + nullptr, /* newEnumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + DisplayNamesObject::finalize}; + +const JSClass DisplayNamesObject::class_ = { + "Intl.DisplayNames", + JSCLASS_HAS_RESERVED_SLOTS(DisplayNamesObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_DisplayNames) | + JSCLASS_FOREGROUND_FINALIZE, + &DisplayNamesObject::classOps_, &DisplayNamesObject::classSpec_}; + +const JSClass& DisplayNamesObject::protoClass_ = PlainObject::class_; + +static bool displayNames_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().DisplayNames); + return true; +} + +static const JSFunctionSpec displayNames_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", + "Intl_DisplayNames_supportedLocalesOf", 1, 0), + JS_FS_END}; + +static const JSFunctionSpec displayNames_methods[] = { + JS_SELF_HOSTED_FN("of", "Intl_DisplayNames_of", 1, 0), + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DisplayNames_resolvedOptions", 0, + 0), + JS_FN(js_toSource_str, displayNames_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec displayNames_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.DisplayNames", JSPROP_READONLY), + JS_PS_END}; + +static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec DisplayNamesObject::classSpec_ = { + GenericCreateConstructor<DisplayNames, 2, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<DisplayNamesObject>, + displayNames_static_methods, + nullptr, + displayNames_methods, + displayNames_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +enum class DisplayNamesOptions { + Standard, + + // Calendar display names are no longer available with the current spec + // proposal text, but may be re-enabled in the future. For our internal use + // we still need to have them present, so use a feature guard for now. + EnableMozExtensions, +}; + +/** + * Initialize a new Intl.DisplayNames object using the named self-hosted + * function. + */ +static bool InitializeDisplayNamesObject(JSContext* cx, HandleObject obj, + HandlePropertyName initializer, + HandleValue locales, + HandleValue options, + DisplayNamesOptions dnoptions) { + FixedInvokeArgs<4> args(cx); + + args[0].setObject(*obj); + args[1].set(locales); + args[2].set(options); + args[3].setBoolean(dnoptions == DisplayNamesOptions::EnableMozExtensions); + + RootedValue ignored(cx); + if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args, + &ignored)) { + return false; + } + + MOZ_ASSERT(ignored.isUndefined(), + "Unexpected return value from non-legacy Intl object initializer"); + return true; +} + +/** + * Intl.DisplayNames ([ locales [ , options ]]) + */ +static bool DisplayNames(JSContext* cx, const CallArgs& args, + DisplayNamesOptions dnoptions) { + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.DisplayNames")) { + return false; + } + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (dnoptions == DisplayNamesOptions::Standard) { + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DisplayNames, + &proto)) { + return false; + } + } else { + RootedObject newTarget(cx, &args.newTarget().toObject()); + if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Null, &proto)) { + return false; + } + } + + // TypeError anyway, but this gives a better error message. + if (!args.requireAtLeast(cx, "DisplayNames", 2)) { + return false; + } + + Rooted<DisplayNamesObject*> displayNames(cx); + displayNames = NewObjectWithClassProto<DisplayNamesObject>(cx, proto); + if (!displayNames) { + return false; + } + + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Steps 3-26. + if (!InitializeDisplayNamesObject(cx, displayNames, + cx->names().InitializeDisplayNames, locales, + options, dnoptions)) { + return false; + } + + // Step 27. + args.rval().setObject(*displayNames); + return true; +} + +static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return DisplayNames(cx, args, DisplayNamesOptions::Standard); +} + +static bool MozDisplayNames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return DisplayNames(cx, args, DisplayNamesOptions::EnableMozExtensions); +} + +void js::DisplayNamesObject::finalize(JSFreeOp* fop, JSObject* obj) { + MOZ_ASSERT(fop->onMainThread()); + + if (ULocaleDisplayNames* ldn = + obj->as<DisplayNamesObject>().getLocaleDisplayNames()) { + intl::RemoveICUCellMemory(fop, obj, DisplayNamesObject::EstimatedMemoryUse); + + uldn_close(ldn); + } +} + +bool JS::AddMozDisplayNamesConstructor(JSContext* cx, HandleObject intl) { + RootedObject ctor(cx, GlobalObject::createConstructor( + cx, MozDisplayNames, cx->names().DisplayNames, 2)); + if (!ctor) { + return false; + } + + RootedObject proto( + cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global())); + if (!proto) { + return false; + } + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) { + return false; + } + + if (!JS_DefineFunctions(cx, ctor, displayNames_static_methods)) { + return false; + } + + if (!JS_DefineFunctions(cx, proto, displayNames_methods)) { + return false; + } + + if (!JS_DefineProperties(cx, proto, displayNames_properties)) { + return false; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0); +} + +enum class DisplayNamesStyle { Long, Short, Narrow }; + +enum class DisplayNamesFallback { None, Code }; + +static ULocaleDisplayNames* NewULocaleDisplayNames( + JSContext* cx, const char* locale, DisplayNamesStyle displayStyle) { + UErrorCode status = U_ZERO_ERROR; + + UDisplayContext contexts[] = { + // Use the standard names, not the dialect names. + // For example "English (GB)" instead of "British English". + UDISPCTX_STANDARD_NAMES, + + // Assume the display names are used in a stand-alone context. + UDISPCTX_CAPITALIZATION_FOR_STANDALONE, + + // Select either the long or short form. There's no separate narrow form + // available in ICU, therefore we equate "narrow"/"short" styles here. + displayStyle == DisplayNamesStyle::Long ? UDISPCTX_LENGTH_FULL + : UDISPCTX_LENGTH_SHORT, + + // Don't apply substitutes, because we need to apply our own fallbacks. + UDISPCTX_NO_SUBSTITUTE, + }; + + ULocaleDisplayNames* ldn = uldn_openForContext(IcuLocale(locale), contexts, + std::size(contexts), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + return ldn; +} + +static ULocaleDisplayNames* GetOrCreateLocaleDisplayNames( + JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale, + DisplayNamesStyle displayStyle) { + // Obtain a cached ULocaleDisplayNames object. + ULocaleDisplayNames* ldn = displayNames->getLocaleDisplayNames(); + if (!ldn) { + ldn = NewULocaleDisplayNames(cx, locale, displayStyle); + if (!ldn) { + return nullptr; + } + displayNames->setLocaleDisplayNames(ldn); + + intl::AddICUCellMemory(displayNames, + DisplayNamesObject::EstimatedMemoryUse); + } + return ldn; +} + +static void ReportInvalidOptionError(JSContext* cx, const char* type, + HandleString option) { + if (UniqueChars str = QuoteString(cx, option, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, type, str.get()); + } +} + +static void ReportInvalidOptionError(JSContext* cx, const char* type, + double option) { + ToCStringBuf cbuf; + if (const char* str = NumberToCString(cx, &cbuf, option)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_DIGITS_VALUE, str); + } +} + +static JSString* GetLanguageDisplayName( + JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale, + DisplayNamesStyle displayStyle, DisplayNamesFallback fallback, + HandleLinearString languageStr) { + bool ok; + intl::LanguageTag tag(cx); + JS_TRY_VAR_OR_RETURN_NULL( + cx, ok, intl::LanguageTagParser::tryParseBaseName(cx, languageStr, tag)); + if (!ok) { + ReportInvalidOptionError(cx, "language", languageStr); + return nullptr; + } + + // ICU always canonicalizes the input locale, but since we know that ICU's + // canonicalization is incomplete, we need to perform our own canonicalization + // to ensure consistent result. + if (!tag.canonicalizeBaseName(cx)) { + return nullptr; + } + + UniqueChars languageChars = tag.toStringZ(cx); + if (!languageChars) { + return nullptr; + } + + ULocaleDisplayNames* ldn = + GetOrCreateLocaleDisplayNames(cx, displayNames, locale, displayStyle); + if (!ldn) { + return nullptr; + } + + JSString* str = CallICU(cx, [ldn, &languageChars](UChar* chars, uint32_t size, + UErrorCode* status) { + int32_t res = + uldn_localeDisplayName(ldn, languageChars.get(), chars, size, status); + + // |uldn_localeDisplayName| reports U_ILLEGAL_ARGUMENT_ERROR when no + // display name was found. + if (*status == U_ILLEGAL_ARGUMENT_ERROR) { + *status = U_ZERO_ERROR; + res = 0; + } + return res; + }); + if (!str) { + return nullptr; + } + + // Return the canonicalized input when no localized language name was found. + if (str->empty() && fallback == DisplayNamesFallback::Code) { + return NewStringCopyZ<CanGC>(cx, languageChars.get()); + } + + return str; +} + +template <typename CharT> +static JSString* NewStringCopy(JSContext* cx, mozilla::Span<const CharT> span) { + return NewStringCopyN<CanGC>(cx, span.data(), span.size()); +} + +static JSString* GetScriptDisplayName(JSContext* cx, + Handle<DisplayNamesObject*> displayNames, + const char* locale, + DisplayNamesStyle displayStyle, + DisplayNamesFallback fallback, + HandleLinearString scriptStr) { + intl::ScriptSubtag script; + if (!intl::ParseStandaloneScriptTag(scriptStr, script)) { + ReportInvalidOptionError(cx, "script", scriptStr); + return nullptr; + } + + intl::LanguageTag tag(cx); + tag.setLanguage("und"); + tag.setScript(script); + + // ICU always canonicalizes the input locale, but since we know that ICU's + // canonicalization is incomplete, we need to perform our own canonicalization + // to ensure consistent result. + if (!tag.canonicalizeBaseName(cx)) { + return nullptr; + } + MOZ_ASSERT(tag.script().present()); + + // |uldn_scriptDisplayName| doesn't use the stand-alone form for script + // subtags, so we're using |uloc_getDisplayScript| instead. (This only applies + // to the long form.) + // + // ICU bug: https://unicode-org.atlassian.net/browse/ICU-9301 + if (displayStyle == DisplayNamesStyle::Long) { + // |uloc_getDisplayScript| expects a full locale identifier as its input. + UniqueChars scriptChars = tag.toStringZ(cx); + if (!scriptChars) { + return nullptr; + } + + JSString* str = + CallICU(cx, [locale, &scriptChars](UChar* chars, uint32_t size, + UErrorCode* status) { + int32_t res = uloc_getDisplayScript(scriptChars.get(), locale, chars, + size, status); + + // |uloc_getDisplayScript| reports U_USING_DEFAULT_WARNING when no + // display name was found. + if (*status == U_USING_DEFAULT_WARNING) { + *status = U_ZERO_ERROR; + res = 0; + } + return res; + }); + if (!str) { + return nullptr; + } + + // Return the case-canonicalized input when no localized name was found. + if (str->empty() && fallback == DisplayNamesFallback::Code) { + script.toTitleCase(); + return NewStringCopy(cx, script.span()); + } + + return str; + } + + // Note: ICU requires the script subtag to be in canonical case. + const intl::ScriptSubtag& canonicalScript = tag.script(); + + char scriptChars[intl::LanguageTagLimits::ScriptLength + 1] = {}; + std::copy_n(canonicalScript.span().data(), canonicalScript.length(), + scriptChars); + + ULocaleDisplayNames* ldn = + GetOrCreateLocaleDisplayNames(cx, displayNames, locale, displayStyle); + if (!ldn) { + return nullptr; + } + + JSString* str = CallICU(cx, [ldn, scriptChars](UChar* chars, uint32_t size, + UErrorCode* status) { + int32_t res = uldn_scriptDisplayName(ldn, scriptChars, chars, size, status); + + // |uldn_scriptDisplayName| reports U_ILLEGAL_ARGUMENT_ERROR when no display + // name was found. + if (*status == U_ILLEGAL_ARGUMENT_ERROR) { + *status = U_ZERO_ERROR; + res = 0; + } + return res; + }); + if (!str) { + return nullptr; + } + + // Return the case-canonicalized input when no localized name was found. + if (str->empty() && fallback == DisplayNamesFallback::Code) { + script.toTitleCase(); + return NewStringCopy(cx, script.span()); + } + + return str; +} + +static JSString* GetRegionDisplayName(JSContext* cx, + Handle<DisplayNamesObject*> displayNames, + const char* locale, + DisplayNamesStyle displayStyle, + DisplayNamesFallback fallback, + HandleLinearString regionStr) { + intl::RegionSubtag region; + if (!intl::ParseStandaloneRegionTag(regionStr, region)) { + ReportInvalidOptionError(cx, "region", regionStr); + return nullptr; + } + + intl::LanguageTag tag(cx); + tag.setLanguage("und"); + tag.setRegion(region); + + // ICU always canonicalizes the input locale, but since we know that ICU's + // canonicalization is incomplete, we need to perform our own canonicalization + // to ensure consistent result. + if (!tag.canonicalizeBaseName(cx)) { + return nullptr; + } + MOZ_ASSERT(tag.region().present()); + + // Note: ICU requires the region subtag to be in canonical case. + const intl::RegionSubtag& canonicalRegion = tag.region(); + + char regionChars[intl::LanguageTagLimits::RegionLength + 1] = {}; + std::copy_n(canonicalRegion.span().data(), canonicalRegion.length(), + regionChars); + + ULocaleDisplayNames* ldn = + GetOrCreateLocaleDisplayNames(cx, displayNames, locale, displayStyle); + if (!ldn) { + return nullptr; + } + + JSString* str = CallICU(cx, [ldn, regionChars](UChar* chars, uint32_t size, + UErrorCode* status) { + int32_t res = uldn_regionDisplayName(ldn, regionChars, chars, size, status); + + // |uldn_regionDisplayName| reports U_ILLEGAL_ARGUMENT_ERROR when no display + // name was found. + if (*status == U_ILLEGAL_ARGUMENT_ERROR) { + *status = U_ZERO_ERROR; + res = 0; + } + return res; + }); + if (!str) { + return nullptr; + } + + // Return the case-canonicalized input when no localized name was found. + if (str->empty() && fallback == DisplayNamesFallback::Code) { + region.toUpperCase(); + return NewStringCopy(cx, region.span()); + } + + return str; +} + +static JSString* GetCurrencyDisplayName(JSContext* cx, const char* locale, + DisplayNamesStyle displayStyle, + DisplayNamesFallback fallback, + HandleLinearString currencyStr) { + // Inlined implementation of `IsWellFormedCurrencyCode ( currency )`. + if (currencyStr->length() != 3) { + ReportInvalidOptionError(cx, "currency", currencyStr); + return nullptr; + } + + char16_t currency[] = {currencyStr->latin1OrTwoByteChar(0), + currencyStr->latin1OrTwoByteChar(1), + currencyStr->latin1OrTwoByteChar(2), '\0'}; + + if (!mozilla::IsAsciiAlpha(currency[0]) || + !mozilla::IsAsciiAlpha(currency[1]) || + !mozilla::IsAsciiAlpha(currency[2])) { + ReportInvalidOptionError(cx, "currency", currencyStr); + return nullptr; + } + + UCurrNameStyle currencyStyle; + switch (displayStyle) { + case DisplayNamesStyle::Long: + currencyStyle = UCURR_LONG_NAME; + break; + case DisplayNamesStyle::Short: + currencyStyle = UCURR_SYMBOL_NAME; + break; + case DisplayNamesStyle::Narrow: + currencyStyle = UCURR_NARROW_SYMBOL_NAME; + break; + } + + int32_t length = 0; + UErrorCode status = U_ZERO_ERROR; + const char16_t* name = + ucurr_getName(currency, locale, currencyStyle, nullptr, &length, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + MOZ_ASSERT(length >= 0); + + if (status == U_USING_DEFAULT_WARNING) { + // Return the canonicalized input when no localized currency name was found. + if (fallback == DisplayNamesFallback::Code) { + // Canonical case for currency is upper case. + return js::StringToUpperCase(cx, currencyStr); + } + return cx->emptyString(); + } + + return NewStringCopyN<CanGC>(cx, name, size_t(length)); +} + +#ifdef DEBUG +static bool IsStandaloneMonth(UDateFormatSymbolType symbolType) { + switch (symbolType) { + case UDAT_STANDALONE_MONTHS: + case UDAT_STANDALONE_SHORT_MONTHS: + case UDAT_STANDALONE_NARROW_MONTHS: + return true; + + case UDAT_ERAS: + case UDAT_MONTHS: + case UDAT_SHORT_MONTHS: + case UDAT_WEEKDAYS: + case UDAT_SHORT_WEEKDAYS: + case UDAT_AM_PMS: + case UDAT_LOCALIZED_CHARS: + case UDAT_ERA_NAMES: + case UDAT_NARROW_MONTHS: + case UDAT_NARROW_WEEKDAYS: + case UDAT_STANDALONE_WEEKDAYS: + case UDAT_STANDALONE_SHORT_WEEKDAYS: + case UDAT_STANDALONE_NARROW_WEEKDAYS: + case UDAT_QUARTERS: + case UDAT_SHORT_QUARTERS: + case UDAT_STANDALONE_QUARTERS: + case UDAT_STANDALONE_SHORT_QUARTERS: + case UDAT_SHORTER_WEEKDAYS: + case UDAT_STANDALONE_SHORTER_WEEKDAYS: + case UDAT_CYCLIC_YEARS_WIDE: + case UDAT_CYCLIC_YEARS_ABBREVIATED: + case UDAT_CYCLIC_YEARS_NARROW: + case UDAT_ZODIAC_NAMES_WIDE: + case UDAT_ZODIAC_NAMES_ABBREVIATED: + case UDAT_ZODIAC_NAMES_NARROW: + return false; + } + + MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented symbol type"); + return false; +} +#endif + +static ListObject* GetDateTimeDisplayNames( + JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale, + HandleLinearString calendar, UDateFormatSymbolType symbolType, + mozilla::Span<const int32_t> indices) { + if (auto* names = displayNames->getDateTimeNames()) { + return names; + } + + intl::LanguageTag tag(cx); + if (!intl::LanguageTagParser::parse(cx, mozilla::MakeStringSpan(locale), + tag)) { + return nullptr; + } + + JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); + if (!keywords.emplaceBack("ca", calendar)) { + return nullptr; + } + + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return nullptr; + } + + UniqueChars localeWithCalendar = tag.toStringZ(cx); + if (!localeWithCalendar) { + return nullptr; + } + + constexpr char16_t* timeZone = nullptr; + constexpr int32_t timeZoneLength = 0; + + constexpr char16_t* pattern = nullptr; + constexpr int32_t patternLength = 0; + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* fmt = + udat_open(UDAT_DEFAULT, UDAT_DEFAULT, IcuLocale(localeWithCalendar.get()), + timeZone, timeZoneLength, pattern, patternLength, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + ScopedICUObject<UDateFormat, udat_close> datToClose(fmt); + + Rooted<ListObject*> names(cx, ListObject::create(cx)); + if (!names) { + return nullptr; + } + + RootedValue value(cx); + for (uint32_t i = 0; i < indices.size(); i++) { + int32_t index = indices[i]; + JSString* name = + CallICU(cx, [fmt, symbolType, index](UChar* chars, int32_t size, + UErrorCode* status) { + return udat_getSymbols(fmt, symbolType, index, chars, size, status); + }); + if (!name) { + return nullptr; + } + + // Everything except Undecimber should always have a non-empty name. + MOZ_ASSERT_IF(!IsStandaloneMonth(symbolType) || index != UCAL_UNDECIMBER, + !name->empty()); + + value.setString(name); + if (!names->append(cx, value)) { + return nullptr; + } + } + + displayNames->setDateTimeNames(names); + return names; +} + +static JSString* GetWeekdayDisplayName(JSContext* cx, + Handle<DisplayNamesObject*> displayNames, + const char* locale, + HandleLinearString calendar, + DisplayNamesStyle displayStyle, + HandleLinearString code) { + uint8_t weekday; + { + double d; + if (!StringToNumber(cx, code, &d)) { + return nullptr; + } + + // Inlined implementation of `IsValidWeekdayCode ( weekday )`. + if (!IsInteger(d) || d < 1 || d > 7) { + ReportInvalidOptionError(cx, "weekday", d); + return nullptr; + } + + weekday = uint8_t(d); + } + + UDateFormatSymbolType symbolType; + switch (displayStyle) { + case DisplayNamesStyle::Long: + symbolType = UDAT_STANDALONE_WEEKDAYS; + break; + + case DisplayNamesStyle::Short: + // ICU "short" is CLDR "abbreviated"; "shorter" is CLDR "short" format. + symbolType = UDAT_STANDALONE_SHORTER_WEEKDAYS; + break; + + case DisplayNamesStyle::Narrow: + symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS; + break; + } + + static constexpr int32_t indices[] = { + UCAL_MONDAY, UCAL_TUESDAY, UCAL_WEDNESDAY, UCAL_THURSDAY, + UCAL_FRIDAY, UCAL_SATURDAY, UCAL_SUNDAY}; + + ListObject* names = GetDateTimeDisplayNames( + cx, displayNames, locale, calendar, symbolType, mozilla::Span(indices)); + if (!names) { + return nullptr; + } + MOZ_ASSERT(names->length() == std::size(indices)); + + return names->get(weekday - 1).toString(); +} + +static JSString* GetMonthDisplayName( + JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale, + HandleLinearString calendar, DisplayNamesStyle displayStyle, + DisplayNamesFallback fallback, HandleLinearString code) { + uint8_t month; + { + double d; + if (!StringToNumber(cx, code, &d)) { + return nullptr; + } + + // Inlined implementation of `IsValidMonthCode ( month )`. + if (!IsInteger(d) || d < 1 || d > 13) { + ReportInvalidOptionError(cx, "month", d); + return nullptr; + } + + month = uint8_t(d); + } + + UDateFormatSymbolType symbolType; + switch (displayStyle) { + case DisplayNamesStyle::Long: + symbolType = UDAT_STANDALONE_MONTHS; + break; + + case DisplayNamesStyle::Short: + symbolType = UDAT_STANDALONE_SHORT_MONTHS; + break; + + case DisplayNamesStyle::Narrow: + symbolType = UDAT_STANDALONE_NARROW_MONTHS; + break; + } + + static constexpr int32_t indices[] = { + UCAL_JANUARY, UCAL_FEBRUARY, UCAL_MARCH, UCAL_APRIL, + UCAL_MAY, UCAL_JUNE, UCAL_JULY, UCAL_AUGUST, + UCAL_SEPTEMBER, UCAL_OCTOBER, UCAL_NOVEMBER, UCAL_DECEMBER, + UCAL_UNDECIMBER}; + + ListObject* names = GetDateTimeDisplayNames( + cx, displayNames, locale, calendar, symbolType, mozilla::Span(indices)); + if (!names) { + return nullptr; + } + MOZ_ASSERT(names->length() == std::size(indices)); + + JSString* str = names->get(month - 1).toString(); + if (str->empty() && fallback == DisplayNamesFallback::Code) { + return cx->staticStrings().getInt(month); + } + return str; +} + +static JSString* GetQuarterDisplayName(JSContext* cx, + Handle<DisplayNamesObject*> displayNames, + const char* locale, + HandleLinearString calendar, + DisplayNamesStyle displayStyle, + HandleLinearString code) { + uint8_t quarter; + { + double d; + if (!StringToNumber(cx, code, &d)) { + return nullptr; + } + + // Inlined implementation of `IsValidQuarterCode ( quarter )`. + if (!IsInteger(d) || d < 1 || d > 4) { + ReportInvalidOptionError(cx, "quarter", d); + return nullptr; + } + + quarter = uint8_t(d); + } + + UDateFormatSymbolType symbolType; + switch (displayStyle) { + case DisplayNamesStyle::Long: + symbolType = UDAT_STANDALONE_QUARTERS; + break; + + case DisplayNamesStyle::Short: + case DisplayNamesStyle::Narrow: + // CLDR "narrow" style not supported in ICU. + symbolType = UDAT_STANDALONE_SHORT_QUARTERS; + break; + } + + // ICU doesn't provide an enum for quarters. + static constexpr int32_t indices[] = {0, 1, 2, 3}; + + ListObject* names = GetDateTimeDisplayNames( + cx, displayNames, locale, calendar, symbolType, mozilla::Span(indices)); + if (!names) { + return nullptr; + } + MOZ_ASSERT(names->length() == std::size(indices)); + + return names->get(quarter - 1).toString(); +} + +static JSString* GetDayPeriodDisplayName( + JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale, + HandleLinearString calendar, HandleLinearString dayPeriod) { + // Inlined implementation of `IsValidDayPeriodCode ( dayperiod )`. + uint32_t index; + if (StringEqualsLiteral(dayPeriod, "am")) { + index = 0; + } else if (StringEqualsLiteral(dayPeriod, "pm")) { + index = 1; + } else { + ReportInvalidOptionError(cx, "dayPeriod", dayPeriod); + return nullptr; + } + + UDateFormatSymbolType symbolType = UDAT_AM_PMS; + + static constexpr int32_t indices[] = {UCAL_AM, UCAL_PM}; + + ListObject* names = GetDateTimeDisplayNames( + cx, displayNames, locale, calendar, symbolType, mozilla::Span(indices)); + if (!names) { + return nullptr; + } + MOZ_ASSERT(names->length() == std::size(indices)); + + return names->get(index).toString(); +} + +static JSString* GetDateTimeFieldDisplayName(JSContext* cx, const char* locale, + DisplayNamesStyle displayStyle, + HandleLinearString dateTimeField) { + // Inlined implementation of `IsValidDateTimeFieldCode ( field )`. + UDateTimePatternField field; + if (StringEqualsLiteral(dateTimeField, "era")) { + field = UDATPG_ERA_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "year")) { + field = UDATPG_YEAR_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "quarter")) { + field = UDATPG_QUARTER_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "month")) { + field = UDATPG_MONTH_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "weekOfYear")) { + field = UDATPG_WEEK_OF_YEAR_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "weekday")) { + field = UDATPG_WEEKDAY_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "day")) { + field = UDATPG_DAY_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "dayPeriod")) { + field = UDATPG_DAYPERIOD_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "hour")) { + field = UDATPG_HOUR_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "minute")) { + field = UDATPG_MINUTE_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "second")) { + field = UDATPG_SECOND_FIELD; + } else if (StringEqualsLiteral(dateTimeField, "timeZoneName")) { + field = UDATPG_ZONE_FIELD; + } else { + ReportInvalidOptionError(cx, "dateTimeField", dateTimeField); + return nullptr; + } + + UDateTimePGDisplayWidth width; + switch (displayStyle) { + case DisplayNamesStyle::Long: + width = UDATPG_WIDE; + break; + case DisplayNamesStyle::Short: + width = UDATPG_ABBREVIATED; + break; + case DisplayNamesStyle::Narrow: + width = UDATPG_NARROW; + break; + } + + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + UDateTimePatternGenerator* dtpg = + sharedIntlData.getDateTimePatternGenerator(cx, locale); + if (!dtpg) { + return nullptr; + } + + JSString* str = intl::CallICU(cx, [dtpg, field, width](UChar* chars, + uint32_t size, + UErrorCode* status) { + return udatpg_getFieldDisplayName(dtpg, field, width, chars, size, status); + }); + MOZ_ASSERT_IF(str, !str->empty()); + return str; +} + +/** + * intl_ComputeDisplayName(displayNames, locale, calendar, style, fallback, + * type, code) + */ +bool js::intl_ComputeDisplayName(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 7); + + Rooted<DisplayNamesObject*> displayNames( + cx, &args[0].toObject().as<DisplayNamesObject>()); + + UniqueChars locale = intl::EncodeLocale(cx, args[1].toString()); + if (!locale) { + return false; + } + + RootedLinearString calendar(cx, args[2].toString()->ensureLinear(cx)); + if (!calendar) { + return false; + } + + RootedLinearString code(cx, args[6].toString()->ensureLinear(cx)); + if (!code) { + return false; + } + + DisplayNamesStyle displayStyle; + { + JSLinearString* style = args[3].toString()->ensureLinear(cx); + if (!style) { + return false; + } + + if (StringEqualsLiteral(style, "long")) { + displayStyle = DisplayNamesStyle::Long; + } else if (StringEqualsLiteral(style, "short")) { + displayStyle = DisplayNamesStyle::Short; + } else { + MOZ_ASSERT(StringEqualsLiteral(style, "narrow")); + displayStyle = DisplayNamesStyle::Narrow; + } + } + + DisplayNamesFallback displayFallback; + { + JSLinearString* fallback = args[4].toString()->ensureLinear(cx); + if (!fallback) { + return false; + } + + if (StringEqualsLiteral(fallback, "none")) { + displayFallback = DisplayNamesFallback::None; + } else { + MOZ_ASSERT(StringEqualsLiteral(fallback, "code")); + displayFallback = DisplayNamesFallback::Code; + } + } + + JSLinearString* type = args[5].toString()->ensureLinear(cx); + if (!type) { + return false; + } + + JSString* result; + if (StringEqualsLiteral(type, "language")) { + result = GetLanguageDisplayName(cx, displayNames, locale.get(), + displayStyle, displayFallback, code); + } else if (StringEqualsLiteral(type, "script")) { + result = GetScriptDisplayName(cx, displayNames, locale.get(), displayStyle, + displayFallback, code); + } else if (StringEqualsLiteral(type, "region")) { + result = GetRegionDisplayName(cx, displayNames, locale.get(), displayStyle, + displayFallback, code); + } else if (StringEqualsLiteral(type, "currency")) { + result = GetCurrencyDisplayName(cx, locale.get(), displayStyle, + displayFallback, code); + } else if (StringEqualsLiteral(type, "weekday")) { + result = GetWeekdayDisplayName(cx, displayNames, locale.get(), calendar, + displayStyle, code); + } else if (StringEqualsLiteral(type, "month")) { + result = GetMonthDisplayName(cx, displayNames, locale.get(), calendar, + displayStyle, displayFallback, code); + } else if (StringEqualsLiteral(type, "quarter")) { + result = GetQuarterDisplayName(cx, displayNames, locale.get(), calendar, + displayStyle, code); + } else if (StringEqualsLiteral(type, "dayPeriod")) { + result = + GetDayPeriodDisplayName(cx, displayNames, locale.get(), calendar, code); + } else { + MOZ_ASSERT(StringEqualsLiteral(type, "dateTimeField")); + result = GetDateTimeFieldDisplayName(cx, locale.get(), displayStyle, code); + } + if (!result) { + return false; + } + + if (!result->empty()) { + args.rval().setString(result); + } else if (displayFallback == DisplayNamesFallback::Code) { + args.rval().setString(code); + } else { + args.rval().setUndefined(); + } + return true; +} |