Skip to main content

The Complete Guide to Next.js Internationalization

Set up next-intl with the App Router, configure locale routing, and automate translations with AI.

1

Install next-intl

next-intl is a single package that handles locale routing, message loading, and translation hooks for the Next.js App Router.

Terminal
npm install next-intl
Why next-intl over next-i18next? next-intl is built for the App Router and server components. next-i18next was designed for the Pages Router and has limited App Router support.
2

Create 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
// 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);
Turbopack (default bundler in Next.js 15) requires experimental.turbo.resolveAlias in your next.config.js. Without it, you get "Couldn't find next-intl config file" errors.
3

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
// 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|.*\\..*).*)'],
};
middleware.ts MUST be in your project root directory, not inside src/. This is the single most common configuration mistake with next-intl.
4

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
// app/[locale]/layout.tsx
import { routing } from '@/i18n/routing';

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}
You MUST call setRequestLocale(locale) in every page.tsx and layout.tsx that uses translations. Without it, Next.js falls back to dynamic rendering and your build performance degrades significantly.
5

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
// 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>
  );
}
NextIntlClientProvider requires an explicit locale prop. Omitting it causes subtle errors in client components that are hard to debug.
6

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.

app/[locale]/page.tsx
// 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')} />;
}
Prefer server components for translated content. They keep translation strings out of your client JavaScript bundle, reducing load time for users.
7

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
// 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}`])
      ),
    },
  };
}
On Vercel, builds can generate localhost as the canonical URL if metadataBase is not set. Always set metadataBase in your root layout to your production domain.
8

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
// 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>
  );
}
Next.js only renders your localized 404 page when notFound() is called explicitly in your code. Unknown routes without a matching page show the default Next.js 404, not your localized version.
9

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.

Terminal
# 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)
Use next-intl-localechain for intelligent locale fallbacks — a pt-BR user sees pt-PT translations instead of falling back to English when Brazilian Portuguese isn't available.

Automate Translation Quality

Catch missing keys and broken placeholders before they ship with i18n-validate. Test your UI with fake translations using i18n-pseudo before real translations arrive.

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.

src/i18n/request.ts
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'
}));
Deep-merges translations across locale chains automatically
Built-in chains for Portuguese, Spanish, French, German, and more
Gracefully skips missing message files without errors
One-line setup — wraps your existing getRequestConfig
View on GitHub

@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.

Terminal
# 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,es
Translate JSON, YAML, PO, and other i18n file formats from the terminal
CI/CD ready — authenticate via environment variable for automated pipelines
Track job status, resume failed jobs, and download results
Machine-readable JSON output for scripting and automation
View on GitHub

Common 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

Project 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.json

Try i18n Agent Now

Drop your translation file here

JSON, YAML, PO, XML, CSV, Markdown, Properties

or click to browse

Target languages

No signup requiredInstant estimate

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.

Terminal
npm install next-intl-localechain
Configuration
import '{ 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 →

Frequently Asked Questions