Internationalization of Server & Client Components in Next.js 13
This page contains background information about the advantages of moving internationalization to Server Components. Note that this is currently only available in the Server Components beta version.
With the introduction of the App Router in Next.js 13, React Server Components (opens in a new tab) became publicly available. This new paradigm allows components that don’t require React’s interactive features, such as useState
and useEffect
, to remain server-side only.
This applies to handling internationalization too.
import {useTranslations} from 'next-intl';
// Since this component doesn't use any interactive features
// from React, it can be implemented as a Server Component.
export default function Index() {
const t = useTranslations('Index');
return <h1>{t('title')}</h1>;
}
Benefits of handling i18n in Server Components
Moving internationalization to the server side unlocks new levels of performance, leaving the client side for interactive features.
Benefits of server-side internationalization:
Your messages never leave the server and don't need to be serialized for the client side
Library code for internationalization doesn't need to be loaded on the client side
- No need to split your messages, e.g. based on routes or components
- No runtime cost on the client side
No need to handle environment differences like different time zones on the server and client
Using internationalization in Client Components
Depending on your situation, you may need to handle internationalization in Client Components as well. There are several options for using translations or other functionality from next-intl
in Client Components, listed here in order of recommendation.
Option 1: Passing translations to Client Components
The preferred approach is to pass the processed 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>
);
}
As you see, we can use interactive features from React like useState
on translated content, even though the translation only runs on the server side.
Learn more in the Next.js docs: Nesting Server Components inside Client Components (opens in a new tab)
Option 2: Moving state to the server side
You might run into cases where you have dynamic state, such as pagination, that should be reflected in translated messages.
function Pagination({curPage, totalPages}) {
const t = useTranslations('Pagination');
return <p>{t('info', {curPage, totalPages})}</p>;
}
You can still manage your translations on the server side by using:
- Page or search params (opens in a new tab)
- Cookies (opens in a new tab)
- Database state (opens in a new tab)
In particular, page and search params are often a great option because they offer additional benefits such as preserving the state of the app when the URL is shared, as well as integration with the browser history.
There's an article on Smashing Magazine about using next-intl
in Server
Components (opens in a new tab)
which explores the usage of search params through a real-world example
(specifically the section about adding
interactivity (opens in a new tab)).
Option 3: Providing individual messages
If you need to incorporate dynamic state that can not be moved to the server side, you can wrap the respective components with NextIntlClientProvider
.
import pick from 'lodash/pick';
import {useLocale, NextIntlClientProvider} from 'next-intl';
import ClientCounter from './ClientCounter';
export default function Counter() {
const locale = useLocale();
// Receive messages provided in `i18n.ts`
const messages = useMessages();
return (
<NextIntlClientProvider
locale={locale}
messages={
// Only provide the minimum of messages
pick(messages, 'ClientCounter')
}
>
<ClientCounter />
</NextIntlClientProvider>
);
}
(working example (opens in a new tab))
NextIntlClientProvider
doesn't automatically inherit configuration from
i18n.ts
, therefore make sure to provide all relevant props on the component.
If you're configuring non-serializable values like functions, you have to mark
the component that renders NextIntlClientProvider
with 'use client'
(example (opens in a new tab)).
Option 4: Providing all messages
If you're building a highly dynamic app where most components use React's interactive features, you may prefer to make all messages available to Client Components.
import {NextIntlClientProvider} from 'next-intl';
import {notFound} from 'next/navigation';
export default async function LocaleLayout({children, params: {locale}}) {
// Receive messages provided in `i18n.ts`
const messages = useMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Note that this is a tradeoff in regard to performance (see the bullet points above).