LibJS: Ensure relevant extension keys are included in ICU locale data

This is a normative change in the ECMA-402 spec. See:
https://github.com/tc39/ecma402/commit/7508197

In our implementation, we don't have the affected AOs directly, as we
delegate to ICU. So instead, we must ensure we provide ICU a locale with
the relevant extension keys present.
This commit is contained in:
Timothy Flynn 2025-03-17 16:24:09 -04:00 committed by Tim Flynn
parent 37b8ba96f1
commit 00d00b84d3
Notes: github-actions[bot] 2025-03-18 15:48:22 +00:00
17 changed files with 82 additions and 67 deletions

View file

@ -500,6 +500,8 @@ ResolvedLocale resolve_locale(ReadonlySpan<String> requested_locales, LocaleOpti
// 12. Let supportedKeywords be a new empty List.
Vector<Unicode::Keyword> supported_keywords;
Vector<Unicode::Keyword> icu_keywords;
// 13. For each element key of relevantExtensionKeys, do
for (auto const& key : relevant_extension_keys) {
// a. Let keyLocaleData be foundLocaleData.[[<key>]].
@ -574,10 +576,23 @@ ResolvedLocale resolve_locale(ReadonlySpan<String> requested_locales, LocaleOpti
if (supported_keyword.has_value())
supported_keywords.append(supported_keyword.release_value());
if (auto* value_string = value.get_pointer<String>())
icu_keywords.empend(MUST(String::from_utf8(key)), *value_string);
// m. Set result.[[<key>]] to value.
find_key_in_value(result, key) = move(value);
}
// AD-HOC: For ICU, we need to form a locale with all relevant extension keys present.
if (icu_keywords.is_empty()) {
result.icu_locale = found_locale;
} else {
auto locale_id = Unicode::parse_unicode_locale_id(found_locale);
VERIFY(locale_id.has_value());
result.icu_locale = insert_unicode_extension_and_canonicalize(locale_id.release_value(), {}, move(icu_keywords));
}
// 14. If supportedKeywords is not empty, then
if (!supported_keywords.is_empty()) {
auto locale_id = Unicode::parse_unicode_locale_id(found_locale);

View file

@ -38,6 +38,7 @@ struct MatchedLocale {
struct ResolvedLocale {
String locale;
String icu_locale;
LocaleKey ca; // [[Calendar]]
LocaleKey co; // [[Collation]]
LocaleKey hc; // [[HourCycle]]

View file

@ -47,32 +47,32 @@ static Optional<Unicode::DateTimeFormat const&> get_or_create_formatter(StringVi
Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_date_formatter()
{
return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_date_formatter, m_temporal_plain_date_format);
return get_or_create_formatter(m_icu_locale, m_temporal_time_zone, m_temporal_plain_date_formatter, m_temporal_plain_date_format);
}
Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_year_month_formatter()
{
return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_year_month_formatter, m_temporal_plain_year_month_format);
return get_or_create_formatter(m_icu_locale, m_temporal_time_zone, m_temporal_plain_year_month_formatter, m_temporal_plain_year_month_format);
}
Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_month_day_formatter()
{
return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_month_day_formatter, m_temporal_plain_month_day_format);
return get_or_create_formatter(m_icu_locale, m_temporal_time_zone, m_temporal_plain_month_day_formatter, m_temporal_plain_month_day_format);
}
Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_time_formatter()
{
return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_time_formatter, m_temporal_plain_time_format);
return get_or_create_formatter(m_icu_locale, m_temporal_time_zone, m_temporal_plain_time_formatter, m_temporal_plain_time_format);
}
Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_date_time_formatter()
{
return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_date_time_formatter, m_temporal_plain_date_time_format);
return get_or_create_formatter(m_icu_locale, m_temporal_time_zone, m_temporal_plain_date_time_formatter, m_temporal_plain_date_time_format);
}
Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_instant_formatter()
{
return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_instant_formatter, m_temporal_instant_format);
return get_or_create_formatter(m_icu_locale, m_temporal_time_zone, m_temporal_instant_formatter, m_temporal_instant_format);
}
// 11.5.5 FormatDateTimePattern ( dateTimeFormat, patternParts, x, rangeFormatOptions ), https://tc39.es/ecma402/#sec-formatdatetimepattern

View file

@ -38,6 +38,9 @@ public:
String const& locale() const { return m_locale; }
void set_locale(String locale) { m_locale = move(locale); }
String const& icu_locale() const { return m_icu_locale; }
void set_icu_locale(String icu_locale) { m_icu_locale = move(icu_locale); }
String const& calendar() const { return m_calendar; }
void set_calendar(String calendar) { m_calendar = move(calendar); }
@ -107,6 +110,7 @@ private:
GC::Ptr<NativeFunction> m_bound_format; // [[BoundFormat]]
// Non-standard. Stores the ICU date-time formatters for the Intl object's formatting options.
String m_icu_locale;
OwnPtr<Unicode::DateTimeFormat> m_formatter;
OwnPtr<Unicode::DateTimeFormat> m_temporal_plain_date_formatter;
OwnPtr<Unicode::DateTimeFormat> m_temporal_plain_year_month_formatter;

View file

@ -149,6 +149,7 @@ ThrowCompletionOr<GC::Ref<DateTimeFormat>> create_date_time_format(VM& vm, Funct
// 18. Set dateTimeFormat.[[Locale]] to r.[[Locale]].
date_time_format->set_locale(move(result.locale));
date_time_format->set_icu_locale(move(result.icu_locale));
// 19. Let resolvedCalendar be r.[[ca]].
// 20. Set dateTimeFormat.[[Calendar]] to resolvedCalendar.
@ -355,7 +356,7 @@ ThrowCompletionOr<GC::Ref<DateTimeFormat>> create_date_time_format(VM& vm, Funct
// d. Let styles be resolvedLocaleData.[[styles]].[[<resolvedCalendar>]].
// e. Let bestFormat be DateTimeStyleFormat(dateStyle, timeStyle, styles).
formatter = Unicode::DateTimeFormat::create_for_date_and_time_style(
date_time_format->locale(),
date_time_format->icu_locale(),
time_zone,
format_options.hour_cycle,
format_options.hour12,
@ -443,7 +444,7 @@ ThrowCompletionOr<GC::Ref<DateTimeFormat>> create_date_time_format(VM& vm, Funct
}
formatter = Unicode::DateTimeFormat::create_for_pattern_options(
date_time_format->locale(),
date_time_format->icu_locale(),
time_zone,
best_format);
}

View file

@ -89,7 +89,7 @@ ThrowCompletionOr<GC::Ref<Object>> DurationFormatConstructor::construct(Function
// 11. Let resolvedLocaleData be r.[[LocaleData]].
// 12. Let digitalFormat be resolvedLocaleData.[[DigitalFormat]].
auto digital_format = Unicode::digital_format(duration_format->locale());
auto digital_format = Unicode::digital_format(result.icu_locale);
// 13. Set durationFormat.[[HourMinuteSeparator]] to digitalFormat.[[HourMinuteSeparator]].
duration_format->set_hour_minute_separator(move(digital_format.hours_minutes_separator));

View file

@ -184,8 +184,7 @@ ThrowCompletionOr<GC::Ref<Object>> NumberFormatConstructor::construct(FunctionOb
// Non-standard, create an ICU number formatter for this Intl object.
auto formatter = Unicode::NumberFormat::create(
number_format->locale(),
number_format->numbering_system(),
result.icu_locale,
number_format->display_options(),
number_format->rounding_options());
number_format->set_formatter(move(formatter));

View file

@ -87,8 +87,7 @@ ThrowCompletionOr<GC::Ref<Object>> PluralRulesConstructor::construct(FunctionObj
// Non-standard, create an ICU number formatter for this Intl object.
auto formatter = Unicode::NumberFormat::create(
plural_rules->locale(),
{},
result.icu_locale,
{},
plural_rules->rounding_options());

View file

@ -112,7 +112,7 @@ ThrowCompletionOr<GC::Ref<Object>> RelativeTimeFormatConstructor::construct(Func
// 20. Let relativeTimeFormat.[[NumberFormat]] be ! Construct(%Intl.NumberFormat%, « locale »).
// 21. Let relativeTimeFormat.[[PluralRules]] be ! Construct(%Intl.PluralRules%, « locale »).
auto formatter = Unicode::RelativeTimeFormat::create(
relative_time_format->locale(),
result.icu_locale,
relative_time_format->style());
relative_time_format->set_formatter(move(formatter));

View file

@ -652,17 +652,17 @@ describe("Temporal objects", () => {
test("Temporal.PlainDate", () => {
const plainDate = new Temporal.PlainDate(1989, 1, 23);
expect(formatter.format(plainDate)).toBe("1/23/1989");
expect(formatter.format(plainDate)).toBe("1989-01-23");
});
test("Temporal.PlainYearMonth", () => {
const plainYearMonth = new Temporal.PlainYearMonth(1989, 1);
expect(formatter.format(plainYearMonth)).toBe("1/1989");
expect(formatter.format(plainYearMonth)).toBe("1989-01");
});
test("Temporal.PlainMonthDay", () => {
const plainMonthDay = new Temporal.PlainMonthDay(1, 23);
expect(formatter.format(plainMonthDay)).toBe("1/23");
expect(formatter.format(plainMonthDay)).toBe("01-23");
});
test("Temporal.PlainTime", () => {
@ -672,6 +672,6 @@ describe("Temporal objects", () => {
test("Temporal.Instant", () => {
const instant = new Temporal.Instant(1732740069000000000n);
expect(formatter.format(instant)).toBe("11/27/2024, 8:41:09 PM");
expect(formatter.format(instant)).toBe("2024-11-27, 8:41:09 PM");
});
});

View file

@ -322,19 +322,19 @@ describe("Temporal objects", () => {
test("Temporal.PlainDate", () => {
const plainDate1 = new Temporal.PlainDate(1989, 1, 23);
const plainDate2 = new Temporal.PlainDate(2024, 11, 27);
expect(formatter.formatRange(plainDate1, plainDate2)).toBe("1/23/1989 11/27/2024");
expect(formatter.formatRange(plainDate1, plainDate2)).toBe("1989-01-23 2024-11-27");
});
test("Temporal.PlainYearMonth", () => {
const plainYearMonth1 = new Temporal.PlainYearMonth(1989, 1);
const plainYearMonth2 = new Temporal.PlainYearMonth(2024, 11);
expect(formatter.formatRange(plainYearMonth1, plainYearMonth2)).toBe("1/1989 11/2024");
expect(formatter.formatRange(plainYearMonth1, plainYearMonth2)).toBe("1989-01 2024-11");
});
test("Temporal.PlainMonthDay", () => {
const plainMonthDay1 = new Temporal.PlainMonthDay(1, 23);
const plainMonthDay2 = new Temporal.PlainMonthDay(11, 27);
expect(formatter.formatRange(plainMonthDay1, plainMonthDay2)).toBe("1/23 11/27");
expect(formatter.formatRange(plainMonthDay1, plainMonthDay2)).toBe("01-23 11-27");
});
test("Temporal.PlainTime", () => {
@ -347,7 +347,7 @@ describe("Temporal objects", () => {
const instant1 = new Temporal.Instant(601546251000000000n);
const instant2 = new Temporal.Instant(1732740069000000000n);
expect(formatter.formatRange(instant1, instant2)).toBe(
"1/23/1989, 8:10:51 AM 11/27/2024, 8:41:09 PM"
"1989-01-23, 8:10:51 AM 2024-11-27, 8:41:09 PM"
);
});
});

View file

@ -736,17 +736,17 @@ describe("Temporal objects", () => {
const plainDate1 = new Temporal.PlainDate(1989, 1, 23);
const plainDate2 = new Temporal.PlainDate(2024, 11, 27);
expect(formatter.formatRangeToParts(plainDate1, plainDate2)).toEqual([
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "-", source: "startRange" },
{ type: "month", value: "01", source: "startRange" },
{ type: "literal", value: "-", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "27", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2024", source: "endRange" },
{ type: "literal", value: "-", source: "endRange" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "-", source: "endRange" },
{ type: "day", value: "27", source: "endRange" },
]);
});
@ -754,13 +754,13 @@ describe("Temporal objects", () => {
const plainYearMonth1 = new Temporal.PlainYearMonth(1989, 1);
const plainYearMonth2 = new Temporal.PlainYearMonth(2024, 11);
expect(formatter.formatRangeToParts(plainYearMonth1, plainYearMonth2)).toEqual([
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "-", source: "startRange" },
{ type: "month", value: "01", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2024", source: "endRange" },
{ type: "literal", value: "-", source: "endRange" },
{ type: "month", value: "11", source: "endRange" },
]);
});
@ -768,12 +768,12 @@ describe("Temporal objects", () => {
const plainMonthDay1 = new Temporal.PlainMonthDay(1, 23);
const plainMonthDay2 = new Temporal.PlainMonthDay(11, 27);
expect(formatter.formatRangeToParts(plainMonthDay1, plainMonthDay2)).toEqual([
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "month", value: "01", source: "startRange" },
{ type: "literal", value: "-", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "literal", value: "-", source: "endRange" },
{ type: "day", value: "27", source: "endRange" },
]);
});
@ -804,11 +804,11 @@ describe("Temporal objects", () => {
const instant1 = new Temporal.Instant(601546251000000000n);
const instant2 = new Temporal.Instant(1732740069000000000n);
expect(formatter.formatRangeToParts(instant1, instant2)).toEqual([
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "-", source: "startRange" },
{ type: "month", value: "01", source: "startRange" },
{ type: "literal", value: "-", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: ", ", source: "startRange" },
{ type: "hour", value: "8", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
@ -818,11 +818,11 @@ describe("Temporal objects", () => {
{ type: "literal", value: " ", source: "startRange" },
{ type: "dayPeriod", value: "AM", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "27", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2024", source: "endRange" },
{ type: "literal", value: "-", source: "endRange" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "-", source: "endRange" },
{ type: "day", value: "27", source: "endRange" },
{ type: "literal", value: ", ", source: "endRange" },
{ type: "hour", value: "8", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },

View file

@ -350,28 +350,28 @@ describe("Temporal objects", () => {
test("Temporal.PlainDate", () => {
const plainDate = new Temporal.PlainDate(1989, 1, 23);
expect(formatter.formatToParts(plainDate)).toEqual([
{ type: "month", value: "1" },
{ type: "literal", value: "/" },
{ type: "day", value: "23" },
{ type: "literal", value: "/" },
{ type: "year", value: "1989" },
{ type: "literal", value: "-" },
{ type: "month", value: "01" },
{ type: "literal", value: "-" },
{ type: "day", value: "23" },
]);
});
test("Temporal.PlainYearMonth", () => {
const plainYearMonth = new Temporal.PlainYearMonth(1989, 1);
expect(formatter.formatToParts(plainYearMonth)).toEqual([
{ type: "month", value: "1" },
{ type: "literal", value: "/" },
{ type: "year", value: "1989" },
{ type: "literal", value: "-" },
{ type: "month", value: "01" },
]);
});
test("Temporal.PlainMonthDay", () => {
const plainMonthDay = new Temporal.PlainMonthDay(1, 23);
expect(formatter.formatToParts(plainMonthDay)).toEqual([
{ type: "month", value: "1" },
{ type: "literal", value: "/" },
{ type: "month", value: "01" },
{ type: "literal", value: "-" },
{ type: "day", value: "23" },
]);
});
@ -392,11 +392,11 @@ describe("Temporal objects", () => {
test("Temporal.Instant", () => {
const instant = new Temporal.Instant(1732740069000000000n);
expect(formatter.formatToParts(instant)).toEqual([
{ type: "month", value: "11" },
{ type: "literal", value: "/" },
{ type: "day", value: "27" },
{ type: "literal", value: "/" },
{ type: "year", value: "2024" },
{ type: "literal", value: "-" },
{ type: "month", value: "11" },
{ type: "literal", value: "-" },
{ type: "day", value: "27" },
{ type: "literal", value: ", " },
{ type: "hour", value: "8" },
{ type: "literal", value: ":" },

View file

@ -129,6 +129,11 @@ describe("second", () => {
runTest("second", "narrow", "auto", en, ar, pl);
});
test("numberingSystem set via locale options", () => {
const formatter = new Intl.RelativeTimeFormat("en", { numberingSystem: "arab" });
expect(formatter.format(1, "second")).toBe("in ١ second");
});
});
describe("minute", () => {

View file

@ -38,7 +38,7 @@ DigitalFormat digital_format(StringView locale)
rounding_options.min_significant_digits = 1;
rounding_options.max_significant_digits = 2;
auto number_formatter = NumberFormat::create(locale, "latn"sv, {}, rounding_options);
auto number_formatter = NumberFormat::create(locale, {}, rounding_options);
auto icu_locale = adopt_own(*locale_data->locale().clone());
icu_locale->setUnicodeKeywordValue("nu", "latn", status);

View file

@ -866,12 +866,9 @@ private:
NonnullOwnPtr<NumberFormat> NumberFormat::create(
StringView locale,
StringView numbering_system,
DisplayOptions const& display_options,
RoundingOptions const& rounding_options)
{
UErrorCode status = U_ZERO_ERROR;
auto locale_data = LocaleData::for_locale(locale);
VERIFY(locale_data.has_value());
@ -879,11 +876,6 @@ NonnullOwnPtr<NumberFormat> NumberFormat::create(
apply_display_options(formatter, display_options);
apply_rounding_options(formatter, rounding_options);
if (!numbering_system.is_empty()) {
if (auto* symbols = icu::NumberingSystem::createInstanceByName(ByteString(numbering_system).characters(), status); symbols && icu_success(status))
formatter = formatter.adoptSymbols(symbols);
}
bool is_unit = display_options.style == NumberFormatStyle::Unit;
return adopt_own(*new NumberFormatImpl(locale_data->locale(), move(formatter), is_unit));
}

View file

@ -142,7 +142,6 @@ class NumberFormat {
public:
static NonnullOwnPtr<NumberFormat> create(
StringView locale,
StringView numbering_system,
DisplayOptions const&,
RoundingOptions const&);