diff --git a/deps/v8/src/objects/js-date-time-format.cc b/deps/v8/src/objects/js-date-time-format.cc index bac96514bfa340..56d21e6d40e7b5 100644 --- a/deps/v8/src/objects/js-date-time-format.cc +++ b/deps/v8/src/objects/js-date-time-format.cc @@ -2695,11 +2695,35 @@ MaybeDirectHandle 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( + "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::type generator_cache = LAZY_INSTANCE_INITIALIZER; std::unique_ptr 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)); @@ -2926,7 +2950,7 @@ MaybeDirectHandle 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)); @@ -3002,13 +3026,18 @@ MaybeDirectHandle 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)); } diff --git a/test/parallel/test-intl-iso8601-calendar-month.js b/test/parallel/test-intl-iso8601-calendar-month.js new file mode 100644 index 00000000000000..40bc272605b7b6 --- /dev/null +++ b/test/parallel/test-intl-iso8601-calendar-month.js @@ -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'); +}