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:
Timothy Flynn 2024-06-16 15:36:34 -04:00 committed by Andreas Kling
parent 1039acca8c
commit 4598a505b1
Notes: sideshowbarker 2024-07-17 00:25:35 +09:00
7 changed files with 100 additions and 58 deletions

View File

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

View File

@ -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 {

View File

@ -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
// a. Let numeric be ! ToString(numeric).
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).

View File

@ -110,11 +110,11 @@ 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();
}
// 9. Set opt.[[ca]] to calendar.
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,11 +123,11 @@ 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();
}
// 12. Set opt.[[nu]] to numberingSystem.
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())

View File

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

View File

@ -106,11 +106,11 @@ 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();
}
// 8. Set opt.[[nu]] to numberingSystem.
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).
auto result = resolve_locale(requested_locales, opt, NumberFormat::relevant_extension_keys());
@ -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));

View File

@ -77,11 +77,11 @@ 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();
}
// 10. Set opt.[[nu]] to numberingSystem.
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));