Vite + React Router
Vite's import.meta.glob makes per-locale code-splits a one-liner.
Discovering locale bundlesβ
src/i18n/load.ts
// Each JSON becomes its own chunk; the import is async by default.
const loaders = import.meta.glob<{ default: Record<string, unknown> }>(
'./locales/*.json',
);
export async function loadLocale(locale: string) {
const loader = loaders[`./locales/${locale}.json`];
if (!loader) throw new Error(`Unknown locale: ${locale}`);
const mod = await loader();
return mod.default;
}
Suspense providerβ
src/i18n/Provider.tsx
import { LocalizationProvider } from 'localize-react';
import { use, type ReactNode } from 'react';
import { loadLocale } from './load';
const cache = new Map<string, Promise<Record<string, unknown>>>();
function get(locale: string) {
let p = cache.get(locale);
if (!p) {
p = loadLocale(locale);
cache.set(locale, p);
}
return p;
}
export function I18nProvider({
locale,
children,
}: {
locale: string;
children: ReactNode;
}) {
const tree = use(get(locale));
return (
<LocalizationProvider locale={locale} translations={{ [locale]: tree }}>
{children}
</LocalizationProvider>
);
}
Router wiringβ
src/main.tsx
import {
createBrowserRouter,
RouterProvider,
Outlet,
useParams,
} from 'react-router-dom';
import { Suspense } from 'react';
import { I18nProvider } from './i18n/Provider';
import { Home } from './pages/Home';
function LocaleShell() {
const { locale = 'en' } = useParams();
return (
<Suspense fallback="Loadingβ¦">
<I18nProvider locale={locale}>
<Outlet />
</I18nProvider>
</Suspense>
);
}
const router = createBrowserRouter([
{
path: '/:locale',
element: <LocaleShell />,
children: [{ index: true, element: <Home /> }],
},
]);
createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />,
);
Resultβ
- Visiting
/enloads onlyen.json. - Switching to
/frtriggers a single network request forfr.json, then hydrates. - The main bundle stays tiny β only
localize-reactplus your app code.
Static-import alternative (no Suspense)β
If your translation files are small, just import them statically and skip lazy-loading entirely:
import en from './locales/en.json';
import es from './locales/es.json';
import fr from './locales/fr.json';
export const translations = { en, es, fr };
Less moving parts, instant locale switches, slightly larger initial bundle.