From 5b721e7c8d30c7a30f261bbccfbf2555501b00af Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 11 Sep 2026 11:37:15 +0200 Subject: [PATCH] Extract shared Intl.Collator into CollatorUtils --- src/components/LocaleContextProvider.tsx | 5 ++--- src/libs/CollatorUtils.ts | 25 ++++++++++++++++++++++++ src/libs/ReportNameUtils.ts | 3 ++- src/libs/SearchQueryUtils.ts | 3 ++- 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 src/libs/CollatorUtils.ts diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index b9023fe30d74..35d4578b9382 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -1,6 +1,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; +import getCollator from '@libs/CollatorUtils'; import DateUtils from '@libs/DateUtils'; import {fromLocaleDigit as fromLocaleDigitLocaleDigitUtils, toLocaleDigit as toLocaleDigitLocaleDigitUtils, toLocaleOrdinal as toLocaleOrdinalLocaleDigitUtils} from '@libs/LocaleDigitUtils'; import {formatPhoneNumberWithCountryCode} from '@libs/LocalePhoneNumber'; @@ -94,8 +95,6 @@ const LocaleContext = createContext({ dateFnsLocale: undefined, }); -const COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}; - function LocaleContextProvider({children}: LocaleContextProviderProps) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [areTranslationsLoading = true] = useOnyx(ONYXKEYS.RAM_ONLY_ARE_TRANSLATIONS_LOADING); @@ -140,7 +139,7 @@ function LocaleContextProvider({children}: LocaleContextProviderProps) { const selectedTimezone = currentUserPersonalDetails?.timezone?.selected; const effectiveTimezone = selectedTimezone ?? CONST.DEFAULT_TIME_ZONE.selected; - const collator = new Intl.Collator(currentLocale, COLLATOR_OPTIONS); + const collator = getCollator(currentLocale); const translate: LocaleContextProps['translate'] = (path, ...parameters) => translateLocalize(currentLocale, path, ...parameters); diff --git a/src/libs/CollatorUtils.ts b/src/libs/CollatorUtils.ts new file mode 100644 index 000000000000..09a4dd78c5ca --- /dev/null +++ b/src/libs/CollatorUtils.ts @@ -0,0 +1,25 @@ +import type Locale from '@src/types/onyx/Locale'; + +/** + * Options shared by every collator in the app so that sorting stays consistent everywhere. + */ +const COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}; + +const collators = new Map(); + +/** + * Returns a cached collator for the given locale. Collators are cached because constructing an Intl.Collator + * loads locale data, which is far too expensive to repeat per comparison inside a sort. + */ +function getCollator(locale: Locale | undefined): Intl.Collator { + const key = locale ?? ''; + const cachedCollator = collators.get(key); + if (cachedCollator) { + return cachedCollator; + } + const collator = new Intl.Collator(locale, COLLATOR_OPTIONS); + collators.set(key, collator); + return collator; +} + +export default getCollator; diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 416fe057a550..1b47de3b76a9 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -29,6 +29,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {Str} from 'expensify-common'; import {getAddAgentRuleMessage, getDeleteAgentRuleMessage, getUpdateAgentRuleMessage} from './AgentRuleChangeLogUtils'; +import getCollator from './CollatorUtils'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from './LocalePhoneNumber'; import {translateLocal} from './Localize'; // eslint-disable-next-line import/no-cycle @@ -270,7 +271,7 @@ const buildReportNameFromParticipantNames = ({ * The reason for this is that the computation of default group name should not depend on the locale. * This is used to ensure that group name stays consistent across locales. */ -const customCollator = new Intl.Collator('en', {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}); +const customCollator = getCollator(CONST.LOCALES.EN); /** * Returns the report name if the report is a group chat diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index c9fe9d89562f..17e670525573 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -60,6 +60,7 @@ import {getStandardExportTemplateDisplayName} from './AccountingUtils'; import {getBankAccountSearchLabel, isBankAccountPartiallySetup} from './BankAccountUtils'; import {getCardFeedsForDisplay} from './CardFeedUtils'; import {getCardDescription} from './CardUtils'; +import getCollator from './CollatorUtils'; import {convertToBackendAmount, convertToFrontendAmountAsInteger} from './CurrencyUtils'; import DateUtils from './DateUtils'; import Log from './Log'; @@ -553,7 +554,7 @@ function getUpdatedFilterValue(filterName: SyntaxFilterKey, filterValue: string * The reason for this is that the computation of hashes should not depend on the locale. * This is used to ensure that hashes stay consistent. */ -const customCollator = new Intl.Collator('en', {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}); +const customCollator = getCollator(CONST.LOCALES.EN); let defaultSearchQueryJSON: SearchQueryJSON | undefined;