guides
i18n in the App Router with next-intl
Locale routing, server translations, and the gotchas that bite every team once.
Next.js 13 dropped the old i18n config, and the App Router replacement is next-intl. It works well. The rough edges are all in the first hour.
Middleware is now proxy.ts#
Next 16 renamed middleware. next-intl's guide still says middleware.ts. Your file must be src/proxy.ts — createMiddleware(routing) is the same; the filename is the only change.
setRequestLocale on every page#
Skip it and your page falls back to dynamic rendering — silently. The layout setting it does not cover children; every page under [locale] must call setRequestLocale(locale) before any getTranslations call.
Keep navigation imports clean#
Always import { Link, redirect, useRouter, usePathname } from "@/i18n/navigation". Never from next/link or next/navigation (with notFound and useSearchParams as the only exceptions). Otherwise locale prefixes silently disappear at runtime.
Diacritics matter#
ASCII-stripped Portuguese or Spanish makes native speakers wince. sessao is wrong. sessão is right. Proofreading translated strings is a release-blocker step, not a nice-to-have.
Flat vs nested error codes#
Error codes use dot notation (auth.invalid_credentials), but next-intl requires them as nested objects in JSON. "auth.invalid_credentials": "..." at the top level will crash at runtime with a missing-key error. Keep the structure nested.
check:i18n in CI#
Write a script that reads each locale file and asserts the same key set as en.json. Drift between locales breaks silently; a boolean check in CI catches it before merge.