Skip to content

Commit 0cf569e

Browse files
authored
Merge pull request Expensify#64619 from linhvovan29546/feat/64490-lazy-load-date-fns-locales
feat: handle lazy load date fns locales
2 parents eee5400 + 51b1291 commit 0cf569e

39 files changed

Lines changed: 325 additions & 333 deletions

desktop/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {translate} from '@libs/Localize';
1111
import Log from '@libs/Log';
1212
import CONFIG from '@src/CONFIG';
1313
import CONST from '@src/CONST';
14-
import TranslationStore from '@src/languages/TranslationStore';
14+
import IntlStore from '@src/languages/IntlStore';
1515
import type {TranslationPaths} from '@src/languages/types';
1616
import type PlatformSpecificUpdater from '@src/setup/platformSetup/types';
1717
import type {Locale} from '@src/types/onyx';
@@ -666,7 +666,7 @@ const mainWindow = (): Promise<void> => {
666666
// because the only way code can be shared between the main and renderer processes at runtime is via the context bridge
667667
// So we track preferredLocale separately via ELECTRON_EVENTS.LOCALE_UPDATED
668668
ipcMain.on(ELECTRON_EVENTS.LOCALE_UPDATED, (event, updatedLocale: Locale) => {
669-
TranslationStore.load(updatedLocale).then(() => {
669+
IntlStore.load(updatedLocale).then(() => {
670670
preferredLocale = updatedLocale;
671671
Menu.setApplicationMenu(Menu.buildFromTemplate(localizeMenuItems(initialMenuTemplate, updatedLocale)));
672672
disposeContextMenu?.();

src/components/DatePicker/CalendarPicker/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function CalendarPicker({
6161
const {isSmallScreenWidth} = useResponsiveLayout();
6262
const styles = useThemeStyles();
6363
const themeStyles = useThemeStyles();
64-
const {preferredLocale, translate} = useLocalize();
64+
const {translate} = useLocalize();
6565
const pressableRef = useRef<View>(null);
6666
const [currentDateView, setCurrentDateView] = useState(() => getInitialCurrentDateView(value, minDate, maxDate));
6767
const [isYearPickerVisible, setIsYearPickerVisible] = useState(false);
@@ -150,8 +150,8 @@ function CalendarPicker({
150150
});
151151
};
152152

153-
const monthNames = DateUtils.getMonthNames(preferredLocale).map((month) => Str.recapitalize(month));
154-
const daysOfWeek = DateUtils.getDaysOfWeek(preferredLocale).map((day) => day.toUpperCase());
153+
const monthNames = DateUtils.getMonthNames().map((month) => Str.recapitalize(month));
154+
const daysOfWeek = DateUtils.getDaysOfWeek().map((day) => day.toUpperCase());
155155
const hasAvailableDatesNextMonth = startOfDay(new Date(maxDate)) > endOfMonth(new Date(currentDateView));
156156
const hasAvailableDatesPrevMonth = endOfDay(new Date(minDate)) < startOfMonth(new Date(currentDateView));
157157

src/components/LocaleContextProvider.tsx

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {fromLocaleDigit as fromLocaleDigitLocaleDigitUtils, toLocaleDigit as toL
66
import {formatPhoneNumber as formatPhoneNumberLocalePhoneNumber} from '@libs/LocalePhoneNumber';
77
import {translate as translateLocalize} from '@libs/Localize';
88
import {format} from '@libs/NumberFormatUtils';
9-
import TranslationStore from '@src/languages/TranslationStore';
9+
import IntlStore from '@src/languages/IntlStore';
1010
import type {TranslationParameters, TranslationPaths} from '@src/languages/types';
1111
import ONYXKEYS from '@src/ONYXKEYS';
1212
import type Locale from '@src/types/onyx/Locale';
@@ -33,9 +33,6 @@ type LocaleContextProps = {
3333
/** Formats a datetime to local date and time string */
3434
datetimeToCalendarTime: (datetime: string, includeTimezone: boolean, isLowercase?: boolean) => string;
3535

36-
/** Updates date-fns internal locale */
37-
updateLocale: () => void;
38-
3936
/** Returns a locally converted phone number for numbers from the same region
4037
* and an internationally converted phone number with the country code for numbers from other regions */
4138
formatPhoneNumber: (phoneNumber: string) => string;
@@ -59,7 +56,6 @@ const LocaleContext = createContext<LocaleContextProps>({
5956
getLocalDateFromDatetime: () => new Date(),
6057
datetimeToRelative: () => '',
6158
datetimeToCalendarTime: () => '',
62-
updateLocale: () => '',
6359
formatPhoneNumber: () => '',
6460
toLocaleDigit: () => '',
6561
toLocaleOrdinal: () => '',
@@ -70,14 +66,14 @@ const LocaleContext = createContext<LocaleContextProps>({
7066
function LocaleContextProvider({children}: LocaleContextProviderProps) {
7167
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
7268
const [areTranslationsLoading = true] = useOnyx(ONYXKEYS.ARE_TRANSLATIONS_LOADING, {initWithStoredValues: false, canBeMissing: true});
73-
const [currentLocale, setCurrentLocale] = useState<Locale | undefined>(() => TranslationStore.getCurrentLocale());
69+
const [currentLocale, setCurrentLocale] = useState<Locale | undefined>(() => IntlStore.getCurrentLocale());
7470

7571
useEffect(() => {
7672
if (areTranslationsLoading) {
7773
return;
7874
}
7975

80-
const locale = TranslationStore.getCurrentLocale();
76+
const locale = IntlStore.getCurrentLocale();
8177
if (!locale) {
8278
return;
8379
}
@@ -110,8 +106,6 @@ function LocaleContextProvider({children}: LocaleContextProviderProps) {
110106
[currentLocale, selectedTimezone],
111107
);
112108

113-
const updateLocale = useMemo<LocaleContextProps['updateLocale']>(() => () => DateUtils.setLocale(currentLocale), [currentLocale]);
114-
115109
const formatPhoneNumber = useMemo<LocaleContextProps['formatPhoneNumber']>(() => (phoneNumber) => formatPhoneNumberLocalePhoneNumber(phoneNumber), []);
116110

117111
const toLocaleDigit = useMemo<LocaleContextProps['toLocaleDigit']>(() => (digit) => toLocaleDigitLocaleDigitUtils(currentLocale, digit), [currentLocale]);
@@ -132,26 +126,13 @@ function LocaleContextProvider({children}: LocaleContextProviderProps) {
132126
getLocalDateFromDatetime,
133127
datetimeToRelative,
134128
datetimeToCalendarTime,
135-
updateLocale,
136129
formatPhoneNumber,
137130
toLocaleDigit,
138131
toLocaleOrdinal,
139132
fromLocaleDigit,
140133
preferredLocale: currentLocale,
141134
}),
142-
[
143-
translate,
144-
numberFormat,
145-
getLocalDateFromDatetime,
146-
datetimeToRelative,
147-
datetimeToCalendarTime,
148-
updateLocale,
149-
formatPhoneNumber,
150-
toLocaleDigit,
151-
toLocaleOrdinal,
152-
fromLocaleDigit,
153-
currentLocale,
154-
],
135+
[translate, numberFormat, getLocalDateFromDatetime, datetimeToRelative, datetimeToCalendarTime, formatPhoneNumber, toLocaleDigit, toLocaleOrdinal, fromLocaleDigit, currentLocale],
155136
);
156137

157138
return <LocaleContext.Provider value={contextValue}>{children}</LocaleContext.Provider>;

src/languages/IntlStore.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import {setDefaultOptions} from 'date-fns';
2+
import type {Locale as DateUtilsLocale} from 'date-fns';
3+
import Onyx from 'react-native-onyx';
4+
import extractModuleDefaultExport from '@libs/extractModuleDefaultExport';
5+
import {LOCALES} from '@src/CONST/LOCALES';
6+
import type {Locale} from '@src/CONST/LOCALES';
7+
import ONYXKEYS from '@src/ONYXKEYS';
8+
import type DynamicModule from '@src/types/utils/DynamicModule';
9+
import type de from './de';
10+
import type en from './en';
11+
import type es from './es';
12+
import flattenObject from './flattenObject';
13+
import type fr from './fr';
14+
import type it from './it';
15+
import type ja from './ja';
16+
import type nl from './nl';
17+
import type pl from './pl';
18+
import type ptBR from './pt-BR';
19+
import type {FlatTranslationsObject, TranslationPaths} from './types';
20+
import type zhHans from './zh-hans';
21+
22+
// This function was added here to avoid circular dependencies
23+
function setAreTranslationsLoading(areTranslationsLoading: boolean) {
24+
// eslint-disable-next-line rulesdir/prefer-actions-set-data
25+
Onyx.set(ONYXKEYS.ARE_TRANSLATIONS_LOADING, areTranslationsLoading);
26+
}
27+
28+
class IntlStore {
29+
private static currentLocale: Locale | undefined = undefined;
30+
31+
/**
32+
* Cache for translations
33+
*/
34+
private static cache = new Map<Locale, FlatTranslationsObject>();
35+
36+
/**
37+
* Cache for localized date-fns
38+
* @private
39+
*/
40+
private static dateUtilsCache = new Map<Locale, DateUtilsLocale>();
41+
42+
/**
43+
* Set of loaders for each locale.
44+
* Note that this can't be trivially DRYed up because dynamic imports must use string literals in metro: https://github.com/facebook/metro/issues/52
45+
*/
46+
private static loaders: Record<Locale, () => Promise<[void, void]>> = {
47+
[LOCALES.DE]: () =>
48+
this.cache.has(LOCALES.DE)
49+
? Promise.all([Promise.resolve(), Promise.resolve()])
50+
: Promise.all([
51+
import('./de').then((module: DynamicModule<typeof de>) => {
52+
this.cache.set(LOCALES.DE, flattenObject(extractModuleDefaultExport(module)));
53+
}),
54+
import('date-fns/locale/de').then((module) => {
55+
this.dateUtilsCache.set(LOCALES.DE, module.de);
56+
}),
57+
]),
58+
[LOCALES.EN]: () =>
59+
this.cache.has(LOCALES.EN)
60+
? Promise.all([Promise.resolve(), Promise.resolve()])
61+
: Promise.all([
62+
import('./en').then((module: DynamicModule<typeof en>) => {
63+
this.cache.set(LOCALES.EN, flattenObject(extractModuleDefaultExport(module)));
64+
}),
65+
import('date-fns/locale/en-GB').then((module) => {
66+
this.dateUtilsCache.set(LOCALES.EN, module.enGB);
67+
}),
68+
]),
69+
[LOCALES.ES]: () =>
70+
this.cache.has(LOCALES.ES)
71+
? Promise.all([Promise.resolve(), Promise.resolve()])
72+
: Promise.all([
73+
import('./es').then((module: DynamicModule<typeof es>) => {
74+
this.cache.set(LOCALES.ES, flattenObject(extractModuleDefaultExport(module)));
75+
}),
76+
import('date-fns/locale/es').then((module) => {
77+
this.dateUtilsCache.set(LOCALES.ES, module.es);
78+
}),
79+
]),
80+
[LOCALES.FR]: () =>
81+
this.cache.has(LOCALES.FR)
82+
? Promise.all([Promise.resolve(), Promise.resolve()])
83+
: Promise.all([
84+
import('./fr').then((module: DynamicModule<typeof fr>) => {
85+
this.cache.set(LOCALES.FR, flattenObject(extractModuleDefaultExport(module)));
86+
}),
87+
import('date-fns/locale/fr').then((module) => {
88+
this.dateUtilsCache.set(LOCALES.FR, module.fr);
89+
}),
90+
]),
91+
[LOCALES.IT]: () =>
92+
this.cache.has(LOCALES.IT)
93+
? Promise.all([Promise.resolve(), Promise.resolve()])
94+
: Promise.all([
95+
import('./it').then((module: DynamicModule<typeof it>) => {
96+
this.cache.set(LOCALES.IT, flattenObject(extractModuleDefaultExport(module)));
97+
}),
98+
import('date-fns/locale/it').then((module) => {
99+
this.dateUtilsCache.set(LOCALES.IT, module.it);
100+
}),
101+
]),
102+
[LOCALES.JA]: () =>
103+
this.cache.has(LOCALES.JA)
104+
? Promise.all([Promise.resolve(), Promise.resolve()])
105+
: Promise.all([
106+
import('./ja').then((module: DynamicModule<typeof ja>) => {
107+
this.cache.set(LOCALES.JA, flattenObject(extractModuleDefaultExport(module)));
108+
}),
109+
import('date-fns/locale/ja').then((module) => {
110+
this.dateUtilsCache.set(LOCALES.JA, module.ja);
111+
}),
112+
]),
113+
[LOCALES.NL]: () =>
114+
this.cache.has(LOCALES.NL)
115+
? Promise.all([Promise.resolve(), Promise.resolve()])
116+
: Promise.all([
117+
import('./nl').then((module: DynamicModule<typeof nl>) => {
118+
this.cache.set(LOCALES.NL, flattenObject(extractModuleDefaultExport(module)));
119+
}),
120+
import('date-fns/locale/nl').then((module) => {
121+
this.dateUtilsCache.set(LOCALES.NL, module.nl);
122+
}),
123+
]),
124+
[LOCALES.PL]: () =>
125+
this.cache.has(LOCALES.PL)
126+
? Promise.all([Promise.resolve(), Promise.resolve()])
127+
: Promise.all([
128+
import('./pl').then((module: DynamicModule<typeof pl>) => {
129+
this.cache.set(LOCALES.PL, flattenObject(extractModuleDefaultExport(module)));
130+
}),
131+
import('date-fns/locale/pl').then((module) => {
132+
this.dateUtilsCache.set(LOCALES.PL, module.pl);
133+
}),
134+
]),
135+
[LOCALES.PT_BR]: () =>
136+
this.cache.has(LOCALES.PT_BR)
137+
? Promise.all([Promise.resolve(), Promise.resolve()])
138+
: Promise.all([
139+
import('./pt-BR').then((module: DynamicModule<typeof ptBR>) => {
140+
this.cache.set(LOCALES.PT_BR, flattenObject(extractModuleDefaultExport(module)));
141+
}),
142+
import('date-fns/locale/pt-BR').then((module) => {
143+
this.dateUtilsCache.set(LOCALES.PT_BR, module.ptBR);
144+
}),
145+
]),
146+
[LOCALES.ZH_HANS]: () =>
147+
this.cache.has(LOCALES.ZH_HANS)
148+
? Promise.all([Promise.resolve(), Promise.resolve()])
149+
: Promise.all([
150+
import('./zh-hans').then((module: DynamicModule<typeof zhHans>) => {
151+
this.cache.set(LOCALES.ZH_HANS, flattenObject(extractModuleDefaultExport(module)));
152+
}),
153+
import('date-fns/locale/zh-CN').then((module) => {
154+
this.dateUtilsCache.set(LOCALES.ZH_HANS, module.zhCN);
155+
}),
156+
]),
157+
};
158+
159+
public static getCurrentLocale() {
160+
return this.currentLocale;
161+
}
162+
163+
public static load(locale: Locale) {
164+
if (this.currentLocale === locale) {
165+
return Promise.resolve();
166+
}
167+
const loaderPromise = this.loaders[locale];
168+
setAreTranslationsLoading(true);
169+
return loaderPromise()
170+
.then(() => {
171+
this.currentLocale = locale;
172+
// Set the default date-fns locale
173+
const dateUtilsLocale = this.dateUtilsCache.get(locale);
174+
if (dateUtilsLocale) {
175+
setDefaultOptions({locale: dateUtilsLocale});
176+
}
177+
})
178+
.then(() => {
179+
setAreTranslationsLoading(false);
180+
});
181+
}
182+
183+
public static get<TPath extends TranslationPaths>(key: TPath, locale?: Locale) {
184+
const localeToUse = locale && this.cache.has(locale) ? locale : this.currentLocale;
185+
if (!localeToUse) {
186+
return null;
187+
}
188+
const translations = this.cache.get(localeToUse);
189+
return translations?.[key] ?? null;
190+
}
191+
}
192+
193+
export default IntlStore;

0 commit comments

Comments
 (0)