Skip to content

Commit 04d86da

Browse files
committed
deps,test: restore month name in DateTimeFormat with calendar=iso8601
`Intl.DateTimeFormat` with `calendar: 'iso8601'` (or via the `-u-ca-iso8601` locale extension) silently dropped the month part of the formatted output for `dateStyle` `'medium'`/`'long'`/`'full'` and for any explicit `month` option, leaving e.g. new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeStyle: 'long', timeZone: 'UTC', calendar: 'iso8601', }).format(new Date('2024-09-09T08:00:00Z')) producing `"2024 9, Monday at 08:00:00 AM UTC"` instead of `"2024 September 9, Monday at 08:00:00 AM UTC"`. The ICU `iso8601` calendar resource bundle is intentionally empty: per Unicode TR35 the iso8601 calendar is gregorian with ISO week-numbering and therefore inherits gregorian symbols. ICU does not currently provide month-name fallback for that calendar, so MMMM/MMM expansions resolve to empty strings during pattern construction. Use a copy of the ICU locale with `ca=gregory` for the pattern generator and SimpleDateFormat construction. The iso8601 Calendar instance is still produced from the original locale and attached via `adoptCalendar`, so `resolvedOptions().calendar` continues to report `"iso8601"` and ISO week-numbering semantics are preserved. Refs: #63041 https://claude.ai/code/session_016L6yWKzfkf5JhyDzK5S6sC
1 parent bbf51ad commit 04d86da

2 files changed

Lines changed: 120 additions & 7 deletions

File tree

deps/v8/src/objects/js-date-time-format.cc

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2695,11 +2695,35 @@ MaybeDirectHandle<JSDateTimeFormat> JSDateTimeFormat::CreateDateTimeFormat(
26952695
DCHECK(U_SUCCESS(status));
26962696
}
26972697

2698+
// The ICU "iso8601" calendar resource bundle does not carry month-name (or
2699+
// most other) symbol data and therefore inherits empty strings rather than
2700+
// falling back to "gregory", which leaves MMMM/MMM expansions blank in the
2701+
// generated pattern (see nodejs/node#63041). Per the Unicode TR35 definition
2702+
// the iso8601 calendar is gregorian with ISO week-numbering, so date and
2703+
// time symbols are identical to gregorian. Use a copy of the locale with
2704+
// ca=gregory for pattern lookup and SimpleDateFormat construction; the
2705+
// iso8601 Calendar instance is still attached below via adoptCalendar so
2706+
// resolvedOptions().calendar continues to report "iso8601".
2707+
icu::Locale icu_locale_for_patterns(icu_locale);
2708+
{
2709+
UErrorCode ca_status = U_ZERO_ERROR;
2710+
std::string ca_value =
2711+
icu_locale_for_patterns.getUnicodeKeywordValue<std::string>(
2712+
"ca", ca_status);
2713+
if (U_SUCCESS(ca_status) && ca_value == "iso8601") {
2714+
ca_status = U_ZERO_ERROR;
2715+
icu_locale_for_patterns.setUnicodeKeywordValue("ca", "gregory",
2716+
ca_status);
2717+
DCHECK(U_SUCCESS(ca_status));
2718+
}
2719+
}
2720+
26982721
static base::LazyInstance<DateTimePatternGeneratorCache>::type
26992722
generator_cache = LAZY_INSTANCE_INITIALIZER;
27002723

27012724
std::unique_ptr<icu::DateTimePatternGenerator> generator(
2702-
generator_cache.Pointer()->CreateGenerator(isolate, icu_locale));
2725+
generator_cache.Pointer()->CreateGenerator(isolate,
2726+
icu_locale_for_patterns));
27032727

27042728
// 15.Let hcDefault be dataLocaleData.[[hourCycle]].
27052729
HourCycle hc_default = ToHourCycle(generator->getDefaultHourCycle(status));
@@ -2926,7 +2950,7 @@ MaybeDirectHandle<JSDateTimeFormat> JSDateTimeFormat::CreateDateTimeFormat(
29262950
v8::Isolate::UseCounterFeature::kDateTimeFormatDateTimeStyle);
29272951

29282952
icu_date_format =
2929-
DateTimeStylePattern(date_style, time_style, icu_locale,
2953+
DateTimeStylePattern(date_style, time_style, icu_locale_for_patterns,
29302954
dateTimeFormatHourCycle, generator.get());
29312955
if (icu_date_format.get() == nullptr) {
29322956
THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError));
@@ -3002,13 +3026,18 @@ MaybeDirectHandle<JSDateTimeFormat> JSDateTimeFormat::CreateDateTimeFormat(
30023026
dateTimeFormatHourCycle = HourCycle::kUndefined;
30033027
}
30043028
icu::UnicodeString skeleton_ustr(skeleton.c_str());
3005-
icu_date_format = CreateICUDateFormatFromCache(
3006-
icu_locale, skeleton_ustr, generator.get(), dateTimeFormatHourCycle);
3029+
icu_date_format = CreateICUDateFormatFromCache(icu_locale_for_patterns,
3030+
skeleton_ustr,
3031+
generator.get(),
3032+
dateTimeFormatHourCycle);
30073033
if (icu_date_format.get() == nullptr) {
30083034
// Remove extensions and try again.
3009-
icu_locale = icu::Locale(icu_locale.getBaseName());
3010-
icu_date_format = CreateICUDateFormatFromCache(
3011-
icu_locale, skeleton_ustr, generator.get(), dateTimeFormatHourCycle);
3035+
icu_locale_for_patterns =
3036+
icu::Locale(icu_locale_for_patterns.getBaseName());
3037+
icu_date_format = CreateICUDateFormatFromCache(icu_locale_for_patterns,
3038+
skeleton_ustr,
3039+
generator.get(),
3040+
dateTimeFormatHourCycle);
30123041
if (icu_date_format.get() == nullptr) {
30133042
THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError));
30143043
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict';
2+
3+
// Regression test for nodejs/node#63041:
4+
// `Intl.DateTimeFormat` with `calendar: 'iso8601'` previously dropped the
5+
// month-name part because ICU's iso8601 calendar resource bundle is empty
6+
// and inherits blank month-name symbols instead of falling back to gregory.
7+
// The fix routes pattern lookup through ca=gregory while keeping the
8+
// iso8601 Calendar instance attached, so resolvedOptions().calendar still
9+
// reports "iso8601" and the formatted output contains the month name.
10+
11+
const common = require('../common');
12+
if (!common.hasIntl) {
13+
common.skip('missing Intl');
14+
}
15+
16+
const assert = require('assert');
17+
18+
const date = new Date('2024-09-09T08:00:00Z');
19+
20+
{
21+
// dateStyle:'full' + timeStyle:'long' is the exact case from the bug report.
22+
const dtf = new Intl.DateTimeFormat('en-US', {
23+
dateStyle: 'full',
24+
timeStyle: 'long',
25+
timeZone: 'UTC',
26+
calendar: 'iso8601',
27+
});
28+
const formatted = dtf.format(date);
29+
assert.match(
30+
formatted,
31+
/September/,
32+
`expected month name in ${JSON.stringify(formatted)}`,
33+
);
34+
// resolvedOptions().calendar must still be the user-requested iso8601.
35+
assert.strictEqual(dtf.resolvedOptions().calendar, 'iso8601');
36+
}
37+
38+
{
39+
// Explicit field options must also retain the month name.
40+
const dtf = new Intl.DateTimeFormat('en-US', {
41+
weekday: 'long',
42+
year: 'numeric',
43+
month: 'long',
44+
day: 'numeric',
45+
timeZone: 'UTC',
46+
calendar: 'iso8601',
47+
});
48+
assert.match(dtf.format(date), /September/);
49+
assert.strictEqual(dtf.resolvedOptions().calendar, 'iso8601');
50+
}
51+
52+
{
53+
// Standalone month: 'long' must format as the month name, not empty string.
54+
const dtf = new Intl.DateTimeFormat('en-US', {
55+
month: 'long',
56+
timeZone: 'UTC',
57+
calendar: 'iso8601',
58+
});
59+
assert.strictEqual(dtf.format(date), 'September');
60+
}
61+
62+
{
63+
// dateStyle:'short' has always produced ISO-style numeric output and should
64+
// continue to do so.
65+
const dtf = new Intl.DateTimeFormat('en-US', {
66+
dateStyle: 'short',
67+
timeZone: 'UTC',
68+
calendar: 'iso8601',
69+
});
70+
assert.strictEqual(dtf.format(date), '2024-09-09');
71+
}
72+
73+
{
74+
// formatToParts must expose the month part with the iso8601 calendar.
75+
const dtf = new Intl.DateTimeFormat('en-US', {
76+
dateStyle: 'long',
77+
timeZone: 'UTC',
78+
calendar: 'iso8601',
79+
});
80+
const parts = dtf.formatToParts(date);
81+
const monthPart = parts.find((p) => p.type === 'month');
82+
assert.ok(monthPart, `expected a month part, got ${JSON.stringify(parts)}`);
83+
assert.strictEqual(monthPart.value, 'September');
84+
}

0 commit comments

Comments
 (0)