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 549b4e1459..9746b4c861 100644 --- a/packages/calendar-time-utils/src/calendar-time.spec.js +++ b/packages/calendar-time-utils/src/calendar-time.spec.js @@ -18,6 +18,8 @@ describe('getLocalizedDateTimeFormatPattern', () => { en: 'MM/DD/YYYY', 'en-GB': 'DD/MM/YYYY', 'en-AU': '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', @@ -29,6 +31,8 @@ describe('getLocalizedDateTimeFormatPattern', () => { en: 'HH:mm AM/PM', 'en-GB': 'HH:mm', 'en-AU': 'HH:mm AM/PM', + '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', @@ -40,6 +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', // 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', 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(