diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index e790fac620..9e13c7959f 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -84,6 +84,7 @@ namespace JS { P(call) \ P(callee) \ P(caller) \ + P(caseFirst) \ P(cause) \ P(cbrt) \ P(ceil) \ @@ -93,6 +94,7 @@ namespace JS { P(clear) \ P(clz32) \ P(codePointAt) \ + P(collation) \ P(compareExchange) \ P(compile) \ P(concat) \ @@ -224,6 +226,7 @@ namespace JS { P(hasOwn) \ P(hasOwnProperty) \ P(hour) \ + P(hourCycle) \ P(hours) \ P(hypot) \ P(id) \ @@ -262,6 +265,7 @@ namespace JS { P(join) \ P(keyFor) \ P(keys) \ + P(language) \ P(lastIndex) \ P(lastIndexOf) \ P(length) \ @@ -297,6 +301,8 @@ namespace JS { P(negated) \ P(next) \ P(now) \ + P(numberingSystem) \ + P(numeric) \ P(of) \ P(offset) \ P(offsetNanoseconds) \ @@ -325,6 +331,7 @@ namespace JS { P(reason) \ P(reduce) \ P(reduceRight) \ + P(region) \ P(reject) \ P(repeat) \ P(resolve) \ @@ -335,6 +342,7 @@ namespace JS { P(round) \ P(roundingIncrement) \ P(roundingMode) \ + P(script) \ P(seal) \ P(second) \ P(seconds) \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp index 7d8fb3ca2a..34aa2869f6 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp @@ -17,7 +17,7 @@ namespace JS::Intl { // 6.2.2 IsStructurallyValidLanguageTag ( locale ), https://tc39.es/ecma402/#sec-isstructurallyvalidlanguagetag -static Optional is_structurally_valid_language_tag(StringView locale) +Optional is_structurally_valid_language_tag(StringView locale) { auto contains_duplicate_variant = [](auto& variants) { if (variants.is_empty()) @@ -77,7 +77,7 @@ static Optional is_structurally_valid_language_tag(StringView } // 6.2.3 CanonicalizeUnicodeLocaleId ( locale ), https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid -static String canonicalize_unicode_locale_id(Unicode::LocaleID& locale) +String canonicalize_unicode_locale_id(Unicode::LocaleID& locale) { // Note: This implementation differs from the spec in how Step 3 is implemented. The spec assumes // the input to this method is a string, and is written such that operations are performed on parts @@ -309,6 +309,17 @@ static MatcherResult best_fit_matcher(Vector const& requested_locales) return lookup_matcher(requested_locales); } +// 9.2.6 InsertUnicodeExtensionAndCanonicalize ( locale, extension ), https://tc39.es/ecma402/#sec-insert-unicode-extension-and-canonicalize +String insert_unicode_extension_and_canonicalize(Unicode::LocaleID locale, Unicode::LocaleExtension extension) +{ + // Note: This implementation differs from the spec in how the extension is inserted. The spec assumes + // the input to this method is a string, and is written such that operations are performed on parts + // of that string. LibUnicode gives us the parsed locale in a structure, so we can mutate that + // structure directly. + locale.extensions.append(move(extension)); + return JS::Intl::canonicalize_unicode_locale_id(locale); +} + // 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) { @@ -385,6 +396,19 @@ LocaleResult resolve_locale(Vector const& requested_locales, LocaleOptio return result; } +// 9.2.12 CoerceOptionsToObject ( options ), https://tc39.es/ecma402/#sec-coerceoptionstoobject +Object* coerce_options_to_object(GlobalObject& global_object, Value options) +{ + // 1. If options is undefined, then + if (options.is_undefined()) { + // a. Return ! OrdinaryObjectCreate(null). + return Object::create(global_object, nullptr); + } + + // 2. Return ? ToObject(options). + return options.to_object(global_object); +} + // 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) { diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h index 182a79f705..be5c4c9e7d 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace JS::Intl { @@ -25,8 +26,12 @@ struct LocaleResult { String locale; }; +Optional is_structurally_valid_language_tag(StringView locale); +String canonicalize_unicode_locale_id(Unicode::LocaleID& locale); Vector canonicalize_locale_list(GlobalObject&, Value locales); +Object* coerce_options_to_object(GlobalObject& global_object, Value options); Value get_option(GlobalObject& global_object, Value options, PropertyName const& property, Value::Type type, Vector const& values, Fallback fallback); +String insert_unicode_extension_and_canonicalize(Unicode::LocaleID locale_id, Unicode::LocaleExtension extension); LocaleResult resolve_locale(Vector const& requested_locales, LocaleOptions const& options, Vector relevant_extension_keys); Value canonical_code_for_display_names(GlobalObject&, DisplayNames::Type type, StringView code); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/LocaleConstructor.cpp b/Userland/Libraries/LibJS/Runtime/Intl/LocaleConstructor.cpp index 8db4311478..3ee98bb1b6 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/LocaleConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/LocaleConstructor.cpp @@ -4,13 +4,248 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include +#include #include #include +#include #include #include +#include namespace JS::Intl { +struct LocaleAndKeys { + String locale; + Optional ca; + Optional co; + Optional hc; + Optional kf; + Optional kn; + Optional nu; +}; + +static Vector const& locale_relevant_extension_keys() +{ + // 14.2.2 Internal slots, https://tc39.es/ecma402/#sec-intl.locale-internal-slots + // The value of the [[RelevantExtensionKeys]] internal slot is « "ca", "co", "hc", "kf", "kn", "nu" ». + // If %Collator%.[[RelevantExtensionKeys]] does not contain "kf", then remove "kf" from %Locale%.[[RelevantExtensionKeys]]. + // If %Collator%.[[RelevantExtensionKeys]] does not contain "kn", then remove "kn" from %Locale%.[[RelevantExtensionKeys]]. + + // FIXME: We do not yet have an Intl.Collator object. For now, we behave as if "kf" and "kn" exist, as test262 depends on it. + static Vector relevant_extension_keys { "ca"sv, "co"sv, "hc"sv, "kf"sv, "kn"sv, "nu"sv }; + return relevant_extension_keys; +} + +// Note: This is not an AO in the spec. This just serves to abstract very similar steps in ApplyOptionsToTag and the Intl.Locale constructor. +static Optional get_string_option(GlobalObject& global_object, Object const& options, PropertyName const& property, Function validator, Vector const& values = {}) +{ + auto& vm = global_object.vm(); + + auto option = get_option(global_object, &options, property, Value::Type::String, values, Empty {}); + if (vm.exception()) + return {}; + if (option.is_undefined()) + return {}; + + if (validator && !validator(option.as_string().string())) { + vm.throw_exception(global_object, ErrorType::OptionIsNotValidValue, option, property); + return {}; + } + + return option.as_string().string(); +} + +// 14.1.1 ApplyOptionsToTag ( tag, options ), https://tc39.es/ecma402/#sec-apply-options-to-tag +static Optional apply_options_to_tag(GlobalObject& global_object, StringView tag, Object const& options) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(tag) is String. + // 2. Assert: Type(options) is Object. + + // 3. 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 {}; + } + + // 4. Let language be ? GetOption(options, "language", "string", undefined, undefined). + // 5. If language is not undefined, then + // a. If language does not match the unicode_language_subtag production, throw a RangeError exception. + auto language = get_string_option(global_object, options, vm.names.language, Unicode::is_unicode_language_subtag); + if (vm.exception()) + return {}; + + // 6. Let script be ? GetOption(options, "script", "string", undefined, undefined). + // 7. If script is not undefined, then + // a. If script does not match the unicode_script_subtag production, throw a RangeError exception. + auto script = get_string_option(global_object, options, vm.names.script, Unicode::is_unicode_script_subtag); + if (vm.exception()) + return {}; + + // 8. Let region be ? GetOption(options, "region", "string", undefined, undefined). + // 9. If region is not undefined, then + // a. If region does not match the unicode_region_subtag production, throw a RangeError exception. + auto region = get_string_option(global_object, options, vm.names.region, Unicode::is_unicode_region_subtag); + if (vm.exception()) + return {}; + + // 10. Set tag to CanonicalizeUnicodeLocaleId(tag). + auto canonicalized_tag = JS::Intl::canonicalize_unicode_locale_id(*locale_id); + + // 11. Assert: tag matches the unicode_locale_id production. + locale_id = Unicode::parse_unicode_locale_id(canonicalized_tag); + VERIFY(locale_id.has_value()); + + // 12. Let languageId be the substring of tag corresponding to the unicode_language_id production. + auto& language_id = locale_id->language_id; + + // 13. If language is not undefined, then + if (language.has_value()) { + // a. Set languageId to languageId with the substring corresponding to the unicode_language_subtag production replaced by the string language. + language_id.language = language.release_value(); + } + + // 14. If script is not undefined, then + if (script.has_value()) { + // a. If languageId does not contain a unicode_script_subtag production, then + // i. Set languageId to the string-concatenation of the unicode_language_subtag production of languageId, "-", script, and the rest of languageId. + // b. Else, + // i. Set languageId to languageId with the substring corresponding to the unicode_script_subtag production replaced by the string script. + language_id.script = script.release_value(); + } + + // 15. If region is not undefined, then + if (region.has_value()) { + // a. If languageId does not contain a unicode_region_subtag production, then + // i. Set languageId to the string-concatenation of the unicode_language_subtag production of languageId, the substring corresponding to "-"` and the `unicode_script_subtag` production if present, `"-", region, and the rest of languageId. + // b. Else, + // i. Set languageId to languageId with the substring corresponding to the unicode_region_subtag production replaced by the string region. + language_id.region = region.release_value(); + } + + // 16. Set tag to tag with the substring corresponding to the unicode_language_id production replaced by the string languageId. + // 17. Return CanonicalizeUnicodeLocaleId(tag). + return JS::Intl::canonicalize_unicode_locale_id(*locale_id); +} + +// 14.1.2 ApplyUnicodeExtensionToTag ( tag, options, relevantExtensionKeys ), https://tc39.es/ecma402/#sec-apply-unicode-extension-to-tag +static LocaleAndKeys apply_unicode_extension_to_tag(StringView tag, LocaleAndKeys options, Vector const& relevant_extension_keys) +{ + // 1. Assert: Type(tag) is String. + // 2. Assert: tag matches the unicode_locale_id production. + auto locale_id = Unicode::parse_unicode_locale_id(tag); + VERIFY(locale_id.has_value()); + + Vector attributes; + Vector keywords; + + // 3. If tag contains a substring that is a Unicode locale extension sequence, then + for (auto& extension : locale_id->extensions) { + if (!extension.has()) + continue; + + // a. Let extension be the String value consisting of the substring of the Unicode locale extension sequence within tag. + // b. Let components be ! UnicodeExtensionComponents(extension). + auto& components = extension.get(); + // c. Let attributes be components.[[Attributes]]. + attributes = move(components.attributes); + // d. Let keywords be components.[[Keywords]]. + keywords = move(components.keywords); + + break; + } + // 4. Else, + // a. Let attributes be a new empty List. + // b. Let keywords be a new empty List. + + auto field_from_key = [](LocaleAndKeys& value, StringView key) -> Optional& { + if (key == "ca"sv) + return value.ca; + if (key == "co"sv) + return value.co; + if (key == "hc"sv) + return value.hc; + if (key == "kf"sv) + return value.kf; + if (key == "kn"sv) + return value.kn; + if (key == "nu"sv) + return value.nu; + VERIFY_NOT_REACHED(); + }; + + // 5. Let result be a new Record. + LocaleAndKeys result {}; + + // 6. For each element key of relevantExtensionKeys, do + for (auto const& key : relevant_extension_keys) { + // a. Let value be undefined. + Optional value {}; + + Unicode::Keyword* entry = nullptr; + // b. If keywords contains an element whose [[Key]] is the same as key, then + if (auto it = keywords.find_if([&](auto const& k) { return key == k.key; }); it != keywords.end()) { + // i. Let entry be the element of keywords whose [[Key]] is the same as key. + entry = &(*it); + + // ii. Let value be entry.[[Value]]. + StringBuilder builder; + builder.join('-', entry->types); + value = builder.build(); + } + // c. Else, + // i. Let entry be empty. + + // d. Assert: options has a field [[]]. + // e. Let optionsValue be options.[[]]. + auto options_value = field_from_key(options, key); + + // f. If optionsValue is not undefined, then + if (options_value.has_value()) { + // i. Assert: Type(optionsValue) is String. + // ii. Let value be optionsValue. + value = options_value.release_value(); + + // iii. If entry is not empty, then + if (entry != nullptr) { + // 1. Set entry.[[Value]] to value. + entry->types = value->split('-'); + } + // iv. Else, + else { + // 1. Append the Record { [[Key]]: key, [[Value]]: value } to keywords. + keywords.append({ key, value->split('-') }); + } + } + + // g. Set result.[[]] to value. + field_from_key(result, key) = move(value); + } + + // 7. Let locale be the String value that is tag with any Unicode locale extension sequences removed. + locale_id->remove_extension_type(); + auto locale = locale_id->to_string(); + + // 8. Let newExtension be a Unicode BCP 47 U Extension based on attributes and keywords. + Unicode::LocaleExtension new_extension { move(attributes), move(keywords) }; + + // 9. If newExtension is not the empty String, then + if (!new_extension.attributes.is_empty() || !new_extension.keywords.is_empty()) { + // a. Let locale be ! InsertUnicodeExtensionAndCanonicalize(locale, newExtension). + locale = insert_unicode_extension_and_canonicalize(locale_id.release_value(), move(new_extension)); + } + + // 10. Set result.[[locale]] to locale. + result.locale = move(locale); + + // 11. Return result. + return result; +} + // 14.1 The Intl.Locale Constructor, https://tc39.es/ecma402/#sec-intl-locale-constructor LocaleConstructor::LocaleConstructor(GlobalObject& global_object) : NativeFunction(vm().names.Locale.as_string(), *global_object.function_prototype()) @@ -42,11 +277,143 @@ Value LocaleConstructor::construct(FunctionObject& new_target) auto& vm = this->vm(); auto& global_object = this->global_object(); + auto tag = vm.argument(0); + auto options = vm.argument(1); + + // 2. Let relevantExtensionKeys be %Locale%.[[RelevantExtensionKeys]]. + auto const& relevant_extension_keys = locale_relevant_extension_keys(); + + // 3. Let internalSlotsList be « [[InitializedLocale]], [[Locale]], [[Calendar]], [[Collation]], [[HourCycle]], [[NumberingSystem]] ». + // 4. If relevantExtensionKeys contains "kf", then + // a. Append [[CaseFirst]] as the last element of internalSlotsList. + // 5. If relevantExtensionKeys contains "kn", then + // a. Append [[Numeric]] as the last element of internalSlotsList. + // 6. Let locale be ? OrdinaryCreateFromConstructor(NewTarget, "%Locale.prototype%", internalSlotsList). auto* locale = ordinary_create_from_constructor(global_object, new_target, &GlobalObject::intl_locale_prototype); if (vm.exception()) return {}; + // 7. If Type(tag) is not String or Object, throw a TypeError exception. + if (!tag.is_string() && !tag.is_object()) { + vm.throw_exception(global_object, ErrorType::NotAnObjectOrString, "tag"sv); + return {}; + } + + // 8. If Type(tag) is Object and tag has an [[InitializedLocale]] internal slot, then + if (tag.is_object() && is(tag.as_object())) { + // a. Let tag be tag.[[Locale]]. + auto const& tag_object = static_cast(tag.as_object()); + tag = js_string(vm, tag_object.locale()); + } + // 9. Else, + else { + // a. Let tag be ? ToString(tag). + tag = tag.to_primitive_string(global_object); + if (vm.exception()) + return {}; + } + + // 10. Set options to ? CoerceOptionsToObject(options). + options = coerce_options_to_object(global_object, options); + if (vm.exception()) + return {}; + + // 11. Set tag to ? ApplyOptionsToTag(tag, options). + auto canonicalized_tag = apply_options_to_tag(global_object, tag.as_string().string(), options.as_object()); + if (vm.exception()) + return {}; + + // 12. Let opt be a new Record. + LocaleAndKeys opt {}; + + // 13. Let calendar be ? GetOption(options, "calendar", "string", undefined, undefined). + // 14. If calendar is not undefined, then + // a. If calendar does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception. + // 15. Set opt.[[ca]] to calendar. + opt.ca = get_string_option(global_object, options.as_object(), vm.names.calendar, Unicode::is_type_identifier); + if (vm.exception()) + return {}; + + // 16. Let collation be ? GetOption(options, "collation", "string", undefined, undefined). + // 17. If collation is not undefined, then + // a. If collation does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception. + // 18. Set opt.[[co]] to collation. + opt.co = get_string_option(global_object, options.as_object(), vm.names.collation, Unicode::is_type_identifier); + if (vm.exception()) + return {}; + + // 19. Let hc be ? GetOption(options, "hourCycle", "string", « "h11", "h12", "h23", "h24" », undefined). + // 20. Set opt.[[hc]] to hc. + opt.hc = get_string_option(global_object, options.as_object(), vm.names.hourCycle, nullptr, { "h11"sv, "h12"sv, "h23"sv, "h24"sv }); + if (vm.exception()) + return {}; + + // 21. Let kf be ? GetOption(options, "caseFirst", "string", « "upper", "lower", "false" », undefined). + // 22. Set opt.[[kf]] to kf. + opt.kf = get_string_option(global_object, options.as_object(), vm.names.caseFirst, nullptr, { "upper"sv, "lower"sv, "false"sv }); + if (vm.exception()) + return {}; + + // 23. Let kn be ? GetOption(options, "numeric", "boolean", undefined, undefined). + auto kn = get_option(global_object, options, vm.names.numeric, Value::Type::Boolean, {}, Empty {}); + if (vm.exception()) + return {}; + + // 24. If kn is not undefined, set kn to ! ToString(kn). + // 25. Set opt.[[kn]] to kn. + if (!kn.is_undefined()) + opt.kn = kn.to_string(global_object); + + // 26. Let numberingSystem be ? GetOption(options, "numberingSystem", "string", undefined, undefined). + // 27. If numberingSystem is not undefined, then + // a. If numberingSystem does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception. + // 28. Set opt.[[nu]] to numberingSystem. + opt.nu = get_string_option(global_object, options.as_object(), vm.names.numberingSystem, Unicode::is_type_identifier); + if (vm.exception()) + return {}; + + // 29. Let r be ! ApplyUnicodeExtensionToTag(tag, opt, relevantExtensionKeys). + auto result = apply_unicode_extension_to_tag(*canonicalized_tag, move(opt), relevant_extension_keys); + + // 30. Set locale.[[Locale]] to r.[[locale]]. + locale->set_locale(move(result.locale)); + // 31. Set locale.[[Calendar]] to r.[[ca]]. + if (result.ca.has_value()) + locale->set_calendar(result.ca.release_value()); + // 32. Set locale.[[Collation]] to r.[[co]]. + if (result.co.has_value()) + locale->set_collation(result.co.release_value()); + // 33. Set locale.[[HourCycle]] to r.[[hc]]. + if (result.hc.has_value()) + locale->set_hour_cycle(result.hc.release_value()); + + // 34. If relevantExtensionKeys contains "kf", then + if (relevant_extension_keys.contains_slow("kf"sv)) { + // a. Set locale.[[CaseFirst]] to r.[[kf]]. + if (result.kf.has_value()) + locale->set_case_first(result.kf.release_value()); + } + + // 35. If relevantExtensionKeys contains "kn", then + if (relevant_extension_keys.contains_slow("kn"sv)) { + // a. If ! SameValue(r.[[kn]], "true") is true or r.[[kn]] is the empty String, then + if (result.kn.has_value() && (same_value(js_string(vm, *result.kn), js_string(vm, "true")) || result.kn->is_empty())) { + // i. Set locale.[[Numeric]] to true. + locale->set_numeric(true); + } + // b. Else, + else { + // i. Set locale.[[Numeric]] to false. + locale->set_numeric(false); + } + } + + // 36. Set locale.[[NumberingSystem]] to r.[[nu]]. + if (result.nu.has_value()) + locale->set_numbering_system(result.nu.release_value()); + + // 37. Return locale. return locale; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/Locale/Locale.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/Locale/Locale.js index ceb74a117f..e71d896c23 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/Locale/Locale.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/Locale/Locale.js @@ -4,10 +4,168 @@ describe("errors", () => { Intl.Locale(); }).toThrowWithMessage(TypeError, "Intl.Locale constructor must be called with 'new'"); }); + + test("tag is neither object nor string", () => { + expect(() => { + new Intl.Locale(1); + }).toThrowWithMessage(TypeError, "tag is neither an object nor a string"); + }); + + test("structurally invalid tag", () => { + expect(() => { + new Intl.Locale("root"); + }).toThrowWithMessage(RangeError, "root is not a structurally valid language tag"); + + expect(() => { + new Intl.Locale("en-"); + }).toThrowWithMessage(RangeError, "en- is not a structurally valid language tag"); + + expect(() => { + new Intl.Locale("Latn"); + }).toThrowWithMessage(RangeError, "Latn is not a structurally valid language tag"); + + expect(() => { + new Intl.Locale("en-u-aa-U-aa"); + }).toThrowWithMessage(RangeError, "en-u-aa-U-aa is not a structurally valid language tag"); + }); + + test("locale option fields are not valid Unicode TR35 subtags", () => { + expect(() => { + new Intl.Locale("en", { language: "a" }); + }).toThrowWithMessage(RangeError, "a is not a valid value for option language"); + + expect(() => { + new Intl.Locale("en", { script: "a" }); + }).toThrowWithMessage(RangeError, "a is not a valid value for option script"); + + expect(() => { + new Intl.Locale("en", { region: "a" }); + }).toThrowWithMessage(RangeError, "a is not a valid value for option region"); + }); + + test("keyword option fields are not valid Unicode TR35 types", () => { + expect(() => { + new Intl.Locale("en", { calendar: "a" }); + }).toThrowWithMessage(RangeError, "a is not a valid value for option calendar"); + + expect(() => { + new Intl.Locale("en", { collation: "a" }); + }).toThrowWithMessage(RangeError, "a is not a valid value for option collation"); + + expect(() => { + new Intl.Locale("en", { hourCycle: "a" }); + }).toThrowWithMessage(RangeError, "a is not a valid value for option hourCycle"); + + expect(() => { + new Intl.Locale("en", { caseFirst: "a" }); + }).toThrowWithMessage(RangeError, "a is not a valid value for option caseFirst"); + }); }); describe("normal behavior", () => { test("length is 1", () => { expect(Intl.Locale).toHaveLength(1); }); + + test("locale option fields create subtags", () => { + const sr = new Intl.Locale("sr", { script: "Latn" }); + expect(sr.toString()).toBe("sr-Latn"); + + const en = new Intl.Locale("en", { region: "US" }); + expect(en.toString()).toBe("en-US"); + }); + + test("locale option fields replace subtags", () => { + const es = new Intl.Locale("en", { language: "es" }); + expect(es.toString()).toBe("es"); + + const sr = new Intl.Locale("sr-Latn", { script: "Cyrl" }); + expect(sr.toString()).toBe("sr-Cyrl"); + + const en = new Intl.Locale("en-US", { region: "GB" }); + expect(en.toString()).toBe("en-GB"); + }); + + test("construction from another locale", () => { + const en1 = new Intl.Locale("en", { region: "US" }); + const en2 = new Intl.Locale(en1, { script: "Latn" }); + expect(en2.toString()).toBe("en-Latn-US"); + }); + + test("locale is canonicalized", () => { + const en = new Intl.Locale("EN", { script: "lAtN", region: "us" }); + expect(en.toString()).toBe("en-Latn-US"); + }); + + test("calendar extension", () => { + const en1 = new Intl.Locale("en-u-ca-abc"); + expect(en1.toString()).toBe("en-u-ca-abc"); + + const en2 = new Intl.Locale("en", { calendar: "abc" }); + expect(en2.toString()).toBe("en-u-ca-abc"); + + const en3 = new Intl.Locale("en-u-ca-abc", { calendar: "def" }); + expect(en3.toString()).toBe("en-u-ca-def"); + }); + + test("collation extension", () => { + const en1 = new Intl.Locale("en-u-co-abc"); + expect(en1.toString()).toBe("en-u-co-abc"); + + const en2 = new Intl.Locale("en", { collation: "abc" }); + expect(en2.toString()).toBe("en-u-co-abc"); + + const en3 = new Intl.Locale("en-u-co-abc", { collation: "def" }); + expect(en3.toString()).toBe("en-u-co-def"); + }); + + test("hour-cycle extension", () => { + const en1 = new Intl.Locale("en-u-hc-h11"); + expect(en1.toString()).toBe("en-u-hc-h11"); + + const en2 = new Intl.Locale("en", { hourCycle: "h12" }); + expect(en2.toString()).toBe("en-u-hc-h12"); + + const en3 = new Intl.Locale("en-u-hc-h23", { hourCycle: "h24" }); + expect(en3.toString()).toBe("en-u-hc-h24"); + }); + + test("case-first extension", () => { + const en1 = new Intl.Locale("en-u-kf-upper"); + expect(en1.toString()).toBe("en-u-kf-upper"); + + const en2 = new Intl.Locale("en", { caseFirst: "lower" }); + expect(en2.toString()).toBe("en-u-kf-lower"); + + const en3 = new Intl.Locale("en-u-kf-upper", { caseFirst: "false" }); + expect(en3.toString()).toBe("en-u-kf-false"); + }); + + test("numeric extension", () => { + // Note: "true" values are removed from Unicode locale extensions during canonicalization. + const en1 = new Intl.Locale("en-u-kn-true"); + expect(en1.toString()).toBe("en-u-kn"); + + const en2 = new Intl.Locale("en", { numeric: false }); + expect(en2.toString()).toBe("en-u-kn-false"); + + const en3 = new Intl.Locale("en-u-kn-false", { numeric: 1 }); + expect(en3.toString()).toBe("en-u-kn"); + }); + + test("numbering-system extension", () => { + const en1 = new Intl.Locale("en-u-nu-abc"); + expect(en1.toString()).toBe("en-u-nu-abc"); + + const en2 = new Intl.Locale("en", { numberingSystem: "abc" }); + expect(en2.toString()).toBe("en-u-nu-abc"); + + const en3 = new Intl.Locale("en-u-nu-abc", { numberingSystem: "def" }); + expect(en3.toString()).toBe("en-u-nu-def"); + }); + + test("unicode extension inserted before private use extension", () => { + const en1 = new Intl.Locale("en-x-abcd", { calendar: "abc" }); + expect(en1.toString()).toBe("en-u-ca-abc-x-abcd"); + }); });