Next.js localization guide
Next.js localization for the App Router and Pages Router
Build locale-aware routes, load messages efficiently in Server and Client Components, compare next-intl with next-i18next, and publish crawlable localized pages with correct metadata.
The foundation of a localized Next.js application
Next.js is a strong foundation for multilingual sites because it can render useful HTML before the browser runs application JavaScript. Static generation works well for product pages and documentation; dynamic rendering can serve account-specific or frequently changing content. Neither rendering mode creates a localization strategy by itself. You still need locale-aware routes, catalogs, formatting rules, and a translation workflow.
The first architectural choice is the router. The App Router uses layouts, Server Components, and route segments. Its official localization pattern places the language in a top-level dynamic segment such as app/[lang]. The Pages Router has a separate i18n configuration that can add sub-path or domain routing and expose locale data to pages. These systems should not be described as interchangeable.
Make locale a validated application concept instead of passing arbitrary URL text into catalog imports. Keep one supported-locale list with the locale code, display name, text direction, and fallback. Use that list to generate routes, build a language switcher, select catalogs, and emit metadata. A request for an unsupported locale should resolve predictably to a not-found response or an intentional fallback.
- A stable URL for every locale and content page
- A catalog-loading boundary that works on the server
- A deliberate policy for detection, fallback, and switching
- Localized metadata and alternate-page signals for crawlers
Choose a locale routing strategy
Sub-path routing gives every language a visible URL, such as /en/pricing and /fr/pricing. It works on one domain, is easy to test, and makes language switching a path transformation. Domain routing, such as example.fr, can support market-specific branding or legal requirements, but adds DNS, deployment, analytics, and canonicalization work. Choose domains for product reasons, not because localization requires them.
In the App Router, put all localized pages under app/[locale]. A Proxy can inspect the pathname, cookie, or Accept-Language header at request time and redirect a neutral entry URL to a locale. Next.js 16 renamed middleware.ts to proxy.ts, although older applications still use the middleware name. Treat negotiation as a convenience: every localized URL must remain directly accessible and the switcher should use ordinary crawlable links.
A pure static export cannot execute Proxy, read request cookies, or perform request-time redirects. For output: 'export', enumerate locale segments with generateStaticParams and provide a static entry experience, such as a default-locale homepage or a visible language chooser. Do not promise automatic browser-language detection in an environment that has no request runtime.
The Pages Router's next.config.js i18n option supports locale sub-paths and domains in deployed Next.js applications. It is not compatible with output: 'export'. If a Pages Router project must produce only static files, model locale as an explicit dynamic segment and generate those routes rather than enabling the built-in i18n field. Static generation and static export are related, but they are not the same deployment mode.
app/
[locale]/
layout.tsx
page.tsx
pricing/
page.tsx
messages/
en.json
fr.json
de.json next-intl vs next-i18next vs custom dictionaries
A library should reduce repeated infrastructure without hiding how locale URLs and rendering work. Compare support for your router, server and client message loading, ICU or i18next syntax, static generation, routing helpers, and extraction tooling. Avoid selecting only from a starter snippet: localization touches navigation, metadata, errors, forms, and background content throughout the application.
next-intl and next-i18next both support the App Router and Pages Router today, but they retain different mental models. next-intl centers ICU messages and Next.js-aware routing. next-i18next brings the i18next ecosystem, including namespaces, detectors, loaders, and familiar react-i18next-style APIs. A custom dictionary approach has the smallest abstraction surface, but transfers more responsibility to your codebase.
| Approach | Routing | Message model | Server Components | Static generation | Best fit |
|---|---|---|---|---|---|
| next-intl | App Router preferred; Pages supported | ICU messages and formatters | Server and Client Component APIs | Supported with static locale params | Next.js-first applications using ICU |
| next-i18next | App Router and Pages Router | i18next keys, namespaces, optional ICU | Server getT and client useT APIs | Supported with explicit locale generation | Teams invested in i18next |
| Next.js + custom dictionaries | Explicit [locale] routes | Your chosen JSON or module shape | Natural Server Component loading | Yes, when routes are enumerable | Small sites with simple messages |
For a new App Router application with rich messages and localized navigation, next-intl is a focused default. For an existing i18next organization or an application shared with other React surfaces, next-i18next can preserve catalog and plugin conventions. Recheck version-specific setup before migrating: both libraries have evolved substantially as Next.js has changed its server APIs.
The framework-only option is reasonable when the catalog is small, messages are simple, and the team accepts building its own validation and extraction. Loading a dictionary in a Server Component keeps its contents off the client, but routing primitives do not provide plural syntax, translator context, catalog linting, or review workflows. Add those capabilities only when the product needs them, while keeping catalog access behind a small interface.
A production-ready App Router setup
The following sequence uses an App Router application because it exposes the decisions most new projects face. The exact imports vary by library, but the boundaries remain stable: validate locale at the route, load server messages for that request, provide only what Client Components need, and generate known locales during the build when the page can be static.
-
Step 1
Add the locale route segment
Create app/[locale] and move localized layouts and pages beneath it. Validate params against one supported-locale list and return not found for unknown values.
-
Step 2
Define negotiation and fallback
Decide what happens at a URL without a locale. A request runtime can negotiate and redirect; a static export needs a deterministic default page or language chooser. Always preserve direct access to locale URLs.
-
Step 3
Create scoped message catalogs
Organize messages by product area or route rather than placing the whole application in one file. Keep the same namespace shape across locales, validate missing keys in CI, and avoid importing all languages into a Client Component.
-
Step 4
Load messages on the server
Resolve one validated catalog for each request or generated route. Keep server instances request-scoped when the library stores active-locale state, preventing concurrent requests from leaking language choices.
-
Step 5
Separate server and client translation
Translate in Server Components by default. Add a provider or client hook only across the smallest interactive boundary that needs it, and pass translated labels as props when that is simpler.
-
Step 6
Generate locale variants
Return every supported locale from generateStaticParams for static pages. Dynamic content may also need slug-and-locale combinations. Rebuild whenever catalogs or the locale list changes.
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
export default async function LocaleLayout({children, params}: Props) {
const {locale} = await params;
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
} const locales = ['en', 'fr', 'de'] as const;
export function generateStaticParams() {
return locales.map((locale) => ({locale}));
} Keep the root locale layout responsible for language-wide concerns: validating params, loading the catalog, setting html lang and dir, and establishing any client provider. Pages should request messages by namespace and avoid knowing where catalogs live. That separation makes it possible to replace local JSON imports with a build-time download or CDN-backed workflow without rewriting every component.
For the Pages Router, translate the same responsibilities into next.config.js locale routing, getStaticProps or getServerSideProps message loading, and getStaticPaths for dynamic localized pages. The router APIs differ, but the catalog and component layers can remain consistent. During a migration, support both routers deliberately and verify that navigation never drops the locale prefix.
Formatting, plurals, and right-to-left layouts
Formatting belongs next to translation, but it is not the same task. Messages explain what a value means; locale-aware formatters decide how a date, number, currency, relative time, or list appears. next-intl and i18next integrations expose formatting helpers, while a custom dictionary layer can call the platform Intl APIs directly. Keep raw dates and numbers as data until the rendering boundary so one source value can be presented correctly in every locale.
Plural rules also belong to the target language, not to an English singular-versus-plural condition. ICU messages can select categories such as one, few, many, and other. i18next maps count values through Intl.PluralRules. In both cases, pass the numeric count into the message and let the library select the grammatical form. Do not concatenate a translated noun onto a formatted number.
Set the html lang attribute from the validated route locale and set dir to rtl for right-to-left scripts. Language does not automatically establish direction in HTML, so make direction an explicit property of your locale configuration. Mirror directional spacing and icons where meaning depends on direction, and use logical CSS properties so layouts adapt without a second stylesheet.
export async function generateMetadata({params}: Props) {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
return {
title: t('title'),
description: t('description'),
alternates: {
canonical: `https://example.com/${locale}`,
languages: {en: '/en', fr: '/fr', de: '/de'}
}
};
} Multilingual SEO for Next.js
Rendered HTML is only the start of multilingual SEO. Search engines need a distinct, stable URL for each language version and clear links among alternatives. In the App Router, generateMetadata can produce localized titles and descriptions plus alternates.canonical and alternates.languages. A sitemap can also list language alternates. Use one consistent URL policy so metadata, navigation, and sitemap entries never disagree.
Each localized page should reference itself and every equivalent locale with reciprocal hreflang annotations. Use absolute URLs in production and add x-default when a neutral selector or default experience is useful. A canonical usually points to the current language version, not automatically to English. When a page is unavailable in a locale, omit that unsupported alternative rather than sending crawlers to unrelated content.
Do not depend on Accept-Language alone. Googlebot commonly crawls without a useful language preference, and locale-adaptive content at one URL can hide variants. Keep locale pages reachable through normal anchors. Set html lang for browsers and assistive technology, but remember that Google determines language mainly from visible content, so the translation itself must be substantial and coherent.
- Return complete localized HTML at a stable, crawlable URL.
- Localize the title, description, headings, and visible body—not only the navigation shell.
- Emit a canonical for the current page and reciprocal hreflang links for every available locale.
- Include localized URLs in the sitemap and use x-default for a neutral fallback when appropriate.
- Keep a crawlable language switcher and avoid forced redirects that trap users or bots in one locale.
Keep localized pages fast
Server Components create an important performance opportunity: catalogs loaded and consumed exclusively on the server do not need to become client JavaScript. Keep page-level headings, navigation labels, and mostly static prose on the server. When an interactive Client Component needs messages, provide only the relevant namespace or translated props instead of serializing the entire locale catalog into every page.
Static generation exchanges request-time work for build-time work. Every locale multiplies the number of page variants, so measure build duration and output size as the site expands. Generate high-value pages ahead of time, split catalogs by route or domain, and avoid importing every language from a shared client module. The goal is localized HTML without making each visitor download content for languages they never selected.
- Load only the active locale and the namespaces required by the route.
- Format and translate in Server Components unless browser interactivity requires a client boundary.
- Generate every supported locale explicitly and monitor the build-time cost as the route set grows.
- Keep locale configuration deterministic so the same URL produces the same static output.
Where Translatable fits
A Next.js library decides how the application selects and renders messages. A localization platform addresses the surrounding lifecycle: collecting keys, translating values, reviewing changes, keeping locale catalogs aligned, and delivering approved content. Those concerns become visible as soon as copy changes more often than deployments or contributors extend beyond one developer.
Translatable is built around JSON localization files. Teams can import existing catalogs, edit or translate them with AI assistance, synchronize through Git providers, and distribute updates through a CDN. The application keeps ownership of its routing and i18n library, so message delivery can evolve without tying view components to a proprietary runtime.
Start with checked-in catalogs and a build-time workflow. Introduce remote delivery only where faster copy updates create real value, and preserve a bundled fallback for critical pages. This progressive path keeps static rendering deterministic while giving content teams a cleaner route to production when the product grows.
Frequently asked questions
Which is better for i18n: App Router or Pages Router?
For new applications, the App Router is generally the better starting point because Server Components can load messages without shipping entire catalogs to the browser. The Pages Router remains valid for established applications and includes next.config.js locale routing. Pick the router that matches the application first, then select an i18n library that supports it.
Should I use next-intl or next-i18next?
Choose next-intl when you want an ICU-oriented API and deeply integrated locale routing. Choose next-i18next when your team prefers the i18next ecosystem, namespaces, and plugins; current versions support both routers. A framework-only dictionary approach suits small projects with simple messages, but you must supply formatting, extraction, validation, and workflow tooling yourself.
Does the Next.js App Router have built-in i18n routing?
The App Router provides the routing and rendering primitives needed for localization, but it does not provide a complete message system. You create a locale segment such as app/[locale], load dictionaries or use a library, and implement negotiation if needed. The Pages Router separately offers built-in locale routing through the i18n field in next.config.js.
Can a localized Next.js site use static export?
Yes, when every localized route can be generated ahead of time. With the App Router, use an explicit [locale] segment and generateStaticParams. A pure static export cannot depend on Proxy, cookies, redirects, or request-time language detection. The Pages Router's built-in i18n routing is not compatible with output: 'export', so export projects need explicit locale routes instead.
Can I add Next.js locales without code changes?
Usually not completely. A new locale needs a catalog, a supported-locale configuration update, static parameters or routing configuration, localized metadata, and a rebuild. A translation platform can remove much of the manual catalog work, but the application still needs to recognize and publish the locale safely.
Build a localization workflow your team can maintain
Bring your JSON catalogs together, translate with AI assistance, and deliver updates through Git or a global CDN.
Get started free