app/
[lang]/
layout.tsx // <html lang={lang} dir={...}>
page.tsx // const dict = await getDictionary(lang)
pricing/page.tsx
dictionaries/
en.json
ar.json
middleware.ts // proxy.ts in Next.js 16: redirect "/" to "/en" or "/ar"
The App Router has no built-in i18n setting. You build it with a dynamic route segment for the locale, plus middleware for the redirect. (The Pages Router had an i18n option in next.config.js, but the App Router doesn't use it.)
The steps:
- Route segment: put every page under
app/[lang]/. Each page readslangfrom itsparams, so/ar/pricinggetslang = "ar". - Redirect: in middleware (renamed
proxyin Next.js 16), check if the path already has a supported locale. If not, choose one from a cookie orAccept-Language, and redirect/pricingto/ar/pricing. Libraries likenegotiatorand@formatjs/intl-localematcherdo the matching. - Messages: load a dictionary per locale in a Server Component, with
await import("./dictionaries/ar.json"). Server Components send no translation JavaScript to the browser. Pass only the strings that Client Components need. - Layout: in
app/[lang]/layout.tsx, render<html lang={lang} dir={lang === "ar" ? "rtl" : "ltr"}>. - Static pages: return every locale from
generateStaticParams, so each language version is prerendered. - Metadata: translate titles in
generateMetadata, and addalternates.languagesforhreflanglinks.
Libraries like next-intl package all of this: middleware, message loading and formatting for both Server and Client Components.
- The App Router has no built-in i18n config, so the locale becomes a
[lang]route segment. - Middleware (proxy in Next.js 16) redirects paths without a locale, using a cookie or
Accept-Language. - Server Components load the locale's dictionary, so translations don't add client JavaScript.
- The root layout sets
langanddiron the<html>element. generateStaticParamsprerenders every locale.generateMetadatatranslates titles and addshreflangalternates.