From 5bcfe1fe4d08059aeef88551662c0f808b7693f0 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:17:14 +0200 Subject: [PATCH] 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/` 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) --- src/lib/__tests__/calendar-locale.test.ts | 30 ++++++++++++++ src/lib/calendar-locale.ts | 49 ++++++++++++++++++++--- 2 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 src/lib/__tests__/calendar-locale.test.ts diff --git a/src/lib/__tests__/calendar-locale.test.ts b/src/lib/__tests__/calendar-locale.test.ts new file mode 100644 index 00000000..953606aa --- /dev/null +++ b/src/lib/__tests__/calendar-locale.test.ts @@ -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 = { + 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'); + }); +}); diff --git a/src/lib/calendar-locale.ts b/src/lib/calendar-locale.ts index 78dac18b..c1b4d52d 100644 --- a/src/lib/calendar-locale.ts +++ b/src/lib/calendar-locale.ts @@ -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 = { - 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 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(); + 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. */