diff --git a/src/internal/utils/date-time/__tests__/intl-date-time-format-cache.test.ts b/src/internal/utils/date-time/__tests__/intl-date-time-format-cache.test.ts new file mode 100644 index 0000000000..ac05bfc9e0 --- /dev/null +++ b/src/internal/utils/date-time/__tests__/intl-date-time-format-cache.test.ts @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { getDateTimeFormat } from '../intl-date-time-format-cache'; + +describe('getDateTimeFormat', () => { + test('returns an Intl.DateTimeFormat instance', () => { + const formatter = getDateTimeFormat('en-US', { month: 'long' }); + expect(formatter).toBeInstanceOf(Intl.DateTimeFormat); + }); + + test('returns the same instance for identical locale and options', () => { + const formatter1 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' }); + const formatter2 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' }); + expect(formatter1).toBe(formatter2); + }); + + test('returns the same instance regardless of options property order', () => { + const formatter1 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' }); + const formatter2 = getDateTimeFormat('en-US', { year: 'numeric', month: 'long' }); + expect(formatter1).toBe(formatter2); + }); + + test('returns different instances for different locales', () => { + const formatter1 = getDateTimeFormat('en-US', { month: 'long' }); + const formatter2 = getDateTimeFormat('de-DE', { month: 'long' }); + expect(formatter1).not.toBe(formatter2); + }); + + test('returns different instances for different options', () => { + const formatter1 = getDateTimeFormat('en-US', { month: 'long' }); + const formatter2 = getDateTimeFormat('en-US', { month: 'short' }); + expect(formatter1).not.toBe(formatter2); + }); + + test('handles undefined locale', () => { + const formatter = getDateTimeFormat(undefined, { month: 'long' }); + expect(formatter).toBeInstanceOf(Intl.DateTimeFormat); + }); + + test('formats dates correctly', () => { + const formatter = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' }); + const date = new Date(2023, 5, 15); // June 15, 2023 + expect(formatter.format(date)).toBe('June 2023'); + }); + + test('caches formatters with complex options', () => { + const options: Intl.DateTimeFormatOptions = { + hour: '2-digit', + hourCycle: 'h23', + minute: '2-digit', + second: '2-digit', + }; + const formatter1 = getDateTimeFormat('en-US', options); + const formatter2 = getDateTimeFormat('en-US', options); + expect(formatter1).toBe(formatter2); + }); +}); diff --git a/src/internal/utils/date-time/format-date-localized.ts b/src/internal/utils/date-time/format-date-localized.ts index 0683e4afbc..9d04ea5295 100644 --- a/src/internal/utils/date-time/format-date-localized.ts +++ b/src/internal/utils/date-time/format-date-localized.ts @@ -3,6 +3,7 @@ import { isValid, parseISO } from 'date-fns'; import { formatTimeOffsetLocalized } from './format-time-offset'; +import { getDateTimeFormat } from './intl-date-time-format-cache'; export default function formatDateLocalized({ date: isoDate, @@ -26,7 +27,7 @@ export default function formatDateLocalized({ } if (isMonthOnly) { - const formattedMonthDate = new Intl.DateTimeFormat(locale, { + const formattedMonthDate = getDateTimeFormat(locale, { month: 'long', year: 'numeric', }).format(date); @@ -34,7 +35,7 @@ export default function formatDateLocalized({ return formattedMonthDate; } - const formattedDate = new Intl.DateTimeFormat(locale, { + const formattedDate = getDateTimeFormat(locale, { month: 'long', year: 'numeric', day: 'numeric', @@ -44,7 +45,7 @@ export default function formatDateLocalized({ return formattedDate; } - const formattedTime = new Intl.DateTimeFormat(locale, { + const formattedTime = getDateTimeFormat(locale, { hour: '2-digit', hourCycle: 'h23', minute: '2-digit', diff --git a/src/internal/utils/date-time/intl-date-time-format-cache.ts b/src/internal/utils/date-time/intl-date-time-format-cache.ts new file mode 100644 index 0000000000..bb5e810e79 --- /dev/null +++ b/src/internal/utils/date-time/intl-date-time-format-cache.ts @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Constructing Intl.DateTimeFormat is expensive (the browser parses locale data and +// resolves options), so we cache instances keyed by locale and options for reuse. + +// No eviction: the number of unique locale/option combinations in an application is +// small and bounded. +const formatterCache = new Map(); + +export function getDateTimeFormat( + locale: string | undefined, + options: Intl.DateTimeFormatOptions +): Intl.DateTimeFormat { + const cacheKey = createCacheKey(locale, options); + const cached = formatterCache.get(cacheKey); + + if (cached) { + return cached; + } + + const formatter = new Intl.DateTimeFormat(locale, options); + formatterCache.set(cacheKey, formatter); + return formatter; +} + +// Options are sorted by key so the cache hits regardless of property order. +function createCacheKey(locale: string | undefined, options: Intl.DateTimeFormatOptions): string { + const localeKey = locale ?? ''; + const optionsKey = Object.keys(options) + .sort() + .map(key => `${key}:${options[key as keyof Intl.DateTimeFormatOptions]}`) + .join(','); + return `${localeKey}|${optionsKey}`; +}