mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-31 21:29:06 +00:00
LibJS: Differentiate between undefined and null locale keys
We were previously treating undefined and null as the same (an empty Optional). However, there are edge cases in ECMA-402 where we must treat them differently. Namely, the hour cycle (hc) keyword. An undefined hc value has no effect on the resolved locale, whereas a null hc value can actively override any hc specified in the locale string. For example: new Intl.DateTimeFormat("en-u-hc-h11", { hour12: false }); In that object, the hour12 option does not match the u-hc-h11 value. So the spec dictates we remove the hc value by setting it to null.
This commit is contained in:
parent
1039acca8c
commit
4598a505b1
Notes:
sideshowbarker
2024-07-17 00:25:35 +09:00
Author: https://github.com/trflynn89
Commit: 4598a505b1
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/180
7 changed files with 100 additions and 58 deletions
|
@ -21,6 +21,17 @@
|
|||
|
||||
namespace JS::Intl {
|
||||
|
||||
Optional<LocaleKey> locale_key_from_value(Value value)
|
||||
{
|
||||
if (value.is_undefined())
|
||||
return OptionalNone {};
|
||||
if (value.is_null())
|
||||
return Empty {};
|
||||
if (value.is_string())
|
||||
return value.as_string().utf8_string();
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// 6.2.2 IsStructurallyValidLanguageTag ( locale ), https://tc39.es/ecma402/#sec-isstructurallyvalidlanguagetag
|
||||
bool is_structurally_valid_language_tag(StringView locale)
|
||||
{
|
||||
|
@ -366,9 +377,30 @@ static auto& find_key_in_value(T& value, StringView key)
|
|||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
static Vector<LocaleKey> available_keyword_values(StringView locale, StringView key)
|
||||
{
|
||||
auto key_locale_data = ::Locale::available_keyword_values(locale, key);
|
||||
|
||||
Vector<LocaleKey> result;
|
||||
result.ensure_capacity(key_locale_data.size());
|
||||
|
||||
for (auto& keyword : key_locale_data)
|
||||
result.unchecked_append(move(keyword));
|
||||
|
||||
if (key == "hc"sv) {
|
||||
// https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots
|
||||
// [[LocaleData]].[[<locale>]].[[hc]] must be « null, "h11", "h12", "h23", "h24" ».
|
||||
result.prepend(Empty {});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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, ReadonlySpan<StringView> relevant_extension_keys)
|
||||
{
|
||||
static auto true_string = "true"_string;
|
||||
|
||||
// 1. Let matcher be options.[[localeMatcher]].
|
||||
auto const& matcher = options.locale_matcher;
|
||||
MatcherResult matcher_result;
|
||||
|
@ -416,7 +448,7 @@ LocaleResult resolve_locale(Vector<String> const& requested_locales, LocaleOptio
|
|||
// b. Assert: Type(foundLocaleData) is Record.
|
||||
// c. Let keyLocaleData be foundLocaleData.[[<key>]].
|
||||
// d. Assert: Type(keyLocaleData) is List.
|
||||
auto key_locale_data = ::Locale::available_keyword_values(found_locale, key);
|
||||
auto key_locale_data = available_keyword_values(found_locale, key);
|
||||
|
||||
// e. Let value be keyLocaleData[0].
|
||||
// f. Assert: Type(value) is either String or Null.
|
||||
|
@ -447,9 +479,9 @@ LocaleResult resolve_locale(Vector<String> const& requested_locales, LocaleOptio
|
|||
}
|
||||
}
|
||||
// 4. Else if keyLocaleData contains "true", then
|
||||
else if (key_locale_data.contains_slow("true"sv)) {
|
||||
else if (key_locale_data.contains_slow(true_string)) {
|
||||
// a. Let value be "true".
|
||||
value = "true"_string;
|
||||
value = true_string;
|
||||
|
||||
// b. Let supportedExtensionAddition be the string-concatenation of "-" and key.
|
||||
supported_extension_addition = ::Locale::Keyword { MUST(String::from_utf8(key)), {} };
|
||||
|
@ -464,15 +496,15 @@ LocaleResult resolve_locale(Vector<String> const& requested_locales, LocaleOptio
|
|||
auto options_value = find_key_in_value(options, key);
|
||||
|
||||
// iii. If Type(optionsValue) is String, then
|
||||
if (options_value.has_value()) {
|
||||
if (auto* options_string = options_value.has_value() ? options_value->get_pointer<String>() : nullptr) {
|
||||
// 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.
|
||||
::Locale::canonicalize_unicode_extension_values(key, *options_value);
|
||||
::Locale::canonicalize_unicode_extension_values(key, *options_string);
|
||||
|
||||
// 3. If optionsValue is the empty String, then
|
||||
if (options_value->is_empty()) {
|
||||
if (options_string->is_empty()) {
|
||||
// a. Let optionsValue be "true".
|
||||
options_value = "true"_string;
|
||||
*options_string = true_string;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,25 +21,28 @@
|
|||
|
||||
namespace JS::Intl {
|
||||
|
||||
using LocaleKey = Variant<Empty, String>;
|
||||
Optional<LocaleKey> locale_key_from_value(Value);
|
||||
|
||||
struct LocaleOptions {
|
||||
Value locale_matcher;
|
||||
Optional<String> ca; // [[Calendar]]
|
||||
Optional<String> co; // [[Collation]]
|
||||
Optional<String> hc; // [[HourCycle]]
|
||||
Optional<String> kf; // [[CaseFirst]]
|
||||
Optional<String> kn; // [[Numeric]]
|
||||
Optional<String> nu; // [[NumberingSystem]]
|
||||
Optional<LocaleKey> ca; // [[Calendar]]
|
||||
Optional<LocaleKey> co; // [[Collation]]
|
||||
Optional<LocaleKey> hc; // [[HourCycle]]
|
||||
Optional<LocaleKey> kf; // [[CaseFirst]]
|
||||
Optional<LocaleKey> kn; // [[Numeric]]
|
||||
Optional<LocaleKey> nu; // [[NumberingSystem]]
|
||||
};
|
||||
|
||||
struct LocaleResult {
|
||||
String locale;
|
||||
String data_locale;
|
||||
Optional<String> ca; // [[Calendar]]
|
||||
Optional<String> co; // [[Collation]]
|
||||
Optional<String> hc; // [[HourCycle]]
|
||||
Optional<String> kf; // [[CaseFirst]]
|
||||
Optional<String> kn; // [[Numeric]]
|
||||
Optional<String> nu; // [[NumberingSystem]]
|
||||
LocaleKey ca; // [[Calendar]]
|
||||
LocaleKey co; // [[Collation]]
|
||||
LocaleKey hc; // [[HourCycle]]
|
||||
LocaleKey kf; // [[CaseFirst]]
|
||||
LocaleKey kn; // [[Numeric]]
|
||||
LocaleKey nu; // [[NumberingSystem]]
|
||||
};
|
||||
|
||||
struct PatternPartition {
|
||||
|
|
|
@ -62,16 +62,19 @@ static ThrowCompletionOr<NonnullGCPtr<Collator>> initialize_collator(VM& vm, Col
|
|||
auto numeric = TRY(get_option(vm, *options, vm.names.numeric, OptionType::Boolean, {}, Empty {}));
|
||||
|
||||
// 14. If numeric is not undefined, then
|
||||
if (!numeric.is_undefined()) {
|
||||
// a. Let numeric be ! ToString(numeric).
|
||||
numeric = PrimitiveString::create(vm, MUST(numeric.to_string(vm)));
|
||||
}
|
||||
|
||||
// 15. Set opt.[[kn]] to numeric.
|
||||
if (!numeric.is_undefined())
|
||||
opt.kn = MUST(numeric.to_string(vm));
|
||||
opt.kn = locale_key_from_value(numeric);
|
||||
|
||||
// 16. Let caseFirst be ? GetOption(options, "caseFirst", string, « "upper", "lower", "false" », undefined).
|
||||
// 17. Set opt.[[kf]] to caseFirst.
|
||||
auto case_first = TRY(get_option(vm, *options, vm.names.caseFirst, OptionType::String, { "upper"sv, "lower"sv, "false"sv }, Empty {}));
|
||||
if (!case_first.is_undefined())
|
||||
opt.kf = case_first.as_string().utf8_string();
|
||||
|
||||
// 17. Set opt.[[kf]] to caseFirst.
|
||||
opt.kf = locale_key_from_value(case_first);
|
||||
|
||||
// 18. Let relevantExtensionKeys be %Collator%.[[RelevantExtensionKeys]].
|
||||
auto relevant_extension_keys = Collator::relevant_extension_keys();
|
||||
|
@ -83,20 +86,26 @@ static ThrowCompletionOr<NonnullGCPtr<Collator>> initialize_collator(VM& vm, Col
|
|||
collator.set_locale(move(result.locale));
|
||||
|
||||
// 21. Let collation be r.[[co]].
|
||||
auto& collation_value = result.co;
|
||||
|
||||
// 22. If collation is null, let collation be "default".
|
||||
if (collation_value.has<Empty>())
|
||||
collation_value = "default"_string;
|
||||
|
||||
// 23. Set collator.[[Collation]] to collation.
|
||||
collator.set_collation(result.co.has_value() ? result.co.release_value() : "default"_string);
|
||||
collator.set_collation(move(collation_value.get<String>()));
|
||||
|
||||
// 24. If relevantExtensionKeys contains "kn", then
|
||||
if (relevant_extension_keys.span().contains_slow("kn"sv) && result.kn.has_value()) {
|
||||
if (relevant_extension_keys.span().contains_slow("kn"sv)) {
|
||||
// a. Set collator.[[Numeric]] to SameValue(r.[[kn]], "true").
|
||||
collator.set_numeric(same_value(PrimitiveString::create(vm, result.kn.release_value()), PrimitiveString::create(vm, "true"_string)));
|
||||
collator.set_numeric(result.kn == "true"_string);
|
||||
}
|
||||
|
||||
// 25. If relevantExtensionKeys contains "kf", then
|
||||
if (relevant_extension_keys.span().contains_slow("kf"sv) && result.kf.has_value()) {
|
||||
if (relevant_extension_keys.span().contains_slow("kf"sv)) {
|
||||
// a. Set collator.[[CaseFirst]] to r.[[kf]].
|
||||
collator.set_case_first(result.kf.release_value());
|
||||
if (auto* resolved_case_first = result.kf.get_pointer<String>())
|
||||
collator.set_case_first(*resolved_case_first);
|
||||
}
|
||||
|
||||
// 26. Let sensitivity be ? GetOption(options, "sensitivity", string, « "base", "accent", "case", "variant" », undefined).
|
||||
|
|
|
@ -110,10 +110,10 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
|
|||
// a. If calendar cannot be matched by the type Unicode locale nonterminal, throw a RangeError exception.
|
||||
if (!::Locale::is_type_identifier(calendar.as_string().utf8_string_view()))
|
||||
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, calendar, "calendar"sv);
|
||||
}
|
||||
|
||||
// 9. Set opt.[[ca]] to calendar.
|
||||
opt.ca = calendar.as_string().utf8_string();
|
||||
}
|
||||
opt.ca = locale_key_from_value(calendar);
|
||||
|
||||
// 10. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined).
|
||||
auto numbering_system = TRY(get_option(vm, *options, vm.names.numberingSystem, OptionType::String, {}, Empty {}));
|
||||
|
@ -123,10 +123,10 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
|
|||
// a. If numberingSystem cannot be matched by the type Unicode locale nonterminal, throw a RangeError exception.
|
||||
if (!::Locale::is_type_identifier(numbering_system.as_string().utf8_string_view()))
|
||||
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, numbering_system, "numberingSystem"sv);
|
||||
}
|
||||
|
||||
// 12. Set opt.[[nu]] to numberingSystem.
|
||||
opt.nu = numbering_system.as_string().utf8_string();
|
||||
}
|
||||
opt.nu = locale_key_from_value(numbering_system);
|
||||
|
||||
// 13. Let hour12 be ? GetOption(options, "hour12", boolean, empty, undefined).
|
||||
auto hour12 = TRY(get_option(vm, *options, vm.names.hour12, OptionType::Boolean, {}, Empty {}));
|
||||
|
@ -141,8 +141,7 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
|
|||
}
|
||||
|
||||
// 16. Set opt.[[hc]] to hourCycle.
|
||||
if (!hour_cycle.is_nullish())
|
||||
opt.hc = hour_cycle.as_string().utf8_string();
|
||||
opt.hc = locale_key_from_value(hour_cycle);
|
||||
|
||||
// 17. Let localeData be %DateTimeFormat%.[[LocaleData]].
|
||||
// 18. Let r be ResolveLocale(%DateTimeFormat%.[[AvailableLocales]], requestedLocales, opt, %DateTimeFormat%.[[RelevantExtensionKeys]], localeData).
|
||||
|
@ -153,12 +152,12 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
|
|||
|
||||
// 20. Let resolvedCalendar be r.[[ca]].
|
||||
// 21. Set dateTimeFormat.[[Calendar]] to resolvedCalendar.
|
||||
if (result.ca.has_value())
|
||||
date_time_format->set_calendar(result.ca.release_value());
|
||||
if (auto* resolved_calendar = result.ca.get_pointer<String>())
|
||||
date_time_format->set_calendar(move(*resolved_calendar));
|
||||
|
||||
// 22. Set dateTimeFormat.[[NumberingSystem]] to r.[[nu]].
|
||||
if (result.nu.has_value())
|
||||
date_time_format->set_numbering_system(result.nu.release_value());
|
||||
if (auto* resolved_numbering_system = result.nu.get_pointer<String>())
|
||||
date_time_format->set_numbering_system(move(*resolved_numbering_system));
|
||||
|
||||
// 23. Let dataLocale be r.[[dataLocale]].
|
||||
auto data_locale = move(result.data_locale);
|
||||
|
@ -184,8 +183,8 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
|
|||
VERIFY(hour12.is_undefined());
|
||||
|
||||
// b. Let hc be r.[[hc]].
|
||||
if (result.hc.has_value())
|
||||
hour_cycle_value = ::Locale::hour_cycle_from_string(*result.hc);
|
||||
if (auto* resolved_hour_cycle = result.hc.get_pointer<String>())
|
||||
hour_cycle_value = ::Locale::hour_cycle_from_string(*resolved_hour_cycle);
|
||||
|
||||
// c. If hc is null, set hc to dataLocaleData.[[hourCycle]].
|
||||
if (!hour_cycle_value.has_value())
|
||||
|
|
|
@ -77,8 +77,7 @@ ThrowCompletionOr<NonnullGCPtr<Object>> DurationFormatConstructor::construct(Fun
|
|||
// 8. Let opt be the Record { [[localeMatcher]]: matcher, [[nu]]: numberingSystem }.
|
||||
LocaleOptions opt {};
|
||||
opt.locale_matcher = matcher;
|
||||
if (!numbering_system.is_undefined())
|
||||
opt.nu = numbering_system.as_string().utf8_string();
|
||||
opt.nu = locale_key_from_value(numbering_system);
|
||||
|
||||
// 9. Let r be ResolveLocale(%DurationFormat%.[[AvailableLocales]], requestedLocales, opt, %DurationFormat%.[[RelevantExtensionKeys]], %DurationFormat%.[[LocaleData]]).
|
||||
auto result = resolve_locale(requested_locales, opt, DurationFormat::relevant_extension_keys());
|
||||
|
@ -110,8 +109,8 @@ ThrowCompletionOr<NonnullGCPtr<Object>> DurationFormatConstructor::construct(Fun
|
|||
duration_format->set_minutes_seconds_separator(move(digital_format.minutes_seconds_separator));
|
||||
|
||||
// 22. Set durationFormat.[[NumberingSystem]] to r.[[nu]].
|
||||
if (result.nu.has_value())
|
||||
duration_format->set_numbering_system(result.nu.release_value());
|
||||
if (auto* resolved_numbering_system = result.nu.get_pointer<String>())
|
||||
duration_format->set_numbering_system(move(*resolved_numbering_system));
|
||||
|
||||
// 23. Let style be ? GetOption(options, "style", string, « "long", "short", "narrow", "digital" », "short").
|
||||
auto style = TRY(get_option(vm, *options, vm.names.style, OptionType::String, { "long"sv, "short"sv, "narrow"sv, "digital"sv }, "short"sv));
|
||||
|
|
|
@ -106,10 +106,10 @@ ThrowCompletionOr<NonnullGCPtr<NumberFormat>> initialize_number_format(VM& vm, N
|
|||
// a. If numberingSystem does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
|
||||
if (!::Locale::is_type_identifier(numbering_system.as_string().utf8_string_view()))
|
||||
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, numbering_system, "numberingSystem"sv);
|
||||
}
|
||||
|
||||
// 8. Set opt.[[nu]] to numberingSystem.
|
||||
opt.nu = numbering_system.as_string().utf8_string();
|
||||
}
|
||||
opt.nu = locale_key_from_value(numbering_system);
|
||||
|
||||
// 9. Let localeData be %NumberFormat%.[[LocaleData]].
|
||||
// 10. Let r be ResolveLocale(%NumberFormat%.[[AvailableLocales]], requestedLocales, opt, %NumberFormat%.[[RelevantExtensionKeys]], localeData).
|
||||
|
@ -122,8 +122,8 @@ ThrowCompletionOr<NonnullGCPtr<NumberFormat>> initialize_number_format(VM& vm, N
|
|||
number_format.set_data_locale(move(result.data_locale));
|
||||
|
||||
// 13. Set numberFormat.[[NumberingSystem]] to r.[[nu]].
|
||||
if (result.nu.has_value())
|
||||
number_format.set_numbering_system(result.nu.release_value());
|
||||
if (auto* resolved_numbering_system = result.nu.get_pointer<String>())
|
||||
number_format.set_numbering_system(move(*resolved_numbering_system));
|
||||
|
||||
// 14. Perform ? SetNumberFormatUnitOptions(numberFormat, options).
|
||||
TRY(set_number_format_unit_options(vm, number_format, *options));
|
||||
|
|
|
@ -77,10 +77,10 @@ ThrowCompletionOr<NonnullGCPtr<Object>> RelativeTimeFormatConstructor::construct
|
|||
// a. If numberingSystem cannot be matched by the type Unicode locale nonterminal, throw a RangeError exception.
|
||||
if (!::Locale::is_type_identifier(numbering_system.as_string().utf8_string_view()))
|
||||
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, numbering_system, "numberingSystem"sv);
|
||||
}
|
||||
|
||||
// 10. Set opt.[[nu]] to numberingSystem.
|
||||
opt.nu = numbering_system.as_string().utf8_string();
|
||||
}
|
||||
opt.nu = locale_key_from_value(numbering_system);
|
||||
|
||||
// 11. Let r be ResolveLocale(%Intl.RelativeTimeFormat%.[[AvailableLocales]], requestedLocales, opt, %Intl.RelativeTimeFormat%.[[RelevantExtensionKeys]], %Intl.RelativeTimeFormat%.[[LocaleData]]).
|
||||
auto result = resolve_locale(requested_locales, opt, RelativeTimeFormat::relevant_extension_keys());
|
||||
|
@ -95,8 +95,8 @@ ThrowCompletionOr<NonnullGCPtr<Object>> RelativeTimeFormatConstructor::construct
|
|||
relative_time_format->set_data_locale(move(result.data_locale));
|
||||
|
||||
// 15. Set relativeTimeFormat.[[NumberingSystem]] to r.[[nu]].
|
||||
if (result.nu.has_value())
|
||||
relative_time_format->set_numbering_system(result.nu.release_value());
|
||||
if (auto* resolved_numbering_system = result.nu.get_pointer<String>())
|
||||
relative_time_format->set_numbering_system(move(*resolved_numbering_system));
|
||||
|
||||
// 16. Let style be ? GetOption(options, "style", string, « "long", "short", "narrow" », "long").
|
||||
auto style = TRY(get_option(vm, *options, vm.names.style, OptionType::String, { "long"sv, "short"sv, "narrow"sv }, "long"sv));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue