Capítulo 49 de 456

Internationalization

Core Idea

Next.js supports internationalized routing (sub-path or domain based) and localized content, letting you route requests by locale and load translated dictionaries per request.

Key Concepts

  • Locale: identifier for language + formatting preferences (e.g. en-US, nl-NL, nl).
  • Sub-path routing: locale encoded in the path, e.g. /fr/products.
  • Domain routing: locale encoded in the domain, e.g. my-site.fr/products.
  • app/[lang] segment: all special files nested under a dynamic [lang] segment so the router forwards lang to every layout/page (including the root layout).
  • Dictionaries: plain objects mapping keys to localized strings, loaded per-locale via dynamic import().
  • next/root-params: exports a getter per dynamic segment above the root layout (e.g. lang()), letting server code read the locale without prop drilling; works in Server Components and server-side utilities only, not Client Components, Server Actions, or Route Handlers.
  • generateStaticParams: used on a layout/page (commonly the root layout) to statically generate routes for a set of locales.

Code Examples

// proxy.js - detect locale from Accept-Language and redirect
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import { NextResponse } from "next/server";

let locales = ['en-US', 'nl-NL', 'nl']
function getLocale(request) { /* use match()/Negotiator */ }

export function proxy(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}`
  return NextResponse.redirect(request.nextUrl)
}

export const config = {
  matcher: ['/((?!_next).*)'],
}
  • O que demonstra: proxy detecta locale ausente na URL e redireciona para a versão prefixada.
// app/[lang]/dictionaries.ts
import 'server-only'

const dictionaries = {
  en: () => import('./dictionaries/en.json').then((m) => m.default),
  nl: () => import('./dictionaries/nl.json').then((m) => m.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]()
  • O que demonstra: dicionários carregados sob demanda por locale, sem afetar o bundle client-side (roda só no server).
// app/[lang]/dictionaries.ts com next/root-params
import { lang } from 'next/root-params'
import { notFound } from 'next/navigation'

export const getDictionary = async () => {
  const locale = await lang()
  if (!hasLocale(locale)) notFound()
  return dictionaries[locale]()
}
  • O que demonstra: next/root-params elimina a necessidade de passar lang manualmente por camadas de componentes.

Anti-patterns

  • Prop drilling de lang: passar lang manualmente por várias camadas quando next/root-params resolve isso diretamente em qualquer Server Component.
  • Não validar o locale: ler lang sem checar hasLocale pode causar erro de runtime em vez de um 404 controlado.

Key Takeaways

  1. Use o header Accept-Language (via negotiator + @formatjs/intl-localematcher) no proxy para decidir o locale preferido.
  2. Estruture todas as rotas sob app/[lang] para o router forwardar o parâmetro automaticamente.
  3. Dicionários carregados via import() dinâmico mantêm o bundle do cliente enxuto, pois rodam só no servidor.
  4. next/root-params evita prop drilling, mas só funciona em Server Components e utilitários server-side.
  5. generateStaticParams no layout raiz gera rotas estáticas por locale.

Connects To

  • next/root-params (API reference): getter de parâmetros de rota em nível raiz, base desta página.
  • Proxy (file convention): mecanismo usado para redirecionamento baseado em locale.