Next.js 13: Internationalization (i18n) in Server Components
Next.js 13 introduces support for React Server Components (opens in a new tab) with the App Router. next-intl
is adopting the new capabilities and is currently offering a beta version to early adopters, who are already building apps with the app
directory.
The App Router is currently in beta, patterns are still emerging and APIs may change. Please use this at your own risk, knowing that you might have to face a migration effort as the App Router becomes stable.
Current beta version
npm install next-intl@2.14.0-beta.2
This beta version was tested with next@13.3.0
.
Roadmap
Feature | Status |
---|---|
Usage of all next-intl APIs in Server Components | ✅ |
Dynamic rendering | ✅ |
Static rendering (i.e. generateStaticParams ) | 🏗️ |
While the support for static rendering is pending, consider CDN caching to get the same performance characteristics from dynamic rendering or use Client Components for the time being.
For details, see the pending pull request for Server Components support (opens in a new tab).
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 creating the following file structure:
├── messages (1)
│ ├── en.json
│ └── ...
├── i18n.ts (2)
├── next.config.js (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
.
{
"Index": {
"title": "Hello world!"
}
}
i18n.ts
next-intl
creates a configuration once per request and makes it available to all Server Components. Here you can provide messages depending the locale of the user.
import {getRequestConfig} from 'next-intl/server';
export default getRequestConfig(async ({locale}) => ({
messages: (await import(`./messages/${locale}.json`)).default
}));
next.config.js
Now, set up the plugin and provide the path to your configuration.
const withNextIntl = require('next-intl/plugin')(
// This is the default (also the `src` folder is supported out of the box)
'./i18n.ts'
);
module.exports = withNextIntl({
// Other Next.js configuration ...
experimental: {appDir: true}
});
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'],
// If this locale is matched, pathnames work without a prefix (e.g. `/about`)
defaultLocale: 'en'
});
export const config = {
// Skip all paths that should not be internationalized
matcher: ['/((?!api|_next|.*\\..*).*)']
};
app/[locale]/layout.tsx
The locale
that was matched by the middleware is available via useLocale
and can be used to configure the document language.
import {useLocale} from 'next-intl';
import {notFound} from 'next/navigation';
export default function LocaleLayout({children, params}) {
const locale = useLocale();
// Show a 404 error if the user requests an unknown locale
if (params.locale !== locale) {
notFound();
}
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! Now you can internationalize your apps on the server side.
If you've encountered an issue, you can explore the code for a working example (opens in a new tab) (demo (opens in a new tab)).
If you're in a transitioning phase, either from the pages
directory to the app
directory, or from Client Components to the Server Components beta, you can apply NextIntlClientProvider
additionally (example (opens in a new tab)).
Routing
Link
next-intl
provides a drop-in replacement for next/link
that will automatically prefix the href
with the current locale as necessary. If the default locale is matched, the href
remains unchanged and no prefix is added.
import {Link} from 'next-intl';
// When the user is on `/en`, the link will point to `/en/about`
<Link href="/about">About</Link>
// You can override the `locale` to switch to another language
<Link href="/" locale="de">Switch to German</Link>
useRouter
If you need to navigate programmatically (e.g. in response to a form submission), next-intl
provides a convience API that wraps useRouter
from Next.js and automatically applies the locale of the user.
'use client';
import {useRouter} from 'next-intl/client';
const router = useRouter();
// When the user is on `/en`, the router will navigate to `/en/about`
router.push('/about');
usePathname
To retrieve the pathname without a potential locale prefix, you can call usePathname
.
'use client';
import {usePathname} from 'next-intl/client';
// When the user is on `/en`, this will be `/`
const pathname = usePathname();
redirect
If you want to interrupt the render of a Server Component and redirect to another page, you can invoke the redirect
function from next-intl/server
. This wraps the redirect
function from Next.js (opens in a new tab) and automatically applies the current locale.
import {redirect} from 'next-intl/server';
export default async function Profile() {
const user = await fetchUser();
if (!user) {
// When the user is on `/en/profile`, this will be `/en/login`
redirect('/login');
}
// ...
}
Using translations in Client Components
If you need to use translations or other functionality from next-intl
in Client Components, the best approach is to pass the labels as props or children
from a Server Component.
import {useTranslations} from 'next-intl';
import Expandable from './Expandable';
export default function FAQEntry() {
const t = useTranslations('FAQEntry');
return (
<Expandable title={t('title')}>
<FAQContent content={t('description')} />
</Expandable>
);
}
'use client';
import {useState} from 'react';
function Expandable({title, children}) {
const [expanded, setExpanded] = useState(false);
function onToggle() {
setExpanded(!expanded);
}
return (
<div>
<button onClick={onToggle}>{title}</button>
{expanded && <div>{children}</div>}
</div>
);
}
Benefits
Your messages never leave the server and don't need to be serialized for the client side.
next-intl
doesn't need to be loaded on the client side- No need to split your messages based on routes or components
If you need to integrate your translations with interactively-controlled state, you can still manage your translations on the server side by using one of the following techniques.
Examples for state that can be read on the server side:
- Page- or search params (opens in a new tab)
- Cookies (opens in a new tab)
- Database state (opens in a new tab)
There's an article about using next-intl
in Server Components (opens in a new tab) which explores this topic in more detail, specifically the section about adding interactivity (opens in a new tab).
If you absolutely need to use functionality from next-intl
on the client side, you can wrap the respective components with NextIntlClientProvider
(example code (opens in a new tab)). Note however that this is a performance tradeoff (see the bullet points above). Also note that NextIntlClientProvider
doesn't automatically inherit configuration from i18n.ts
, therefore make sure to provide all relevant props on the component.
Global request configuration
next-intl
supports the following global configuration:
formats
defaultTranslationValues
timeZone
now
onError
getMessageFallback
For the usage in Server Components, these can be configured in i18n.ts
.
import {headers} from 'next/headers';
import {getRequestConfig} from 'next-intl/server';
export default getRequestConfig(async ({locale}) => ({
messages: (await import(`../messages/${locale}.json`)).default,
// You can read from headers or cookies here
timeZone: headers().get('x-time-zone') ?? 'Europe/Berlin'
}));
Note that the configuration object will be created once for each request and will then be made available to all Server Components in your app.
Using internationalization outside of components
If you need to use translated messages in functions like generateMetadata
, you can import awaitable versions of the functions that you usually call as hooks from next-intl/server
.
import {getTranslations} from 'next-intl/server';
export async function generateMetadata() {
const t = await getTranslations('Metadata');
return {
title: t('title'),
description: t('description')
};
}
These functions are available from next-intl/server
for usage outside of components:
import {
getTranslations, // like `useTranslations`
getFormatter, // like `useFormatter`
getLocale, // like `useLocale`
getNow, // like `useNow`
getTimeZone // like `useTimeZone`
} from 'next-intl/server';
CDN caching
Since next-intl
is currently SSR-only, it's a good idea to use CDN caching (opens in a new tab) via headers
(opens in a new tab) in next.config.js
to get the same level of performance from dynamic rendering as you'd get from static rendering (learn more (opens in a new tab)). Also note that the Next.js Cache (opens in a new tab) improves the performance for dynamically rendered pages.
const ms = require('ms');
const withNextIntl = require('next-intl/plugin')();
module.exports = withNextIntl({
// ... Other config
headers() {
return [
{
// Cache all content pages
source: '/((?!_next|.*\\..*).*)',
headers: [
{
key: 'Cache-Control',
value: [
`s-maxage=` + ms('1d') / 1000,
`stale-while-revalidate=` + ms('1y') / 1000
].join(', ')
}
],
// If you're deploying on a host that doesn't support the `vary` header (e.g. Vercel),
// make sure to disable caching for prefetch requests for Server Components.
missing: [
{
type: 'header',
key: 'Next-Router-Prefetch'
}
]
}
];
}
});
Providing feedback
If you have feedback about using next-intl
in the app
directory, feel free to leave feedback in the PR which implements the React Server Components support (opens in a new tab).