Internationalization of Server & Client Components in Next.js 13
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: Passing Server Components to Client Components as Props (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 {NextIntlClientProvider} from 'next-intl';
import ClientCounter from './ClientCounter';
export default function Counter() {
// Receive messages provided in `i18n.ts`
const messages = useMessages();
return (
<NextIntlClientProvider
messages={
// Only provide the minimum of messages
pick(messages, 'ClientCounter')
}
>
<ClientCounter />
</NextIntlClientProvider>
);
}
(working example (opens in a new tab))
NextIntlClientProvider
inherits the props locale
, now
and timeZone
when the component is rendered from a Server Component. Other configuration
properties like messages
and formats
can be provided as necessary.
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).
Troubleshooting
"Failed to call useTranslations
, because the context from NextIntlClientProvider
was not found."
You might encounter this error or a similar one referencing useFormatter
while working on your app.
This can happen because:
- The component that calls the hook accidentally ended up in a client-side module graph, but you expected it to render as a Server Component. If this is the case, try to pass this component via
children
to the Client Component instead. - You're intentionally calling the hook from a Client Component, but
NextIntlClientProvider
is not present as an ancestor in the component tree. If this is the case, you can wrap your component inNextIntlClientProvider
to resolve this error.
"Functions cannot be passed directly to Client Components because they're not serializable."
You might encounter this error when you try to pass a non-serializable prop to NextIntlClientProvider
.
The component accepts the following props that are not serializable:
onError
getMessageFallback
- Rich text elements for
defaultTranslationValues
To configure these, you can wrap NextIntlClientProvider
with another component that is marked with 'use client'
and defines the relevant props:
'use client';
import {NextIntlClientProvider} from 'next-intl';
export default function MyCustomNextIntlClientProvider({
locale,
timeZone,
now,
...rest
}) {
return (
<NextIntlClientProvider
// Define non-serializable props here
defaultTranslationValues={{
i: (text) => <i>{text}</i>
}}
// Make sure to forward these props to avoid markup mismatches
locale={locale}
timeZone={timeZone}
now={now}
{...props}
/>
);
}
(working example (opens in a new tab))
By doing this, your custom provider will already be part of the client-side bundle and can therefore define and pass functions as props.
Important: Be sure to pass explicit locale
, timeZone
and now
props to NextIntlClientProvider
in this case, since the props aren't automatically inherited from a Server Component when you import NextIntlClientProvider
from a Client Component.