1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 22:57:44 +00:00

LibJS: Implement the Intl.Locale constructor

This commit is contained in:
Timothy Flynn 2021-09-01 22:08:15 -04:00 committed by Linus Groh
parent 990dd037d2
commit 17639a42ae
5 changed files with 564 additions and 2 deletions

View file

@ -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) \

View file

@ -17,7 +17,7 @@
namespace JS::Intl {
// 6.2.2 IsStructurallyValidLanguageTag ( locale ), https://tc39.es/ecma402/#sec-isstructurallyvalidlanguagetag
static Optional<Unicode::LocaleID> is_structurally_valid_language_tag(StringView locale)
Optional<Unicode::LocaleID> is_structurally_valid_language_tag(StringView locale)
{
auto contains_duplicate_variant = [](auto& variants) {
if (variants.is_empty())
@ -77,7 +77,7 @@ static Optional<Unicode::LocaleID> 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<String> 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<String> const& requested_locales, LocaleOptions const& options, [[maybe_unused]] Vector<StringView> relevant_extension_keys)
{
@ -385,6 +396,19 @@ LocaleResult resolve_locale(Vector<String> 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<StringView> const& values, Fallback fallback)
{

View file

@ -12,6 +12,7 @@
#include <LibJS/Forward.h>
#include <LibJS/Runtime/Intl/DisplayNames.h>
#include <LibJS/Runtime/Value.h>
#include <LibUnicode/Forward.h>
namespace JS::Intl {
@ -25,8 +26,12 @@ struct LocaleResult {
String locale;
};
Optional<Unicode::LocaleID> is_structurally_valid_language_tag(StringView locale);
String canonicalize_unicode_locale_id(Unicode::LocaleID& locale);
Vector<String> 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<StringView> const& values, Fallback fallback);
String insert_unicode_extension_and_canonicalize(Unicode::LocaleID locale_id, Unicode::LocaleExtension extension);
LocaleResult resolve_locale(Vector<String> const& requested_locales, LocaleOptions const& options, Vector<StringView> relevant_extension_keys);
Value canonical_code_for_display_names(GlobalObject&, DisplayNames::Type type, StringView code);

View file

@ -4,13 +4,248 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/StringBuilder.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Intl/Locale.h>
#include <LibJS/Runtime/Intl/LocaleConstructor.h>
#include <LibUnicode/Locale.h>
namespace JS::Intl {
struct LocaleAndKeys {
String locale;
Optional<String> ca;
Optional<String> co;
Optional<String> hc;
Optional<String> kf;
Optional<String> kn;
Optional<String> nu;
};
static Vector<StringView> 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<StringView> 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<String> get_string_option(GlobalObject& global_object, Object const& options, PropertyName const& property, Function<bool(StringView)> validator, Vector<StringView> 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<RangeError>(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<String> 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<RangeError>(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<StringView> 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<String> attributes;
Vector<Unicode::Keyword> keywords;
// 3. If tag contains a substring that is a Unicode locale extension sequence, then
for (auto& extension : locale_id->extensions) {
if (!extension.has<Unicode::LocaleExtension>())
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<Unicode::LocaleExtension>();
// 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<String>& {
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<String> 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 [[<key>]].
// e. Let optionsValue be options.[[<key>]].
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.[[<key>]] 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<Unicode::LocaleExtension>();
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<Locale>(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<TypeError>(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<Locale>(tag.as_object())) {
// a. Let tag be tag.[[Locale]].
auto const& tag_object = static_cast<Locale const&>(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;
}

View file

@ -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");
});
});