Next.js App Router Internationalization (i18n)
The Next.js App Router introduces support for React Server Components (opens in a new tab) and unlocks many benefits when handling internationalization on the server side.
Getting started
If you haven't done so already, create a Next.js app (opens in a new tab) that uses the App Router.
next-intl
integrates with the App Router by using a [locale]
dynamic segment (opens in a new tab) so that we can use this segment to provide content in different languages (e.g. /en
, /en/about
, etc.).
Let's get started!
Run npm install next-intl
and create the following file structure:
├── messages (1)
│ ├── en.json
│ └── ...
├── next.config.mjs (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 add JSON files in your project based on locales, e.g. en.json
.
{
"Index": {
"title": "Hello world!"
}
}
next.config.mjs
Now, set up the plugin which creates an alias to provide your i18n configuration (specified in the next step) to Server Components.
If you're using ECMAScript modules for your Next.js config, you can use the plugin as follows:
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);
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.
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
// Can be imported from a shared config
const locales = ['en', 'de'];
export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();
return {
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:
const withNextIntl = createNextIntlPlugin(
// Specify a custom path here
'./somewhere/else/i18n.ts'
);
middleware.ts
The middleware matches a locale for the request and handles redirects and rewrites accordingly.
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.
export default function LocaleLayout({
children,
params: {locale}
}: {
children: React.ReactNode;
params: {locale: string};
}) {
return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}
app/[locale]/page.tsx
Use translations in your page components or anywhere else!
import {useTranslations} from 'next-intl';
export default function Index() {
const t = useTranslations('Index');
return <h1>{t('title')}</h1>;
}
That's all it takes!
In case you ran into an issue, have a look at the App Router example (opens in a new tab) to explore a working app.
Next steps:
- Usage guide: Format messages, dates and times
Environments: Explore usage in Server & Client Components and the Metadata API
Routing: Integrate i18n routing with
<Link />
& friendsWorkflows: Make use of the TypeScript integration & more
Static rendering
By using APIs like useTranslations
from next-intl
in Server Components, your pages will currently opt into dynamic rendering (opens in a new tab). This is a limitation that we aim to remove in the future, but 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.
// Can be imported from a shared config
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 layouts and pages for usage in all Server Components that are rendered as part of the request.
import {unstable_setRequestLocale} from 'next-intl/server';
export default async function LocaleLayout({children, params: {locale}}) {
unstable_setRequestLocale(locale);
return (
// ...
);
}
import {unstable_setRequestLocale} from 'next-intl/server';
export default function IndexPage({params: {locale}}) {
unstable_setRequestLocale(locale);
// Once the request locale is set, you
// can call hooks from `next-intl`
const t = useTranslations('IndexPage');
return (
// ...
);
}
Keep in mind that:
-
The locale that you pass to
unstable_setRequestLocale
should be validated (e.g. ini18n.ts
). -
You need to call this function in every page and every layout that you intend to enable static rendering for since Next.js can render layouts and pages independently.
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 privacy/page.tsx
.
What does "unstable" mean?
unstable_setRequestLocale
is meant to be used as a stopgap solution and we aim to remove it in the future in case Next.js adds an API to access parts of the URL (opens in a new tab). If that's the case, you'll get a deprecation notice in a minor version and the API will be removed as part of a major version.
That being said, the API is expected to work reliably if you're cautious to apply it in all relevant places.
How does unstable_setRequestLocale work?
next-intl
uses cache()
(opens in a new tab) to create a mutable store that holds the current locale. By calling unstable_setRequestLocale
, the current locale will be written to the store, making it available to all APIs that require the locale.
Note that the store is scoped to a request and therefore doesn't affect other requests that might be handled in parallel while a given request resolves asynchronously.
Why is this API necessary?
Next.js currently doesn't provide an API to read route params like locale
at arbitrary places in Server Components (see vercel/next.js
#58862 (opens in a new tab)). The locale
is fundamental to all APIs provided by next-intl
, therefore passing this as a prop throughout the tree doesn't stand out as particularly ergnomic.
Due to this, next-intl
uses its middleware to attach an x-next-intl-locale
header to the incoming request, holding the negotiated locale as a value. This technique allows the locale to be read at arbitrary places via headers().get('x-next-intl-locale')
.
However, the usage of headers
opts the route into dynamic rendering.
By using unstable_setRequestLocale
, you can provide the locale that is received in layouts and pages via params
to next-intl
. All APIs from next-intl
can now read from this value instead of the header, enabling static rendering.
Use the locale
param in metadata
In addition to the rendering of your pages, also page metadata needs to qualify for static rendering.
To achieve this, you can forward the locale
that you receive from Next.js via params
to the awaitable functions from next-intl
.
import {getTranslations} from 'next-intl/server';
export async function generateMetadata({params: {locale}}) {
const t = await getTranslations({locale, namespace: 'Metadata'});
return {
title: t('title')
};
}