i18n in the App Router with next-intl

guides

i18n in the App Router with next-intl

Locale routing, server translations, and the gotchas that bite every team once.

Alexandre Awadallak2 min read

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.tscreateMiddleware(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.