Next.js (App Router)
A pragmatic setup: [locale] URL segments, server-side detection, client-side provider.
File layout
app/
[locale]/
layout.tsx ← reads params.locale, mounts provider
page.tsx
i18n/
translations/
en.json
es.json
fr.json
index.ts
middleware.ts ← optional: redirect / to /en
i18n/index.ts
import en from './translations/en.json';
import es from './translations/es.json';
import fr from './translations/fr.json';
export const SUPPORTED = ['en', 'es', 'fr'] as const;
export type Locale = (typeof SUPPORTED)[number];
export const translations = { en, es, fr };
export function pickLocale(input: string | undefined): Locale {
return SUPPORTED.includes(input as Locale) ? (input as Locale) : 'en';
}
app/[locale]/layout.tsx — client provider
'use client';
import { LocalizationProvider } from 'localize-react';
import { type Locale, translations } from '@/i18n';
export default function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: Locale };
}) {
return (
<html lang={params.locale}>
<body>
<LocalizationProvider
locale={params.locale}
translations={translations}
>
{children}
</LocalizationProvider>
</body>
</html>
);
}
middleware.ts — redirect bare paths
import { NextResponse, type NextRequest } from 'next/server';
import { pickLocale, SUPPORTED } from '@/i18n';
export const config = { matcher: ['/((?!_next|api|.*\\..*).*)'] };
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
if (SUPPORTED.some((l) => pathname.startsWith(`/${l}`))) return;
const accept = req.headers.get('accept-language') ?? 'en';
const primary = accept.split(',')[0]?.split('-')[0];
const locale = pickLocale(primary);
const url = req.nextUrl.clone();
url.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(url);
}
Pure server-side translate (no client provider)
For static pages where you don't need a provider:
app/[locale]/page.tsx
import { translations, pickLocale } from '@/i18n';
export default async function Page({ params }: { params: { locale: string } }) {
const locale = pickLocale(params.locale);
const t = (key: string) =>
key
.split('.')
.reduce<unknown>(
(cursor, seg) =>
cursor && typeof cursor === 'object'
? (cursor as Record<string, unknown>)[seg]
: undefined,
translations[locale],
) as string | undefined;
return <h1>{t('greeting.hello') ?? 'Hello'}</h1>;
}
For richer needs, render <Message /> from a client subtree.
App Router caveats
LocalizationProvideris a client component. Mark the file'use client'once at the layout level; everything below it inherits the context.- Heavy translation maps don't need to be in client-bundle if your leaves are loaded on the server — pass them down as serializable props from a server component.