Usage in Server Components (beta)
Next.js 13 introduces support for React Server Components (opens in a new tab) in the app
directory. 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
directory 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
directory becomes stable.
Current beta version
npm install next-intl@2.12.0-beta.3
This beta version was tested with next@13.2.4
.
Roadmap
- ✅ All APIs from
next-intl
can be used in Server Components. - 💡 Currently SSR-only (i.e. no
generateStaticParams
), therefore use CDN caching.
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
directory (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 createIntlMiddleware from 'next-intl/middleware';
export default createIntlMiddleware({
// 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 aren't pages that you'd like to internationalize
matcher: ['/((?!api|_next|favicon.ico|assets).*)']
};
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.
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
. 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');
}
// ...
}
Switching to Client Components
If you need to use translations in Client Components, the best approach is to pass the generated labels as props.
import {useTranslations} from 'next-intl';
import Expandable from './Expandable';
export default function FAQ() {
const t = useTranslations('FAQ');
return <Expandable title={t('title')}>{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 && <p>{children}</p>}
</div>
);
}
This way your messages never leave the server and the client only needs to load the code that is necessary for initializing your interactive components.
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 will increase your client bundle size.
Global request configuration
If you'd like to apply global configuration like formats
, defaultTranslationValues
, timeZone
, now
, or error handling functions like onError
and getMessageFallback
, you can provide these 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 if necessary
timeZone: headers().get('x-time-zone') ?? 'Europe/Berlin'
}));
Middleware configuration
The middleware handles redirects and rewrites based on the detected user locale.
Routing with a locale prefix (default)
Since your pages are nested within a [locale]
folder, all routes are prefixed with one of your supported locales (e.g. /de/about
). To keep the URL short, requests for the default locale are rewritten internally to work without a locale prefix.
Request examples:
/
→/en
/about
→/en/about
/de/about
→/de/about
When the user visits a route without an explicit locale, next-intl
matches the accept-language
header (opens in a new tab) of the request against the available locales and optionally redirects the user to a prefixed route. The matched locale is saved in a cookie.
Changing locales
To change the locale, users can visit a prefixed route. This will always take precedence over a previously matched locale that is saved in a cookie or the accept-language
header.
Example workflow:
- A user requests
/
and based on theaccept-language
header, thede
locale is matched. - The
de
locale is saved in a cookie and the user is redirected to/de
. - In your app code, you render
<Link locale="en" href="/">Switch to English</Link>
to allow the user to change the locale toen
. - When the user clicks on the link, a request to
/en
is initiated. - The middleware will update the cookie value to
en
and subsequently redirects the user to/
.
Always use a locale prefix
If you want to include a prefix for the default locale as well, you can configure the middleware accordingly.
import createIntlMiddleware from 'next-intl/middleware';
export default createIntlMiddleware({
locales: ['en', 'de'],
defaultLocale: 'en',
routing: {
type: 'prefix',
prefix: 'always' // Defaults to 'as-needed'
}
});
In this case, requests without a prefix will be redirected accordingly (e.g. /about
to /en/about
).
Routing with domains
If you want to serve your localized content based on separate domains, you can provide a list of mappings between domains and locales to the middleware.
import createIntlMiddleware from 'next-intl/middleware';
export default createIntlMiddleware({
locales: ['en', 'de'],
defaultLocale: 'en',
routing: {
type: 'domain',
domains: [
{
domain: 'en.example.com',
locale: 'en'
},
{
domain: 'de.example.com',
locale: 'de'
}
]
}
});
In this case, your content will be available without a locale prefix based on the domain since next-intl
rewrites the request internally (e.g. de.example.com/about
→ /de/about
).
The domain that is needed to match the locale is read from the x-forwarded-host
header, with a fallback to host
. If no domain matches (e.g. on localhost), the defaultLocale
is used.
Note that you might want to turn on domain-based routing only in production, since the middleware will try to redirect to one of the configured domains based on the matched locale.
Changing locales
Since the middleware is aware of all your domains, next-intl
will automatically switch the domain when the user requests to change the locale.
Example workflow:
- The user is on
en.example.com
. - The app renders
<Link locale="de" href="/">Switch to German</Link>
to allow the user to change the locale tode
. - When the link is clicked, a request to
en.example.com/de
is initiated. - The middleware recognizes that the user wants to switch to another domain and responds with a redirect to
de.example.com
.
Alternate links
next-intl
automatically sets the link
header (opens in a new tab) to inform search engines that your content is available in different languages. Note that this automatically integrates with your routing strategy and will generate the correct links based on your configuration (prefix
or domain
).
If you prefer to include these links yourself, you can opt-out of this behaviour.
import createIntlMiddleware from 'next-intl/middleware';
export default createIntlMiddleware({
// ... other config
// Default: `true`
alternateLinks: false
});
Localizing pathnames
If you want to localize the pathnames of your app, you can accomplish this by using appropriate rewrites (opens in a new tab).
const withNextIntl = require('next-intl/plugin')();
module.exports = withNextIntl({
experimental: {appDir: true},
rewrites() {
return [
{
source: '/de/über',
destination: '/de/about'
}
];
}
});
Since next-intl
isn't aware of the rewrites you've configured, you likely want to make some adjustments:
- Turn off the
alternateLinks
option in the middleware and provide search engine hints about localized versions of your content (opens in a new tab) by yourself. - Translate the pathnames you're passing to the routing APIs from
next-intl
(example (opens in a new tab)).
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 SSR'd pages as you'd get from SSG (learn more (opens in a new tab)).
const ms = require('ms');
const withNextIntl = require('next-intl/plugin')();
module.exports = withNextIntl({
// ... Other config
headers() {
return [
{
// Cache all content pages
source: '/((?!_next|assets|favicon.ico).*)',
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).