
The Complete Guide to Next.js Internationalization
Set up next-intl with the App Router, configure locale routing, and automate translations with AI.
Install next-intl
next-intl is a single package that handles locale routing, message loading, and translation hooks for the Next.js App Router.
npm install next-intlCreate the i18n Request Config
Create two files: src/i18n/request.ts for message loading and src/i18n/routing.ts for locale definitions. These configure how next-intl resolves messages and routes.
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
export const routing = defineRouting({
locales: ['en', 'de', 'ja', 'es'],
defaultLocale: 'en',
localePrefix: 'as-needed', // /about for en, /de/about for de
});
export const { Link, redirect, usePathname, useRouter } =
createNavigation(routing);Configure Middleware
Add middleware.ts to handle locale detection, URL rewriting, and redirects. The middleware intercepts every request and ensures the correct locale is applied.
// middleware.ts <- Must be in project ROOT, not src/
import createMiddleware from 'next-intl/middleware';
import { routing } from './src/i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)'],
};Set Up the [locale] Folder Structure
Move your app routes inside app/[locale]/. Add generateStaticParams to generate pages for each locale at build time. This creates the URL structure /en/about, /de/about, etc.
// app/[locale]/layout.tsx
import { routing } from '@/i18n/routing';
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}Update Your Root Layout
Load messages with getMessages() and pass them to NextIntlClientProvider in your root locale layout. Set the html lang attribute from the locale parameter.
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, setRequestLocale } from 'next-intl/server';
import { routing } from '@/i18n/routing';
import { notFound } from 'next/navigation';
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const { locale } = await params;
if (!routing.locales.includes(locale as any)) notFound();
setRequestLocale(locale);
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}Use Translations in Components
Server components use getTranslations (async, await). Client components use useTranslations (hook). Choose based on where your component renders — server components keep translations out of the JavaScript bundle entirely.
// Server Component (default)
import { getTranslations, setRequestLocale } from 'next-intl/server';
export default async function AboutPage({
params,
}: { params: { locale: string } }) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations('AboutPage');
return <h1>{t('title')}</h1>;
}
// Client Component ('use client')
'use client';
import { useTranslations } from 'next-intl';
export default function SearchBar() {
const t = useTranslations('SearchBar');
return <input placeholder={t('placeholder')} />;
}Add SEO: Metadata and Hreflang
Use generateMetadata to produce locale-specific page titles and descriptions. Add alternates.languages for hreflang tags so search engines discover all language versions of every page.
// app/[locale]/layout.tsx or any page.tsx
import { getTranslations } from 'next-intl/server';
import { routing } from '@/i18n/routing';
export async function generateMetadata({
params,
}: { params: { locale: string } }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
return {
title: t('title'),
description: t('description'),
alternates: {
languages: Object.fromEntries(
routing.locales.map((l) => [l, `/${l}`])
),
},
};
}Handle Error and Not-Found Pages
error.tsx and not-found.tsx need special handling because they can render outside the normal locale layout. The root not-found.tsx requires its own i18n provider setup to display localized error messages.
// app/[locale]/error.tsx
'use client';
import { useTranslations } from 'next-intl';
export default function Error() {
const t = useTranslations('Error');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
// app/not-found.tsx (root level -- needs own provider)
import { routing } from '@/i18n/routing';
export default async function GlobalNotFound() {
return (
<html lang={routing.defaultLocale}>
<body>
<h1>404 - Page Not Found</h1>
</body>
</html>
);
}Automate Translations
With your i18n setup complete, translate your message files using AI directly from your IDE, or use the i18n Agent CLI in your CI/CD pipeline for automated translation on every deploy.
# In your IDE, ask your AI assistant:
> Translate messages/en.json to German, Japanese, and Spanish
✓ messages/de.json created (1.1s)
✓ messages/ja.json created (1.4s)
✓ messages/es.json created (1.0s)Automate Translation Quality
Open-Source Tools for Next.js i18n
These open-source packages solve common pain points in Next.js internationalization workflows.
next-intl-localechain
Standard next-intl falls back directly to your default locale when a translation is missing. A Brazilian Portuguese user sees English instead of perfectly good pt-PT translations. next-intl-localechain adds intelligent fallback chains — it deep-merges translations from related locales so regional users always see the closest available translation.
import { getRequestConfig } from 'next-intl/server';
import { withLocaleChain } from 'next-intl-localechain';
export default getRequestConfig(withLocaleChain({
loadMessages: (locale) =>
import(`../../messages/${locale}.json`).then(m => m.default),
defaultLocale: 'en'
}));@i18n-agent/cli
A command-line tool for translating your Next.js message files without leaving the terminal. Translate files directly, check job status, and download results. Works in CI/CD pipelines with API key authentication for fully automated localization workflows.
# Install the CLI
npm install -g @i18n-agent/cli
# Authenticate
i18nagent login
# Translate your message files
i18nagent translate ./messages/en.json --lang de,ja,es
# Or use in CI/CD with an API key
export I18N_AGENT_API_KEY=your-key-here
i18nagent translate ./messages/en.json --lang de,ja,esCommon Pitfalls
"Unable to find next-intl locale"
The middleware didn't match the request. Check: is middleware.ts in the project root? Does the matcher pattern exclude static files correctly? Is the locale included in your routing config?
Unexpected Dynamic Rendering
setRequestLocale(locale) is missing from a page or layout. Without it, next-intl uses headers/cookies to detect the locale, which forces dynamic rendering and prevents static generation.
Parallel Routes Break with i18n
Parallel routes (@modal) and intercepting routes ((.)photo) have known incompatibilities with the [locale] dynamic segment. Use middleware-based routing as a workaround for these advanced routing patterns.
Language Switching Loses Current Route
When switching locales, preserve the current pathname using usePathname() and replace only the locale segment. Be careful with dynamic route parameters — they need to be re-resolved for the new locale.
Recommended File Structure
my-nextjs-app/
├── middleware.ts # Locale routing (project root!)
├── next.config.mjs
├── messages/
│ ├── en.json # Source messages
│ ├── de.json
│ └── ja.json
├── src/
│ ├── i18n/
│ │ ├── request.ts # Message loading config
│ │ └── routing.ts # Locale definitions
│ └── app/
│ └── [locale]/
│ ├── layout.tsx # Root locale layout
│ ├── page.tsx # Home page
│ ├── error.tsx # Localized error page
│ ├── not-found.tsx # Localized 404
│ └── about/
│ └── page.tsx
└── package.jsonTry i18n Agent Now
Drop your translation file here
JSON, YAML, PO, XML, CSV, Markdown, Properties
or click to browse
Target languages
Locale Fallback with next-intl-localechain
When a translation key is missing in a regional locale like pt-BR, next-intl jumps straight to the default locale instead of checking the parent locale pt first.
npm install next-intl-localechainimport '{ withLocaleChain }' from ''next-intl-localechain'';
export default withLocaleChain('{')
fallbacks: '{'
''pt-BR'': [''pt'', ''en''],
''zh-Hant-HK'': [''zh-Hant'', ''zh'', ''en''],
'}',
defaultLocale: ''en'',
loadMessages: (locale) => import(`./messages/$'{locale}'.json`),
'}')See our Locale Fallback Guide for the full list of supported frameworks and 75 built-in chains. Learn more →