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 { removeUrlProtocol } from '@/utility/url';
import type { NextConfig } from 'next'; import type { NextConfig } from 'next';
import { RemotePattern } from 'next/dist/shared/lib/image-config'; 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( const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i, /^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
@ -40,12 +41,28 @@ if (HOSTNAME_AWS_S3) {
remotePatterns.push(generateRemotePattern(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 = { const nextConfig: NextConfig = {
images: { images: {
imageSizes: [200], imageSizes: [200],
remotePatterns, remotePatterns,
minimumCacheTTL: 31536000, 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' module.exports = process.env.ANALYZE === 'true'

2
pnpm-lock.yaml generated
View File

@ -8638,7 +8638,7 @@ snapshots:
postcss@8.4.31: postcss@8.4.31:
dependencies: dependencies:
nanoid: 3.3.8 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.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 { setDefaultOptions } from 'date-fns';
import { enUS, id, ptBR, pt, zhCN } from 'date-fns/locale'; // Dynamically resolves in next.config.ts
import { APP_LOCALE } from '@/app/config'; import locale from './date-fns-locale-alias';
export type I18N = typeof EN_US; export type I18N = typeof EN_US;
@ -10,38 +10,29 @@ export type I18NDeepPartial = {
} }
/** /**
* Translation steps for contributors: * TRANSLATION STEPS FOR CONTRIBUTORS:
* 1. Create new file in `src/i18n/locales` modeled on `en-us.ts`. * 1. Create new file in `src/i18n/locales` modeled on `en-us.ts`
* 2. Add import to `localeTextImports` * MAKE SURE to export a default date-fns locale
* 3. Add date-fn locale to `getDateFnLocale` * 3. Add import to `LOCALE_TEXT_IMPORTS`
* 4. Test locally * 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, string,
() => Promise<I18NDeepPartial | undefined> () => Promise<I18NDeepPartial | undefined>
> = { > = {
'pt-br': () => import('./locales/pt-br').then(m => m.default), 'pt-br': () => import('./locales/pt-br').then(m => m.TEXT),
'pt-pt': () => import('./locales/pt-pt').then(m => m.default), 'pt-pt': () => import('./locales/pt-pt').then(m => m.TEXT),
'id-id': () => import('./locales/id-id').then(m => m.default), 'id-id': () => import('./locales/id-id').then(m => m.TEXT),
'zh-cn': () => import('./locales/zh-cn').then(m => m.default), 'zh-cn': () => import('./locales/zh-cn').then(m => m.TEXT),
};
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;
}
}; };
export const getTextForLocale = async (locale: string): Promise<I18N> => { export const getTextForLocale = async (locale: string): Promise<I18N> => {
const text = EN_US; const text = EN_US;
Object.entries(
Object.entries(await localeTextImports[locale.toLocaleLowerCase()]?.() ?? {}) await LOCALE_TEXT_IMPORTS[locale.toLocaleLowerCase()]?.() ?? {},
)
.forEach(([key, value]) => { .forEach(([key, value]) => {
// Fall back to English for missing keys // Fall back to English for missing keys
text[key as keyof I18N] = { text[key as keyof I18N] = {
@ -53,5 +44,4 @@ export const getTextForLocale = async (locale: string): Promise<I18N> => {
return text; return text;
}; };
export const setDefaultDateFnLocale = () => export const setDefaultDateFnLocale = () => setDefaultOptions({ locale });
setDefaultOptions({ locale: getDateFnLocale(APP_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: 'Photo', photo: 'Photo',
photoPlural: 'Photos', photoPlural: 'Photos',
@ -114,5 +116,3 @@ const TEXT = {
paginateAction: '{{action}} {{index}} of {{count}}', paginateAction: '{{action}} {{index}} of {{count}}',
}, },
}; };
export default TEXT;

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { I18NDeepPartial } from '..'; import { I18NDeepPartial } from '..';
export { pt as default } from 'date-fns/locale/pt';
const TEXT: I18NDeepPartial = { export const TEXT: I18NDeepPartial = {
photo: { photo: {
photo: 'Fotografia', photo: 'Fotografia',
photoPlural: 'Fotografias', photoPlural: 'Fotografias',
@ -116,5 +117,3 @@ const TEXT: I18NDeepPartial = {
paginateAction: '{{action}} {{index}} de {{count}}', 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: {
photo: '照片', photo: '照片',
photoPlural: '照片', photoPlural: '照片',
@ -113,5 +116,3 @@ const TEXT = {
paginateAction: '{{action}} 第 {{index}} 页,共 {{count}} 页', paginateAction: '{{action}} 第 {{index}} 页,共 {{count}} 页',
}, },
}; };
export default TEXT;

View File

@ -2,7 +2,7 @@
import { createContext, use } from 'react'; import { createContext, use } from 'react';
import { generateAppTextState } from '.'; 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)); export const AppTextContext = createContext(generateAppTextState(EN_US));

View File

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