From 142dba7d6e021a1d0bfe6ba0a37bb9392f9f5b16 Mon Sep 17 00:00:00 2001 From: Byron Wall Date: Tue, 10 Feb 2026 10:37:25 -0500 Subject: [PATCH 1/2] fix(getLocalizedDateTimeFormatPattern): use Intl.DateTimeFormat.formatToParts to generate localized date and time format description strings instead of relying on moment.js locale descriptions functions, since moment.js does not include format informations for many english locales and falls back to en which uses US mm/dd/yyyy formatting, which is incorrect as the vast majority of en locales use dd/mm/yyyy formatting. Add test for en-be formatting description to ensure that locales moment uses a fallback for are now displaying the correct format string --- .../src/calendar-time.spec.js | 3 + .../calendar-time-utils/src/calendar-time.ts | 74 ++++++++++++++++--- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/packages/calendar-time-utils/src/calendar-time.spec.js b/packages/calendar-time-utils/src/calendar-time.spec.js index 549b4e1459..8ac3b4b8b2 100644 --- a/packages/calendar-time-utils/src/calendar-time.spec.js +++ b/packages/calendar-time-utils/src/calendar-time.spec.js @@ -18,6 +18,7 @@ describe('getLocalizedDateTimeFormatPattern', () => { en: 'MM/DD/YYYY', 'en-GB': 'DD/MM/YYYY', 'en-AU': 'DD/MM/YYYY', + 'en-BE': 'DD/MM/YYYY', es: 'DD/MM/AAAA', 'fr-FR': 'JJ/MM/AAAA', 'pt-BR': 'DD/MM/AAAA', @@ -29,6 +30,7 @@ describe('getLocalizedDateTimeFormatPattern', () => { en: 'HH:mm AM/PM', 'en-GB': 'HH:mm', 'en-AU': 'HH:mm AM/PM', + 'en-BE': 'HH:mm', es: 'HH:mm', 'fr-FR': 'HH:mm', 'pt-BR': 'HH:mm', @@ -40,6 +42,7 @@ describe('getLocalizedDateTimeFormatPattern', () => { en: 'MM/DD/YYYY - HH:mm AM/PM', 'en-GB': 'DD/MM/YYYY - HH:mm', 'en-AU': 'DD/MM/YYYY - HH:mm AM/PM', + 'en-BE': 'DD/MM/YYYY - HH:mm', es: 'DD/MM/AAAA - HH:mm', 'fr-FR': 'JJ/MM/AAAA - HH:mm', 'pt-BR': 'DD/MM/AAAA - HH:mm', diff --git a/packages/calendar-time-utils/src/calendar-time.ts b/packages/calendar-time-utils/src/calendar-time.ts index b9d49d5346..80620aff3b 100644 --- a/packages/calendar-time-utils/src/calendar-time.ts +++ b/packages/calendar-time-utils/src/calendar-time.ts @@ -198,6 +198,64 @@ export const parseInputText = ( return ''; }; +const getIntlDatePattern = (locale: string): string => { + const formatter = new Intl.DateTimeFormat(locale, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); + const parts = formatter.formatToParts(new Date(2020, 0, 15)); + return parts + .map((part) => { + switch (part.type) { + case 'day': + return 'DD'; + case 'month': + return 'MM'; + case 'year': + return 'YYYY'; + case 'literal': + return part.value; + default: + return ''; + } + }) + .join(''); +}; + +const getIntlTimePattern = (locale: string): string => { + const formatter = new Intl.DateTimeFormat(locale, { + timeStyle: 'short', + }); + // Use 2:30 AM so the hour is single-digit, allowing us to detect padding + const parts = formatter.formatToParts(new Date(2020, 0, 15, 2, 30)); + const hasDayPeriod = parts.some((p) => p.type === 'dayPeriod'); + + return parts + .map((part) => { + switch (part.type) { + case 'hour': { + const padded = part.value.length === 2; + if (hasDayPeriod) { + return padded ? 'hh' : 'h'; + } + return padded ? 'HH' : 'H'; + } + case 'minute': + return 'mm'; + case 'dayPeriod': + return 'A'; + case 'literal': + // Normalize unicode whitespace (e.g. U+202F narrow no-break space) + // to regular spaces for consistent placeholder text + return part.value.replace(/\s/g, ' '); + default: + return ''; + } + }) + .join(''); +}; + const localizedDateFormatPatternCache = new Map(); export const getLocalizedDateTimeFormatPattern = ( locale: string, @@ -208,22 +266,18 @@ export const getLocalizedDateTimeFormatPattern = ( return localizedDateFormatPatternCache.get(key)!; } - // References: - // https://momentjs.com/docs/#/i18n/locale-data/ - // https://momentjs.com/docs/#/displaying/ ("Localized formats" section) - const localeData = moment().locale(locale).localeData(); let localizedFormat = ''; switch (formatType) { case 'date': - localizedFormat = localeData.longDateFormat('L'); + localizedFormat = getIntlDatePattern(locale); break; case 'time': - localizedFormat = localeData.longDateFormat('LT'); + localizedFormat = getIntlTimePattern(locale); break; case 'full': - localizedFormat = `${localeData.longDateFormat( - 'L' - )} - ${localeData.longDateFormat('LT')}`; + localizedFormat = `${getIntlDatePattern(locale)} - ${getIntlTimePattern( + locale + )}`; break; default: throw new Error( @@ -241,7 +295,7 @@ export const getLocalizedDateTimeFormatPattern = ( ); // In case we don't have a translation for the locale, we fallback to the - // pattern from the moment locale data. + // Intl-derived pattern. let pattern = localizedFormat; if (localeMappings && localeMappings.length > 0) { pattern = localeMappings.reduce( From fe32223affa7d0f041ff9e0fa6fb4d600a092946 Mon Sep 17 00:00:00 2001 From: Byron Wall Date: Tue, 10 Feb 2026 11:09:16 -0500 Subject: [PATCH 2/2] fix(getLocalizedDateTimeFormatPattern): add test for en-gu to make sure that us-formatted en locales return correctly --- .changeset/nine-dolls-press.md | 5 +++++ packages/calendar-time-utils/src/calendar-time.spec.js | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .changeset/nine-dolls-press.md diff --git a/.changeset/nine-dolls-press.md b/.changeset/nine-dolls-press.md new file mode 100644 index 0000000000..3d8a32322c --- /dev/null +++ b/.changeset/nine-dolls-press.md @@ -0,0 +1,5 @@ +--- +'@commercetools-uikit/calendar-time-utils': patch +--- + +Update getLocalizedDateTimeFormatPattern method to use Intl.DateTimeFormat.formatToParts to generate localized date and time format description strings instead of relying on moment.js locale description methods diff --git a/packages/calendar-time-utils/src/calendar-time.spec.js b/packages/calendar-time-utils/src/calendar-time.spec.js index 8ac3b4b8b2..9746b4c861 100644 --- a/packages/calendar-time-utils/src/calendar-time.spec.js +++ b/packages/calendar-time-utils/src/calendar-time.spec.js @@ -18,7 +18,8 @@ describe('getLocalizedDateTimeFormatPattern', () => { en: 'MM/DD/YYYY', 'en-GB': 'DD/MM/YYYY', 'en-AU': 'DD/MM/YYYY', - 'en-BE': 'DD/MM/YYYY', + 'en-BE': 'DD/MM/YYYY', // locale information not available via moment.js + 'en-GU': 'MM/DD/YYYY', // locale information not available via moment.js es: 'DD/MM/AAAA', 'fr-FR': 'JJ/MM/AAAA', 'pt-BR': 'DD/MM/AAAA', @@ -30,7 +31,8 @@ describe('getLocalizedDateTimeFormatPattern', () => { en: 'HH:mm AM/PM', 'en-GB': 'HH:mm', 'en-AU': 'HH:mm AM/PM', - 'en-BE': 'HH:mm', + 'en-BE': 'HH:mm', // locale information not available via moment.js + 'en-GU': 'HH:mm AM/PM', // locale information not available via moment.js es: 'HH:mm', 'fr-FR': 'HH:mm', 'pt-BR': 'HH:mm', @@ -42,7 +44,8 @@ describe('getLocalizedDateTimeFormatPattern', () => { en: 'MM/DD/YYYY - HH:mm AM/PM', 'en-GB': 'DD/MM/YYYY - HH:mm', 'en-AU': 'DD/MM/YYYY - HH:mm AM/PM', - 'en-BE': 'DD/MM/YYYY - HH:mm', + 'en-BE': 'DD/MM/YYYY - HH:mm', // locale information not available via moment.js + 'en-GU': 'MM/DD/YYYY - HH:mm AM/PM', // locale information not available via moment.js es: 'DD/MM/AAAA - HH:mm', 'fr-FR': 'JJ/MM/AAAA - HH:mm', 'pt-BR': 'DD/MM/AAAA - HH:mm',