fix: load only the active language's calendar date locale

`src/lib/calendar-locale.ts` imported its 27 date-fns locales from the
`date-fns/locale` barrel, which re-exports all of date-fns' ~100
locales. Metro bundled every one of them and evaluated them at startup
through the calendar screens.

Each locale is now required from its own `date-fns/locale/<code>`
module the first time `getDateFnsLocale()` asks for it; en-US stays a
static import as the fallback. A test checks that every UI language
still maps to the right date-fns locale.

Measured with `npx expo export --platform android`:
- modules in the bundle: 2,469 -> 2,114 (date-fns locale modules:
  531 -> 170, 1,213 KB -> 358 KB unminified)
- Hermes bundle: 8.19 MB -> 7.61 MB
- modules evaluated at startup: 2,038 -> 1,516 (date-fns locale
  modules: 531 -> 10, i.e. only en-US)
This commit is contained in:
Linus Rath
2026-09-24 19:35:47 +02:00
parent 063432bf28
commit 5bcfe1fe4d
2 changed files with 73 additions and 6 deletions
+30
View File
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import { SUPPORTED_LOCALES } from '../../i18n';
import { getDateFnsLocale } from '../calendar-locale';
// date-fns names a few locales differently from the UI language codes.
const EXPECTED_CODE: Record<string, string> = {
en: 'en-US',
fa: 'fa-IR',
zh: 'zh-CN',
'zh-TW': 'zh-TW',
};
describe('getDateFnsLocale', () => {
it('loads the matching date-fns locale for every UI language', () => {
for (const { code } of SUPPORTED_LOCALES) {
const locale = getDateFnsLocale(code);
expect(locale.code, code).toBe(EXPECTED_CODE[code] ?? code);
expect(typeof locale.localize?.month, code).toBe('function');
// Loaded once, then served from the cache.
expect(getDateFnsLocale(code)).toBe(locale);
}
});
it('falls back to en-US for unknown or missing codes', () => {
expect(getDateFnsLocale('xx').code).toBe('en-US');
expect(getDateFnsLocale('toString').code).toBe('en-US');
expect(getDateFnsLocale(null).code).toBe('en-US');
expect(getDateFnsLocale(undefined).code).toBe('en-US');
});
});
+43 -6
View File
@@ -1,19 +1,56 @@
import type { Locale } from 'date-fns';
import {
ar, ca, cs, da, de, enUS, es, faIR, fr, he, hu, it, ja, ko, lv, mn, nb, nl, pl, pt, ro, ru, sk, tr, uk, zhCN, zhTW,
} from 'date-fns/locale';
import { enUS } from 'date-fns/locale/en-US';
import { useLocaleStore } from '../stores/locale-store';
import type { LocaleCode } from '../i18n';
// date-fns locale for each UI language so month/day names, "EEE, MMM d"
// headers and the like render in the user's language (the webmail localizes
// them through next-intl / useFormatEventDate).
const DATE_FNS_LOCALES: Record<LocaleCode, Locale> = {
en: enUS, ar, ca, cs, da, de, es, fa: faIR, fr, he, hu, it, nl, nb, pl, pt, ro, ru, sk, tr, uk, ja, ko, mn, zh: zhCN, 'zh-TW': zhTW, lv,
//
// Each locale is required from its own module on first use: importing from
// the `date-fns/locale` barrel bundled and evaluated all ~100 date-fns
// locales at startup. The require paths must stay string literals for Metro
// to bundle them.
const DATE_FNS_LOCALE_LOADERS: Record<LocaleCode, () => Locale> = {
en: () => enUS,
ar: () => require('date-fns/locale/ar').ar,
ca: () => require('date-fns/locale/ca').ca,
cs: () => require('date-fns/locale/cs').cs,
da: () => require('date-fns/locale/da').da,
de: () => require('date-fns/locale/de').de,
es: () => require('date-fns/locale/es').es,
fa: () => require('date-fns/locale/fa-IR').faIR,
fr: () => require('date-fns/locale/fr').fr,
he: () => require('date-fns/locale/he').he,
hu: () => require('date-fns/locale/hu').hu,
it: () => require('date-fns/locale/it').it,
ja: () => require('date-fns/locale/ja').ja,
ko: () => require('date-fns/locale/ko').ko,
lv: () => require('date-fns/locale/lv').lv,
mn: () => require('date-fns/locale/mn').mn,
nb: () => require('date-fns/locale/nb').nb,
nl: () => require('date-fns/locale/nl').nl,
pl: () => require('date-fns/locale/pl').pl,
pt: () => require('date-fns/locale/pt').pt,
ro: () => require('date-fns/locale/ro').ro,
ru: () => require('date-fns/locale/ru').ru,
sk: () => require('date-fns/locale/sk').sk,
tr: () => require('date-fns/locale/tr').tr,
uk: () => require('date-fns/locale/uk').uk,
zh: () => require('date-fns/locale/zh-CN').zhCN,
'zh-TW': () => require('date-fns/locale/zh-TW').zhTW,
};
const loaded = new Map<string, Locale>();
export function getDateFnsLocale(code: LocaleCode | string | null | undefined): Locale {
return (code && DATE_FNS_LOCALES[code as LocaleCode]) || enUS;
if (!code || !Object.prototype.hasOwnProperty.call(DATE_FNS_LOCALE_LOADERS, code)) return enUS;
let locale = loaded.get(code);
if (!locale) {
locale = DATE_FNS_LOCALE_LOADERS[code as LocaleCode]();
loaded.set(code, locale);
}
return locale;
}
/** `{ locale }` options for date-fns `format()` plus the translate function. */