app/Next.js internationalization (i18n) lets you serve localized pages, URLs, and SEO metadata for different languages and regions. In the App Router, you implement localization cleanly using a dynamic [lang] folder segment, JSON dictionaries, and root middleware.
Keeping translation files on the server inside Server Components avoids shipping unused language strings to the visitor's browser.
[lang] RoutesWrap your application pages inside an app/[lang]/ folder so URLs automatically include the locale prefix, such as /en/pricing and /es/pricing.
app/
└── [lang]/
├── layout.tsx
├── page.tsx
└── pricing/
└── page.tsx
dictionaries/
├── en.json
└── es.json
Define typed dictionary loaders and await only the dictionary matching the active lang parameter.
{
"heroTitle": "Build faster web apps",
"ctaLabel": "Get Started"
}
// app/[lang]/page.tsx
const dictionaries = {
en: () => import('@/dictionaries/en.json').then((m) => m.default),
es: () => import('@/dictionaries/es.json').then((m) => m.default),
};
type Locale = keyof typeof dictionaries;
export default async function LocalizedHome({
params,
}: {
params: Promise<{ lang: Locale }>;
}) {
const { lang } = await params;
const dict = await dictionaries[lang]();
return (
<main className="p-8">
<h2 className="text-3xl font-bold">{dict.heroTitle}</h2>
<button type="button" className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">
{dict.ctaLabel}
</button>
</main>
);
}
Inspect incoming requests in middleware.ts and redirect root paths without a locale prefix to your default language.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['en', 'es'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const hasLocale = locales.some(
(loc) => pathname === `/${loc}` || pathname.startsWith(`/${loc}/`)
);
if (hasLocale) {
return NextResponse.next();
}
request.nextUrl.pathname = `/${defaultLocale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ['/((?!_next|api|favicon.ico).*)'],
};
<html lang="en"> in root layout: Pass the active lang param into app/[lang]/layout.tsx so screen readers and search engines see the right language code._next, api, and image paths in config.matcher so /logo.svg does not redirect to /en/logo.svg./en/about) or domain routing?Subpath routing is easiest to manage in one App Router deployment and works well with Google search indexing.
Export generateStaticParams in app/[lang]/layout.tsx returning [{ lang: 'en' }, { lang: 'es' }].
next-intl with the Next.js App Router?Yes, next-intl builds on top of the [lang] pattern and adds pluralization, date formatting, and typed translation hooks.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With