1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-07 21:17:07 +09:00

LibJS: Implement Intl.Locale.prototype.variants

This is a normative change in the ECMA-402 spec. See:
e8c995a
This commit is contained in:
Timothy Flynn 2025-06-04 10:20:10 -04:00 committed by Tim Flynn
parent 324bd0f163
commit 128675770c
Notes: github-actions[bot] 2025-06-04 21:12:35 +00:00
7 changed files with 123 additions and 7 deletions

View file

@ -583,6 +583,7 @@ namespace JS {
P(value) \
P(valueOf) \
P(values) \
P(variants) \
P(wait) \
P(waitAsync) \
P(warn) \

View file

@ -43,6 +43,24 @@ Unicode::LocaleID const& Locale::locale_id() const
return *m_cached_locale_id;
}
// 15.5.5 GetLocaleVariants ( locale ), https://tc39.es/ecma402/#sec-getlocalevariants
Optional<String> get_locale_variants(Unicode::LocaleID const& locale)
{
// 1. Let baseName be GetLocaleBaseName(locale).
auto const& base_name = locale.language_id;
// 2. NOTE: Each subtag in baseName that is preceded by "-" is either a unicode_script_subtag, unicode_region_subtag,
// or unicode_variant_subtag, but any substring matched by unicode_variant_subtag is strictly longer than any
// prefix thereof which could also be matched by one of the other productions.
// 3. Let variants be the longest suffix of baseName that starts with a "-" followed by a substring that is matched
// by the unicode_variant_subtag Unicode locale nonterminal. If there is no such suffix, return undefined.
// 4. Return the substring of variants from 1.
if (base_name.variants.is_empty())
return {};
return MUST(String::join("-"sv, base_name.variants));
}
// 1.1.1 CreateArrayFromListOrRestricted ( list , restricted )
static GC::Ref<Array> create_array_from_list_or_restricted(VM& vm, Vector<String> list, Optional<String> restricted)
{

View file

@ -91,6 +91,8 @@ struct WeekInfo {
Vector<u8> weekend; // [[Weekend]]
};
Optional<String> get_locale_variants(Unicode::LocaleID const&);
GC::Ref<Array> calendars_of_locale(VM&, Locale const&);
GC::Ref<Array> collations_of_locale(VM&, Locale const& locale);
GC::Ref<Array> hour_cycles_of_locale(VM&, Locale const& locale);

View file

@ -66,25 +66,61 @@ static ThrowCompletionOr<String> update_language_id(VM& vm, StringView tag, Obje
// a. If region cannot be matched by the unicode_region_subtag Unicode locale nonterminal, throw a RangeError exception.
auto region = TRY(get_string_option(vm, options, vm.names.region, Unicode::is_unicode_region_subtag, {}, base_name.region));
// 8. Let allExtensions be the suffix of tag following baseName.
// 8. Let variants be ? GetOption(options, "variants", STRING, EMPTY, GetLocaleVariants(baseName)).
auto variants = TRY(get_string_option(vm, options, vm.names.variants, nullptr, {}, get_locale_variants(*locale_id)));
Vector<String> variant_subtags;
// 9. If variants is not undefined, then
if (variants.has_value()) {
// a. If variants is the empty String, throw a RangeError exception.
if (variants->is_empty())
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, *variants, vm.names.variants);
// b. Let lowerVariants be the ASCII-lowercase of variants.
auto lower_variants = variants->to_ascii_lowercase();
// c. Let variantSubtags be StringSplitToList(lowerVariants, "-").
variant_subtags = MUST(lower_variants.split('-', SplitBehavior::KeepEmpty));
HashTable<String> seen_variants;
bool has_duplicate_variant = false;
// d. For each element variant of variantSubtags, do
for (auto const& variant : variant_subtags) {
has_duplicate_variant |= seen_variants.set(variant) != HashSetResult::InsertedNewEntry;
// i. If variant cannot be matched by the unicode_variant_subtag Unicode locale nonterminal, throw a RangeError exception.
if (!Unicode::is_unicode_variant_subtag(variant))
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, *variants, vm.names.variants);
}
// e. If variantSubtags contains any duplicate elements, throw a RangeError exception.
if (has_duplicate_variant)
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, *variants, vm.names.variants);
}
// 10. Let allExtensions be the suffix of tag following baseName.
auto& extensions = locale_id->extensions;
auto& private_use_extensions = locale_id->private_use_extensions;
// 9. Let newTag be language.
// 11. Let newTag be language.
Unicode::LocaleID new_tag;
new_tag.language_id.language = move(language);
// 10. If script is not undefined, set newTag to the string-concatenation of newTag, "-", and script.
// 12. If script is not undefined, set newTag to the string-concatenation of newTag, "-", and script.
new_tag.language_id.script = move(script);
// 11. If region is not undefined, set newTag to the string-concatenation of newTag, "-", and region.
// 13. If region is not undefined, set newTag to the string-concatenation of newTag, "-", and region.
new_tag.language_id.region = move(region);
// 12. Set newTag to the string-concatenation of newTag and allExtensions.
// 14. If variants is not undefined, set newTag to the string-concatenation of newTag, "-", and variants.
new_tag.language_id.variants = move(variant_subtags);
// 15. Set newTag to the string-concatenation of newTag and allExtensions.
new_tag.extensions = move(extensions);
new_tag.private_use_extensions = move(private_use_extensions);
// 13. Return newTag.
// 16. Return newTag.
return new_tag.to_string();
}

View file

@ -39,7 +39,7 @@ void LocalePrototype::initialize(Realm& realm)
define_native_function(realm, vm.names.getTextInfo, get_text_info, 0, attr);
define_native_function(realm, vm.names.getWeekInfo, get_week_info, 0, attr);
// 15.3.15 Intl.Locale.prototype [ %Symbol.toStringTag% ], https://tc39.es/ecma402/#sec-intl.locale.prototype-%symbol.tostringtag%
// 15.3.16 Intl.Locale.prototype [ %Symbol.toStringTag% ], https://tc39.es/ecma402/#sec-intl.locale.prototype-%symbol.tostringtag%
define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "Intl.Locale"_string), Attribute::Configurable);
define_native_accessor(realm, vm.names.baseName, base_name, {}, Attribute::Configurable);
@ -53,6 +53,7 @@ void LocalePrototype::initialize(Realm& realm)
define_native_accessor(realm, vm.names.numeric, numeric, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.region, region, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.script, script, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.variants, variants, {}, Attribute::Configurable);
}
// 15.3.2 get Intl.Locale.prototype.baseName, https://tc39.es/ecma402/#sec-Intl.Locale.prototype.baseName
@ -182,6 +183,19 @@ JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::to_string)
return PrimitiveString::create(vm, locale_object->locale());
}
// 15.3.15 get Intl.Locale.prototype.variants, https://tc39.es/ecma402/#sec-Intl.Locale.prototype.variants
JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::variants)
{
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
auto locale_object = TRY(typed_this_object(vm));
// 3. Return GetLocaleVariants(loc.[[Locale]]).
if (auto variants = get_locale_variants(locale_object->locale_id()); variants.has_value())
return PrimitiveString::create(vm, variants.release_value());
return js_undefined();
}
#define JS_ENUMERATE_LOCALE_INFO_PROPERTIES \
__JS_ENUMERATE(calendars) \
__JS_ENUMERATE(collations) \

View file

@ -37,6 +37,7 @@ private:
JS_DECLARE_NATIVE_FUNCTION(numeric);
JS_DECLARE_NATIVE_FUNCTION(region);
JS_DECLARE_NATIVE_FUNCTION(script);
JS_DECLARE_NATIVE_FUNCTION(variants);
JS_DECLARE_NATIVE_FUNCTION(get_calendars);
JS_DECLARE_NATIVE_FUNCTION(get_collations);
JS_DECLARE_NATIVE_FUNCTION(get_hour_cycles);

View file

@ -0,0 +1,44 @@
describe("errors", () => {
test("called on non-Locale object", () => {
expect(() => {
Intl.Locale.prototype.variants;
}).toThrowWithMessage(TypeError, "Not an object of type Intl.Locale");
});
test("duplicate variants", () => {
expect(() => {
new Intl.Locale("en-abcde-abcde");
}).toThrowWithMessage(
RangeError,
"en-abcde-abcde is not a structurally valid language tag"
);
expect(() => {
new Intl.Locale("en", { variants: "abcde-abcde" });
}).toThrowWithMessage(RangeError, "abcde-abcde is not a valid value for option variants");
});
test("invalid variant", () => {
expect(() => {
new Intl.Locale("en-a");
}).toThrowWithMessage(RangeError, "en-a is not a structurally valid language tag");
expect(() => {
new Intl.Locale("en", { variants: "a" });
}).toThrowWithMessage(RangeError, "a is not a valid value for option variants");
expect(() => {
new Intl.Locale("en", { variants: "-" });
}).toThrowWithMessage(RangeError, "- is not a valid value for option variants");
});
});
describe("normal behavior", () => {
test("basic functionality", () => {
expect(new Intl.Locale("en").variants).toBeUndefined();
expect(new Intl.Locale("en-abcde").variants).toBe("abcde");
expect(new Intl.Locale("en-1234-abcde").variants).toBe("1234-abcde");
expect(new Intl.Locale("en", { variants: "abcde" }).variants).toBe("abcde");
expect(new Intl.Locale("en", { variants: "1234-abcde" }).variants).toBe("1234-abcde");
});
});