Skip to main content
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.

Build docs developers (and LLMs) love