Next.js lets you configure routing and rendering to support multiple languages. This covers:
- Internationalized routing — Routing based on locale (sub-path or domain)
- Localization — Translating content based on the user’s preferred locale
Terminology
- Locale — An identifier for a set of language and formatting preferences:
en-US — English as spoken in the United States
nl-NL — Dutch as spoken in the Netherlands
nl — Dutch, no specific region
Routing overview
Detect the user’s preferred locale using the Accept-Language request header and redirect to the appropriate locale path.
Detect and redirect
Use libraries like @formatjs/intl-localematcher and negotiator to match the best locale:
npm install @formatjs/intl-localematcher negotiator
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import { NextResponse } from 'next/server'
let locales = ['en-US', 'nl-NL', 'nl']
let defaultLocale = 'en-US'
function getLocale(request) {
const headers = { 'accept-language': request.headers.get('accept-language') ?? '' }
const languages = new Negotiator({ headers }).languages()
return match(languages, locales, defaultLocale)
}
export function middleware(request) {
const { pathname } = request.nextUrl
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return
const locale = getLocale(request)
request.nextUrl.pathname = `/${locale}${pathname}`
// e.g. /products -> /en-US/products
return NextResponse.redirect(request.nextUrl)
}
export const config = {
matcher: ['/((?!_next).*)'],
}
Dynamic locale segment
Nest all pages under app/[lang]/ so the router passes the locale to every layout and page:
export default async function Page({ params }: PageProps<'/[lang]'>) {
const { lang } = await params
return <h1>Hello from {lang}</h1>
}
The root layout can also use the locale:
export default async function RootLayout({ children, params }: LayoutProps<'/[lang]'>) {
return (
<html lang={(await params).lang}>
<body>{children}</body>
</html>
)
}
Localization
Translate content using dictionary files. Maintain separate JSON files per locale:
{
"products": {
"cart": "Add to Cart"
}
}
Create a getDictionary function to load translations:
import 'server-only'
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}
export type Locale = keyof typeof dictionaries
export const hasLocale = (locale: string): locale is Locale =>
locale in dictionaries
export const getDictionary = async (locale: Locale) => dictionaries[locale]()
Fetch the dictionary in your page and render translated content:
import { notFound } from 'next/navigation'
import { getDictionary, hasLocale } from './dictionaries'
export default async function Page({ params }: PageProps<'/[lang]'>) {
const { lang } = await params
if (!hasLocale(lang)) notFound()
const dict = await getDictionary(lang)
return <button>{dict.products.cart}</button>
}
Because all layouts and pages in app/ default to Server Components, translation files do not increase the client-side JavaScript bundle size.
Static rendering
Generate static routes for all locales using generateStaticParams:
export async function generateStaticParams() {
return [{ lang: 'en-US' }, { lang: 'de' }]
}
Libraries
For more advanced use cases, these libraries provide additional features like pluralization, date/number formatting, and React hooks:
next-intl
Full-featured i18n with server and client support.
next-international
Type-safe i18n for App Router.
paraglide-next
Compile-time i18n with zero runtime overhead.
lingui
Comprehensive i18n with message extraction.