Build-time tree-shaking for date-fns locales (#264)

Dynamically import date-fns locales for turbopack (local) and webpack (production)
This commit is contained in:
Sam Becker 2025-06-02 09:50:06 -05:00 committed by GitHub
parent d74ee39f11
commit 4946b0d262
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 56 additions and 61 deletions

View File

@ -1,6 +1,7 @@
import { removeUrlProtocol } from '@/utility/url';
import type { NextConfig } from 'next';
import { RemotePattern } from 'next/dist/shared/lib/image-config';
import path from 'path';
const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
@ -40,12 +41,28 @@ if (HOSTNAME_AWS_S3) {
remotePatterns.push(generateRemotePattern(HOSTNAME_AWS_S3));
}
const LOCALE = process.env.NEXT_PUBLIC_LOCALE || 'en-us';
const LOCALE_ALIAS = './date-fns-locale-alias';
const LOCALE_DYNAMIC = `i18n/locales/${LOCALE}`;
const nextConfig: NextConfig = {
images: {
imageSizes: [200],
remotePatterns,
minimumCacheTTL: 31536000,
},
turbopack: {
resolveAlias: {
[LOCALE_ALIAS]: `@/${LOCALE_DYNAMIC}`,
},
},
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
[LOCALE_ALIAS]: path.resolve(__dirname, `src/${LOCALE_DYNAMIC}`),
};
return config;
},
};
module.exports = process.env.ANALYZE === 'true'

2
pnpm-lock.yaml generated
View File

@ -8638,7 +8638,7 @@ snapshots:
postcss@8.4.31:
dependencies:
nanoid: 3.3.8
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1

View File

@ -0,0 +1,2 @@
// Dynamically resolves in next.config.ts
export { enUS as default } from 'date-fns/locale/en-US';

View File

@ -1,7 +1,7 @@
import EN_US from './locales/en-us';
import { TEXT as EN_US } from './locales/en-us';
import { setDefaultOptions } from 'date-fns';
import { enUS, id, ptBR, pt, zhCN } from 'date-fns/locale';
import { APP_LOCALE } from '@/app/config';
// Dynamically resolves in next.config.ts
import locale from './date-fns-locale-alias';
export type I18N = typeof EN_US;
@ -10,38 +10,29 @@ export type I18NDeepPartial = {
}
/**
* Translation steps for contributors:
* 1. Create new file in `src/i18n/locales` modeled on `en-us.ts`.
* 2. Add import to `localeTextImports`
* 3. Add date-fn locale to `getDateFnLocale`
* TRANSLATION STEPS FOR CONTRIBUTORS:
* 1. Create new file in `src/i18n/locales` modeled on `en-us.ts`
* MAKE SURE to export a default date-fns locale
* 3. Add import to `LOCALE_TEXT_IMPORTS`
* 4. Test locally
* 5. Add translation/credit to `README.md` Supported Languages
* 4. Add translation/credit to `README.md` Supported Languages
*/
const localeTextImports: Record<
const LOCALE_TEXT_IMPORTS: Record<
string,
() => Promise<I18NDeepPartial | undefined>
> = {
'pt-br': () => import('./locales/pt-br').then(m => m.default),
'pt-pt': () => import('./locales/pt-pt').then(m => m.default),
'id-id': () => import('./locales/id-id').then(m => m.default),
'zh-cn': () => import('./locales/zh-cn').then(m => m.default),
};
const getDateFnLocale = (locale: string) => {
switch (locale) {
case 'id-id': return id;
case 'pt-pt': return pt;
case 'pt-br': return ptBR;
case 'zh-cn': return zhCN;
default: return enUS;
}
'pt-br': () => import('./locales/pt-br').then(m => m.TEXT),
'pt-pt': () => import('./locales/pt-pt').then(m => m.TEXT),
'id-id': () => import('./locales/id-id').then(m => m.TEXT),
'zh-cn': () => import('./locales/zh-cn').then(m => m.TEXT),
};
export const getTextForLocale = async (locale: string): Promise<I18N> => {
const text = EN_US;
Object.entries(await localeTextImports[locale.toLocaleLowerCase()]?.() ?? {})
Object.entries(
await LOCALE_TEXT_IMPORTS[locale.toLocaleLowerCase()]?.() ?? {},
)
.forEach(([key, value]) => {
// Fall back to English for missing keys
text[key as keyof I18N] = {
@ -53,5 +44,4 @@ export const getTextForLocale = async (locale: string): Promise<I18N> => {
return text;
};
export const setDefaultDateFnLocale = () =>
setDefaultOptions({ locale: getDateFnLocale(APP_LOCALE) });
export const setDefaultDateFnLocale = () => setDefaultOptions({ locale });

View File

@ -1,4 +1,6 @@
const TEXT = {
export { enUS as default } from 'date-fns/locale/en-US';
export const TEXT = {
photo: {
photo: 'Photo',
photoPlural: 'Photos',
@ -114,5 +116,3 @@ const TEXT = {
paginateAction: '{{action}} {{index}} of {{count}}',
},
};
export default TEXT;

View File

@ -1,6 +1,7 @@
import { I18NDeepPartial } from '..';
export { id as default } from 'date-fns/locale/id';
const TEXT: I18NDeepPartial = {
export const TEXT: I18NDeepPartial = {
photo: {
photo: 'Foto',
photoPlural: 'Foto',
@ -115,5 +116,3 @@ const TEXT: I18NDeepPartial = {
paginateAction: '{{action}} {{index}} dari {{count}}',
},
};
export default TEXT;

View File

@ -1,6 +1,7 @@
import { I18NDeepPartial } from '..';
export { ptBR as default } from 'date-fns/locale/pt-BR';
const TEXT: I18NDeepPartial = {
export const TEXT: I18NDeepPartial = {
photo: {
photo: 'Foto',
photoPlural: 'Fotos',
@ -116,5 +117,3 @@ const TEXT: I18NDeepPartial = {
paginateAction: '{{action}} {{index}} de {{count}}',
},
};
export default TEXT;

View File

@ -1,6 +1,7 @@
import { I18NDeepPartial } from '..';
export { pt as default } from 'date-fns/locale/pt';
const TEXT: I18NDeepPartial = {
export const TEXT: I18NDeepPartial = {
photo: {
photo: 'Fotografia',
photoPlural: 'Fotografias',
@ -116,5 +117,3 @@ const TEXT: I18NDeepPartial = {
paginateAction: '{{action}} {{index}} de {{count}}',
},
};
export default TEXT;

View File

@ -1,4 +1,7 @@
const TEXT = {
import { I18NDeepPartial } from '..';
export { zhCN as default } from 'date-fns/locale/zh-CN';
export const TEXT: I18NDeepPartial = {
photo: {
photo: '照片',
photoPlural: '照片',
@ -113,5 +116,3 @@ const TEXT = {
paginateAction: '{{action}} 第 {{index}} 页,共 {{count}} 页',
},
};
export default TEXT;

View File

@ -2,7 +2,7 @@
import { createContext, use } from 'react';
import { generateAppTextState } from '.';
import EN_US from '../locales/en-us';
import { TEXT as EN_US } from '../locales/en-us';
export const AppTextContext = createContext(generateAppTextState(EN_US));

View File

@ -1,10 +1,7 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "ES2019",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -22,18 +19,9 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"],
},
"target": "ES2019"
},
"include": [
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}