Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions deps/v8/src/objects/js-date-time-format.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2695,11 +2695,35 @@ MaybeDirectHandle<JSDateTimeFormat> JSDateTimeFormat::CreateDateTimeFormat(
DCHECK(U_SUCCESS(status));
}

// The ICU "iso8601" calendar resource bundle does not carry month-name (or
// most other) symbol data and therefore inherits empty strings rather than
// falling back to "gregory", which leaves MMMM/MMM expansions blank in the
// generated pattern (see nodejs/node#63041). Per the Unicode TR35 definition
// the iso8601 calendar is gregorian with ISO week-numbering, so date and
// time symbols are identical to gregorian. Use a copy of the locale with
// ca=gregory for pattern lookup and SimpleDateFormat construction; the
// iso8601 Calendar instance is still attached below via adoptCalendar so
// resolvedOptions().calendar continues to report "iso8601".
icu::Locale icu_locale_for_patterns(icu_locale);
{
UErrorCode ca_status = U_ZERO_ERROR;
std::string ca_value =
icu_locale_for_patterns.getUnicodeKeywordValue<std::string>(
"ca", ca_status);
if (U_SUCCESS(ca_status) && ca_value == "iso8601") {
ca_status = U_ZERO_ERROR;
icu_locale_for_patterns.setUnicodeKeywordValue("ca", "gregory",
ca_status);
DCHECK(U_SUCCESS(ca_status));
}
}

static base::LazyInstance<DateTimePatternGeneratorCache>::type
generator_cache = LAZY_INSTANCE_INITIALIZER;

std::unique_ptr<icu::DateTimePatternGenerator> generator(
generator_cache.Pointer()->CreateGenerator(isolate, icu_locale));
generator_cache.Pointer()->CreateGenerator(isolate,
icu_locale_for_patterns));

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

icu_date_format =
DateTimeStylePattern(date_style, time_style, icu_locale,
DateTimeStylePattern(date_style, time_style, icu_locale_for_patterns,
dateTimeFormatHourCycle, generator.get());
if (icu_date_format.get() == nullptr) {
THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError));
Expand Down Expand Up @@ -3002,13 +3026,18 @@ MaybeDirectHandle<JSDateTimeFormat> JSDateTimeFormat::CreateDateTimeFormat(
dateTimeFormatHourCycle = HourCycle::kUndefined;
}
icu::UnicodeString skeleton_ustr(skeleton.c_str());
icu_date_format = CreateICUDateFormatFromCache(
icu_locale, skeleton_ustr, generator.get(), dateTimeFormatHourCycle);
icu_date_format = CreateICUDateFormatFromCache(icu_locale_for_patterns,
skeleton_ustr,
generator.get(),
dateTimeFormatHourCycle);
if (icu_date_format.get() == nullptr) {
// Remove extensions and try again.
icu_locale = icu::Locale(icu_locale.getBaseName());
icu_date_format = CreateICUDateFormatFromCache(
icu_locale, skeleton_ustr, generator.get(), dateTimeFormatHourCycle);
icu_locale_for_patterns =
icu::Locale(icu_locale_for_patterns.getBaseName());
icu_date_format = CreateICUDateFormatFromCache(icu_locale_for_patterns,
skeleton_ustr,
generator.get(),
dateTimeFormatHourCycle);
if (icu_date_format.get() == nullptr) {
THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError));
}
Expand Down
84 changes: 84 additions & 0 deletions test/parallel/test-intl-iso8601-calendar-month.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict';

// Regression test for nodejs/node#63041:
// `Intl.DateTimeFormat` with `calendar: 'iso8601'` previously dropped the
// month-name part because ICU's iso8601 calendar resource bundle is empty
// and inherits blank month-name symbols instead of falling back to gregory.
// The fix routes pattern lookup through ca=gregory while keeping the
// iso8601 Calendar instance attached, so resolvedOptions().calendar still
// reports "iso8601" and the formatted output contains the month name.

const common = require('../common');
if (!common.hasIntl) {
common.skip('missing Intl');
}

const assert = require('assert');

const date = new Date('2024-09-09T08:00:00Z');

{
// dateStyle:'full' + timeStyle:'long' is the exact case from the bug report.
const dtf = new Intl.DateTimeFormat('en-US', {
dateStyle: 'full',
timeStyle: 'long',
timeZone: 'UTC',
calendar: 'iso8601',
});
const formatted = dtf.format(date);
assert.match(
formatted,
/September/,
`expected month name in ${JSON.stringify(formatted)}`,
);
// resolvedOptions().calendar must still be the user-requested iso8601.
assert.strictEqual(dtf.resolvedOptions().calendar, 'iso8601');
}

{
// Explicit field options must also retain the month name.
const dtf = new Intl.DateTimeFormat('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
calendar: 'iso8601',
});
assert.match(dtf.format(date), /September/);
assert.strictEqual(dtf.resolvedOptions().calendar, 'iso8601');
}

{
// Standalone month: 'long' must format as the month name, not empty string.
const dtf = new Intl.DateTimeFormat('en-US', {
month: 'long',
timeZone: 'UTC',
calendar: 'iso8601',
});
assert.strictEqual(dtf.format(date), 'September');
}

{
// dateStyle:'short' has always produced ISO-style numeric output and should
// continue to do so.
const dtf = new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeZone: 'UTC',
calendar: 'iso8601',
});
assert.strictEqual(dtf.format(date), '2024-09-09');
}

{
// formatToParts must expose the month part with the iso8601 calendar.
const dtf = new Intl.DateTimeFormat('en-US', {
dateStyle: 'long',
timeZone: 'UTC',
calendar: 'iso8601',
});
const parts = dtf.formatToParts(date);
const monthPart = parts.find((p) => p.type === 'month');
assert.ok(monthPart, `expected a month part, got ${JSON.stringify(parts)}`);
assert.strictEqual(monthPart.value, 'September');
}
Loading