From e8dd2eea74f9e0b2c5d58a6c16ac9795e251f0ec Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 24 Aug 2021 22:58:45 -0400 Subject: [PATCH] LibJS: Implement the Intl.DisplayNames constructor There is notably FIXME notations in this commit regarding Unicode locale extensions. We are not parsing extensions (or private use extensions) at all yet. --- Userland/Libraries/LibJS/CMakeLists.txt | 1 + .../LibJS/Runtime/CommonPropertyNames.h | 4 + Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 3 + .../LibJS/Runtime/Intl/AbstractOperations.cpp | 380 ++++++++++++++++++ .../LibJS/Runtime/Intl/AbstractOperations.h | 32 ++ .../Runtime/Intl/DisplayNamesConstructor.cpp | 74 ++++ .../Intl/DisplayNames/DisplayNames.js | 44 ++ 7 files changed, 538 insertions(+) create mode 100644 Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index be329b274a..b697e29417 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -72,6 +72,7 @@ set(SOURCES Runtime/GlobalEnvironment.cpp Runtime/GlobalObject.cpp Runtime/IndexedProperties.cpp + Runtime/Intl/AbstractOperations.cpp Runtime/Intl/DisplayNames.cpp Runtime/Intl/DisplayNamesConstructor.cpp Runtime/Intl/DisplayNamesPrototype.cpp diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 14985da501..0ec9e8c5ee 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -144,6 +144,7 @@ namespace JS { P(exec) \ P(exp) \ P(expm1) \ + P(fallback) \ P(fields) \ P(fill) \ P(filter) \ @@ -262,6 +263,7 @@ namespace JS { P(link) \ P(load) \ P(localeCompare) \ + P(localeMatcher) \ P(log) \ P(log1p) \ P(log2) \ @@ -376,6 +378,7 @@ namespace JS { P(store) \ P(strike) \ P(stringify) \ + P(style) \ P(sub) \ P(subarray) \ P(substr) \ @@ -416,6 +419,7 @@ namespace JS { P(trimRight) \ P(trimStart) \ P(trunc) \ + P(type) \ P(undefined) \ P(unescape) \ P(unicode) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 834035742c..1f3c8cc654 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -30,6 +30,7 @@ M(IndexOutOfRange, "Index {} is out of range of array length {}") \ M(InOperatorWithObject, "'in' operator must be used on an object") \ M(InstanceOfOperatorBadPrototype, "'prototype' property of {} is not an object") \ + M(IntlInvalidLanguageTag, "{} is not a structurally valid language tag") \ M(InvalidAssignToConst, "Invalid assignment to const variable") \ M(InvalidCodePoint, "Invalid code point {}, must be an integer no less than 0 and no greater than 0x10FFFF") \ M(InvalidFormat, "Invalid {} format") \ @@ -43,6 +44,7 @@ M(IsNotA, "{} is not a {}") \ M(IsNotAn, "{} is not an {}") \ M(IsNotAEvaluatedFrom, "{} is not a {} (evaluated from '{}')") \ + M(IsUndefined, "{} is undefined") \ M(IterableNextBadReturn, "iterator.next() returned a non-object value") \ M(IterableNextNotAFunction, "'next' property on returned object from Symbol.iterator method is not a function") \ M(IterableReturnBadReturn, "iterator.return() returned a non-object value") \ @@ -58,6 +60,7 @@ M(NotAn, "Not an {} object") \ M(NotAnObject, "{} is not an object") \ M(NotAnObjectOrNull, "{} is neither an object nor null") \ + M(NotAnObjectOrString, "{} is neither an object nor a string") \ M(NotASymbol, "{} is not a symbol") \ M(NotIterable, "{} is not iterable") \ M(NotObjectCoercible, "{} cannot be converted to an object") \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp new file mode 100644 index 0000000000..5c26aa6027 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2021, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS::Intl { + +// 6.2.2 IsStructurallyValidLanguageTag ( locale ), https://tc39.es/ecma402/#sec-isstructurallyvalidlanguagetag +static Optional is_structurally_valid_language_tag(StringView locale) +{ + // IsStructurallyValidLanguageTag returns true if all of the following conditions hold, false otherwise: + + // locale can be generated from the EBNF grammar for unicode_locale_id in Unicode Technical Standard #35 LDML § 3.2 Unicode Locale Identifier; + auto locale_id = Unicode::parse_unicode_locale_id(locale); + if (!locale_id.has_value()) + return {}; + + // locale does not use any of the backwards compatibility syntax described in Unicode Technical Standard #35 LDML § 3.3 BCP 47 Conformance; + // https://unicode.org/reports/tr35/#BCP_47_Conformance + if (locale.contains('_') || locale_id->language_id.is_root || !locale_id->language_id.language.has_value()) + return {}; + + // the unicode_language_id within locale contains no duplicate unicode_variant_subtag subtags; and + if (auto& variants = locale_id->language_id.variants; !variants.is_empty()) { + quick_sort(variants); + + for (size_t i = 0; i < variants.size() - 1; ++i) { + if (variants[i] == variants[i + 1]) + return {}; + } + } + + // FIXME: Handle extensions. + // if locale contains an extensions* component, that component + // does not contain any other_extensions components with duplicate [alphanum-[tTuUxX]] subtags, + // contains at most one unicode_locale_extensions component, + // contains at most one transformed_extensions component, and + // if a transformed_extensions component that contains a tlang component is present, then + // the tlang component contains no duplicate unicode_variant_subtag subtags. + + return locale_id; +} + +// 6.2.3 CanonicalizeUnicodeLocaleId ( locale ), https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid +static String canonicalize_unicode_locale_id(Unicode::LocaleID& locale) +{ + // 1. Let localeId be the string locale after performing the algorithm to transform it to canonical syntax per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers. + // 2. Let localeId be the string localeId after performing the algorithm to transform it to canonical form. + auto locale_id = Unicode::canonicalize_unicode_locale_id(locale); + VERIFY(locale_id.has_value()); + + // FIXME: Handle extensions. + // 3. If localeId contains a substring extension that is a Unicode locale extension sequence, then + // a. Let components be ! UnicodeExtensionComponents(extension). + // b. Let attributes be components.[[Attributes]]. + // c. Let keywords be components.[[Keywords]]. + // d. Let newExtension be "u". + // e. For each element attr of attributes, do + // i. Append "-" to newExtension. + // ii. Append attr to newExtension. + // f. For each Record { [[Key]], [[Value]] } keyword in keywords, do + // i. Append "-" to newExtension. + // ii. Append keyword.[[Key]] to newExtension. + // iii. If keyword.[[Value]] is not the empty String, then + // 1. Append "-" to newExtension. + // 2. Append keyword.[[Value]] to newExtension. + // g. Assert: newExtension is not equal to "u". + // h. Let localeId be localeId with the substring corresponding to extension replaced by the string newExtension. + + // 4. Return localeId. + return locale_id.release_value(); +} + +// 6.3.1 IsWellFormedCurrencyCode ( currency ), https://tc39.es/ecma402/#sec-canonicalcodefordisplaynames +static bool is_well_formed_currency_code(StringView currency) +{ + // 1. Let normalized be the result of mapping currency to upper case as described in 6.1. + // 2. If the number of elements in normalized is not 3, return false. + if (currency.length() != 3) + return false; + + // 3. If normalized contains any character that is not in the range "A" to "Z" (U+0041 to U+005A), return false. + if (!all_of(currency, is_ascii_alpha)) + return false; + + // 4. Return true. + return true; +} + +// 9.2.1 CanonicalizeLocaleList ( locales ), https://tc39.es/ecma402/#sec-canonicalizelocalelist +Vector canonicalize_locale_list(GlobalObject& global_object, Value locales) +{ + auto& vm = global_object.vm(); + + // 1. If locales is undefined, then + if (locales.is_undefined()) { + // a. Return a new empty List. + return {}; + } + + // 2. Let seen be a new empty List. + Vector seen; + + Object* object = nullptr; + // 3. If Type(locales) is String or Type(locales) is Object and locales has an [[InitializedLocale]] internal slot, then + // FIXME: When we have an Intl.Locale object, handle it it here. + if (locales.is_string()) { + // a. Let O be CreateArrayFromList(« locales »). + object = Array::create_from(global_object, { locales }); + } + // 4. Else, + else { + // a. Let O be ? ToObject(locales). + object = locales.to_object(global_object); + if (vm.exception()) + return {}; + } + + // 5. Let len be ? ToLength(? Get(O, "length")). + auto length_value = object->get(vm.names.length); + if (vm.exception()) + return {}; + auto length = length_value.to_length(global_object); + if (vm.exception()) + return {}; + + // 6. Let k be 0. + // 7. Repeat, while k < len, + for (size_t k = 0; k < length; ++k) { + // a. Let Pk be ToString(k). + auto property_key = PropertyName { k }; + + // b. Let kPresent be ? HasProperty(O, Pk). + auto key_present = object->has_property(property_key); + if (vm.exception()) + return {}; + + // c. If kPresent is true, then + if (key_present) { + // i. Let kValue be ? Get(O, Pk). + auto key_value = object->get(property_key); + if (vm.exception()) + return {}; + + // ii. If Type(kValue) is not String or Object, throw a TypeError exception. + if (!key_value.is_string() && !key_value.is_object()) { + vm.throw_exception(global_object, ErrorType::NotAnObjectOrString, key_value.to_string_without_side_effects()); + return {}; + } + + // iii. If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then + // 1. Let tag be kValue.[[Locale]]. + // iv. Else, + // 1. Let tag be ? ToString(kValue). + // FIXME: When we have an Intl.Locale object, handle it it here. + auto tag = key_value.to_string(global_object); + if (vm.exception()) + return {}; + + // v. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception. + auto locale_id = is_structurally_valid_language_tag(tag); + if (!locale_id.has_value()) { + vm.throw_exception(global_object, ErrorType::IntlInvalidLanguageTag, tag); + return {}; + } + + // vi. Let canonicalizedTag be CanonicalizeUnicodeLocaleId(tag). + auto canonicalized_tag = JS::Intl::canonicalize_unicode_locale_id(*locale_id); + + // vii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen. + if (!seen.contains_slow(canonicalized_tag)) + seen.append(move(canonicalized_tag)); + } + + // d. Increase k by 1. + } + + return seen; +} + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ), https://tc39.es/ecma402/#sec-bestavailablelocale +static Optional best_available_locale(StringView const& locale) +{ + // 1. Let candidate be locale. + StringView candidate = locale; + + // 2. Repeat, + while (true) { + // a. If availableLocales contains an element equal to candidate, return candidate. + if (Unicode::is_locale_available(candidate)) + return candidate; + + // b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined. + auto pos = candidate.find_last('-'); + if (!pos.has_value()) + return {}; + + // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, decrease pos by 2. + if ((*pos >= 2) && (candidate[*pos - 2] == '-')) + pos = *pos - 2; + + // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive. + candidate = candidate.substring_view(0, *pos); + } +} + +// 9.2.3 LookupMatcher ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupmatcher +static LocaleResult lookup_matcher(Vector const& requested_locales) +{ + // 1. Let result be a new Record. + LocaleResult result {}; + + // 2. For each element locale of requestedLocales, do + for (auto const& locale : requested_locales) { + // a. Let noExtensionsLocale be the String value that is locale with any Unicode locale extension sequences removed. + auto const& no_extensions_locale = locale; // FIXME: Handle extensions. + + // b. Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale). + auto available_locale = best_available_locale(no_extensions_locale); + + // c. If availableLocale is not undefined, then + if (available_locale.has_value()) { + // i. Set result.[[locale]] to availableLocale. + result.locale = available_locale.release_value(); + + // FIXME: Handle extensions. + // ii. If locale and noExtensionsLocale are not the same String value, then + // 1. Let extension be the String value consisting of the substring of the Unicode locale extension sequence within locale. + // 2. Set result.[[extension]] to extension. + + // iii. Return result. + return result; + } + } + + // 3. Let defLocale be DefaultLocale(). + // 4. Set result.[[locale]] to defLocale. + result.locale = Unicode::default_locale(); + + // 5. Return result. + return result; +} + +// 9.2.4 BestFitMatcher ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-bestfitmatcher +static LocaleResult best_fit_matcher(Vector const& requested_locales) +{ + // The algorithm is implementation dependent, but should produce results that a typical user of the requested locales would + // perceive as at least as good as those produced by the LookupMatcher abstract operation. + return lookup_matcher(requested_locales); +} + +// 9.2.7 ResolveLocale ( availableLocales, requestedLocales, options, relevantExtensionKeys, localeData ), https://tc39.es/ecma402/#sec-resolvelocale +LocaleResult resolve_locale(Vector const& requested_locales, LocaleOptions const& options, [[maybe_unused]] Vector relevant_extension_keys) +{ + // 1. Let matcher be options.[[localeMatcher]]. + auto const& matcher = options.locale_matcher; + LocaleResult result; + + // 2. If matcher is "lookup", then + if (matcher.is_string() && (matcher.as_string().string() == "lookup"sv)) { + // a. Let r be LookupMatcher(availableLocales, requestedLocales). + result = lookup_matcher(requested_locales); + } + // 3. Else, + else { + // a. Let r be BestFitMatcher(availableLocales, requestedLocales). + result = best_fit_matcher(requested_locales); + } + // 4. Let foundLocale be r.[[locale]]. + // 5. Let result be a new Record. + // 6. Set result.[[dataLocale]] to foundLocale. + + // FIXME: Handle extensions. + // 7. If r has an [[extension]] field, then + // a. Let components be ! UnicodeExtensionComponents(r.[[extension]]). + // b. Let keywords be components.[[Keywords]]. + // 8. Let supportedExtension be "-u". + // 9. For each element key of relevantExtensionKeys, do + // a. Let foundLocaleData be localeData.[[]]. + // b. Assert: Type(foundLocaleData) is Record. + // c. Let keyLocaleData be foundLocaleData.[[]]. + // d. Assert: Type(keyLocaleData) is List. + // e. Let value be keyLocaleData[0]. + // f. Assert: Type(value) is either String or Null. + // g. Let supportedExtensionAddition be "". + // h. If r has an [[extension]] field, then + // i. If keywords contains an element whose [[Key]] is the same as key, then + // 1. Let entry be the element of keywords whose [[Key]] is the same as key. + // 2. Let requestedValue be entry.[[Value]]. + // 3. If requestedValue is not the empty String, then + // a. If keyLocaleData contains requestedValue, then + // i. Let value be requestedValue. + // ii. Let supportedExtensionAddition be the string-concatenation of "-", key, "-", and value. + // 4. Else if keyLocaleData contains "true", then + // a. Let value be "true". + // b. Let supportedExtensionAddition be the string-concatenation of "-" and key. + // i. If options has a field [[]], then + // i. Let optionsValue be options.[[]]. + // ii. Assert: Type(optionsValue) is either String, Undefined, or Null. + // iii. If Type(optionsValue) is String, then + // 1. Let optionsValue be the string optionsValue after performing the algorithm steps to transform Unicode extension values to canonical syntax per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers, treating key as ukey and optionsValue as uvalue productions. + // 2. Let optionsValue be the string optionsValue after performing the algorithm steps to replace Unicode extension values with their canonical form per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers, treating key as ukey and optionsValue as uvalue productions. + // 3. If optionsValue is the empty String, then + // a. Let optionsValue be "true". + // iv. If keyLocaleData contains optionsValue, then + // 1. If SameValue(optionsValue, value) is false, then + // a. Let value be optionsValue. + // b. Let supportedExtensionAddition be "". + // j. Set result.[[]] to value. + // k. Append supportedExtensionAddition to supportedExtension. + // 10. If the number of elements in supportedExtension is greater than 2, then + // a. Let foundLocale be InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedExtension). + // 11. Set result.[[locale]] to foundLocale. + + // 12. Return result. + return result; +} + +// 9.2.13 GetOption ( options, property, type, values, fallback ), https://tc39.es/ecma402/#sec-getoption +Value get_option(GlobalObject& global_object, Value options, PropertyName const& property, Value::Type type, Vector const& values, Fallback fallback) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(options) is Object. + VERIFY(options.is_object()); + + // 2. Let value be ? Get(options, property). + auto value = options.get(global_object, property); + if (vm.exception()) + return {}; + + // 3. If value is undefined, return fallback. + if (value.is_undefined()) { + return fallback.visit( + [](Empty) { return js_undefined(); }, + [](bool f) { return Value(f); }, + [&vm](StringView f) { return Value(js_string(vm, f)); }); + } + + // 4. Assert: type is "boolean" or "string". + VERIFY((type == Value::Type::Boolean) || (type == Value::Type::String)); + + // 5. If type is "boolean", then + if (type == Value::Type::Boolean) { + // a. Let value be ! ToBoolean(value). + value = Value(value.to_boolean()); + } + // 6. If type is "string", then + else { + // a. Let value be ? ToString(value). + value = value.to_primitive_string(global_object); + if (vm.exception()) + return {}; + } + + // 7. If values is not undefined and values does not contain an element equal to value, throw a RangeError exception. + if (!values.is_empty()) { + // Note: Every location in the spec that invokes GetOption with type=boolean also has values=undefined. + VERIFY(value.is_string()); + if (!values.contains_slow(value.as_string().string())) { + vm.throw_exception(global_object, ErrorType::OptionIsNotValidValue, value.to_string_without_side_effects(), property.as_string()); + return {}; + } + } + + // 8. Return value. + return value; +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h new file mode 100644 index 0000000000..be2f1847af --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace JS::Intl { + +using Fallback = Variant; + +struct LocaleOptions { + Value locale_matcher; +}; + +struct LocaleResult { + String locale; +}; + +Vector canonicalize_locale_list(GlobalObject&, Value locales); +Value get_option(GlobalObject& global_object, Value options, PropertyName const& property, Value::Type type, Vector const& values, Fallback fallback); +LocaleResult resolve_locale(Vector const& requested_locales, LocaleOptions const& options, Vector relevant_extension_keys); + +} diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesConstructor.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesConstructor.cpp index 44600b1861..d8990d8ee4 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesConstructor.cpp @@ -6,8 +6,11 @@ #include #include +#include #include #include +#include +#include namespace JS::Intl { @@ -42,11 +45,82 @@ Value DisplayNamesConstructor::construct(FunctionObject& new_target) auto& vm = this->vm(); auto& global_object = this->global_object(); + auto locales = vm.argument(0); + auto options = vm.argument(1); + // 2. Let displayNames be ? OrdinaryCreateFromConstructor(NewTarget, "%DisplayNames.prototype%", « [[InitializedDisplayNames]], [[Locale]], [[Style]], [[Type]], [[Fallback]], [[Fields]] »). auto* display_names = ordinary_create_from_constructor(global_object, new_target, &GlobalObject::intl_display_names_prototype); if (vm.exception()) return {}; + // 3. Let requestedLocales be ? CanonicalizeLocaleList(locales). + auto requested_locales = canonicalize_locale_list(global_object, locales); + if (vm.exception()) + return {}; + + // 4. If options is undefined, throw a TypeError exception. + if (options.is_undefined()) { + vm.throw_exception(global_object, ErrorType::IsUndefined, "options"sv); + return {}; + } + + // 5. Set options to ? GetOptionsObject(options). + options = Temporal::get_options_object(global_object, options); + if (vm.exception()) + return {}; + + // 6. Let opt be a new Record. + LocaleOptions opt {}; + + // 7. Let localeData be %DisplayNames%.[[LocaleData]]. + + // 8. Let matcher be ? GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit"). + auto matcher = get_option(global_object, options, vm.names.localeMatcher, Value::Type::String, { "lookup"sv, "best fit"sv }, "best fit"sv); + if (vm.exception()) + return {}; + + // 9. Set opt.[[localeMatcher]] to matcher. + opt.locale_matcher = matcher; + + // 10. Let r be ResolveLocale(%DisplayNames%.[[AvailableLocales]], requestedLocales, opt, %DisplayNames%.[[RelevantExtensionKeys]]). + auto result = resolve_locale(requested_locales, opt, {}); + + // 11. Let style be ? GetOption(options, "style", "string", « "narrow", "short", "long" », "long"). + auto style = get_option(global_object, options, vm.names.style, Value::Type::String, { "narrow"sv, "short"sv, "long"sv }, "long"sv); + if (vm.exception()) + return {}; + + // 12. Set displayNames.[[Style]] to style. + display_names->set_style(style.as_string().string()); + + // 13. Let type be ? GetOption(options, "type", "string", « "language", "region", "script", "currency" », undefined). + auto type = get_option(global_object, options, vm.names.type, Value::Type::String, { "language"sv, "region"sv, "script"sv, "currency"sv }, Empty {}); + if (vm.exception()) + return {}; + + // 14. If type is undefined, throw a TypeError exception. + if (type.is_undefined()) { + vm.throw_exception(global_object, ErrorType::IsUndefined, "options.type"sv); + return {}; + } + + // 15. Set displayNames.[[Type]] to type. + display_names->set_type(type.as_string().string()); + + // 16. Let fallback be ? GetOption(options, "fallback", "string", « "code", "none" », "code"). + auto fallback = get_option(global_object, options, vm.names.fallback, Value::Type::String, { "code"sv, "none"sv }, "code"sv); + if (vm.exception()) + return {}; + + // 17. Set displayNames.[[Fallback]] to fallback. + display_names->set_fallback(fallback.as_string().string()); + + // 18. Set displayNames.[[Locale]] to r.[[locale]]. + display_names->set_locale(move(result.locale)); + + // Note: The remaining steps are skipped in favor of deferring to LibUnicode. We could copy + // the data from LibUnicode to the DisplayNames object, but for now we do not do that. + // 28. Return displayNames. return display_names; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DisplayNames/DisplayNames.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DisplayNames/DisplayNames.js index 8f1607575c..d5263dc932 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/DisplayNames/DisplayNames.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DisplayNames/DisplayNames.js @@ -4,10 +4,54 @@ describe("errors", () => { Intl.DisplayNames(); }).toThrowWithMessage(TypeError, "Intl.DisplayNames constructor must be called with 'new'"); }); + + test("options is undefined", () => { + expect(() => { + new Intl.DisplayNames("en"); + }).toThrowWithMessage(TypeError, "options is undefined"); + }); + + test("options is an invalid type", () => { + expect(() => { + new Intl.DisplayNames("en", true); + }).toThrowWithMessage(TypeError, "Options is not an object"); + }); + + test("style option is invalid ", () => { + expect(() => { + new Intl.DisplayNames("en", { style: "hello!" }); + }).toThrowWithMessage(RangeError, "hello! is not a valid value for option style"); + }); + + test("type option is invalid ", () => { + expect(() => { + new Intl.DisplayNames("en", { type: "hello!" }); + }).toThrowWithMessage(RangeError, "hello! is not a valid value for option type"); + }); + + test("fallback option is invalid ", () => { + expect(() => { + new Intl.DisplayNames("en", { type: "region", fallback: "hello!" }); + }).toThrowWithMessage(RangeError, "hello! is not a valid value for option fallback"); + }); + + test("missing type options ", () => { + expect(() => { + new Intl.DisplayNames("en", {}); + }).toThrowWithMessage(TypeError, "options.type is undefined"); + }); }); describe("normal behavior", () => { test("length is 2", () => { expect(Intl.DisplayNames).toHaveLength(2); }); + + test("all valid types", () => { + ["language", "region", "script", "currency"].forEach(type => { + expect(() => { + new Intl.DisplayNames("en", { type: type }); + }).not.toThrow(); + }); + }); });