Skip to content
3.0 preview (see announcement)
Docs
Getting started
App Router

Next.js 13: Internationalization (i18n)

Next.js 13 introduces support for React Server Components (opens in a new tab) with the App Router and unlocks many benefits when handling internationalization on the server side.

Getting started

If you haven't done so already, create a Next.js 13 app that uses the App Router (opens in a new tab). All pages should be moved within a [locale] folder so that we can use this segment to provide content in different languages (e.g. /en, /en/about, etc.).

Start by running npm install next-intl and create the following file structure:

├── messages (1)
│   ├── en.json
│   └── ...
├── next.config.js (2)
└── src
    ├── i18n.ts (3)
    ├── middleware.ts (4)
    └── app
        └── [locale]
            ├── layout.tsx (5)
            └── page.tsx (6)

Now, set up the files as follows:

messages/en.json

Messages can be provided locally or loaded from a remote data source (e.g. a translation management system). Use whatever suits your workflow best.

The simplest option is to create JSON files locally based on locales, e.g. en.json.

messages/en.json
{
  "Index": {
    "title": "Hello world!"
  }
}

next.config.js

Now, set up the plugin which provides i18n configuration to Server Components.

next.config.js
const withNextIntl = require('next-intl/plugin')();
 
module.exports = withNextIntl({
  // Other Next.js configuration ...
});

src/i18n.ts

next-intl creates a configuration once per request. Here you can provide messages and other options depending on the locale of the user.

src/i18n.ts
import {getRequestConfig} from 'next-intl/server';
 
export default getRequestConfig(async ({locale}) => ({
  messages: (await import(`../messages/${locale}.json`)).default
}));
Can I move this file somewhere else?

This file is supported out-of-the-box both in the src folder as well as in the project root with the extensions .ts, .tsx, .js and .jsx.

If you prefer to move this file somewhere else, you can provide an optional path to the plugin:

next.config.js
const withNextIntl = require('next-intl/plugin')(
  // Specify a custom path here
  './somewhere/else/i18n.ts'
);
 
module.exports = withNextIntl({
  // Other Next.js configuration ...
});

middleware.ts

The middleware matches a locale for the request and handles redirects and rewrites accordingly.

middleware.ts
import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  // A list of all locales that are supported
  locales: ['en', 'de'],
 
  // Used when no locale matches
  defaultLocale: 'en'
});
 
export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(de|en)/:path*']
};

app/[locale]/layout.tsx

The locale that was matched by the middleware is available via the locale param and can be used to configure the document language.

app/[locale]/layout.tsx
import {useLocale} from 'next-intl';
import {notFound} from 'next/navigation';
 
const locales = ['en', 'de'];
 
export default function LocaleLayout({children, params: {locale}}) {
  // Validate that the incoming `locale` parameter is valid
  if (!locales.includes(locale as any)) notFound();
 
  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

app/[locale]/page.tsx

Use translations in your page components or anywhere else!

app/[locale]/page.tsx
import {useTranslations} from 'next-intl';
 
export default function Index() {
  const t = useTranslations('Index');
  return <h1>{t('title')}</h1>;
}

That's all it takes!

💡

Next steps:

Static rendering

By using APIs like useTranslations from next-intl in Server Components, your pages will currently opt into dynamic rendering. This is a limitation that will eventually be lifted once createServerContext (opens in a new tab) is available and integrated in Next.js.

As a stopgap solution, next-intl provides a temporary API that can be used to enable static rendering:

Add generateStaticParams to app/[locale]/layout.tsx

Since we use a dynamic route segment for the [locale] param, we need to provide all possible values via generateStaticParams (opens in a new tab) to Next.js, so the routes can be rendered at build time.

app/[locale]/layout.tsx
const locales = ['en', 'de'];
 
export function generateStaticParams() {
  return locales.map((locale) => ({locale}));
}

Add unstable_setRequestLocale to all layouts and pages

next-intl provides a temporary API that can be used to distribute the locale that is received via params in a layout or page for usage in all Server Components that are rendered as part of the request.

app/[locale]/layout.tsx
import {unstable_setRequestLocale} from 'next-intl/server';
 
const locales = ['en', 'de'];
 
export default async function LocaleLayout({
  children,
  params: {locale}
}) {
  // Validate that the incoming `locale` parameter is valid
  if (!locales.includes(locale as any)) notFound();
 
  unstable_setRequestLocale(locale);
 
  return (
    // ...
  );
}
app/[locale]/page.tsx
import {unstable_setRequestLocale} from 'next-intl/server';
import {locales} from '..';
 
export default async function IndexPage({
  params: {locale}
}) {
  unstable_setRequestLocale(locale);
 
  return (
    // ...
  );
}

What does "unstable" mean?

unstable_setRequestLocale is meant to be used as a stopgap solution and will eventually be replaced by an API that's based on createServerContext (opens in a new tab). When that time comes, you'll get a deprecation notice in a minor version and the API will be removed as part of a major version.

Note that Next.js can render layouts and pages indepently. This means that e.g. when you navigate from /settings/profile to /settings/privacy, the /settings segment might not re-render as part of the request. Due to this, it's important that unstable_setRequestLocale is called not only in the parent settings/layout.tsx, but also in the individual pages profile/page.tsx and settings/page.tsx.

That being said, the API is expected to work reliably if you're cautious to apply it in all relevant places.