Internationalization of Server & Client Components
React Server Components (opens in a new tab) allow you to implement components that remain server-side only if they don’t require React’s interactive features, such as useState
and useEffect
.
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 run as a Server Component.
export default function Index() {
const t = useTranslations('Index');
return <h1>{t('title')}</h1>;
}
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
Using internationalization in Server Components
Server Components can be declared in two ways:
- Async components
- Non-async, regular components
In a typical app, you'll likely find both types of components. next-intl
provides corresponding APIs that work for the given component type.
Async components
These are primarly concerned with fetching data and can not use hooks (opens in a new tab). Due to this, next-intl
provides a set of awaitable versions of the functions that you usually call as hooks from within components.
import {getTranslations} from 'next-intl/server';
export default async function ProfilePage() {
const user = await fetchUser();
const t = await getTranslations('ProfilePage');
return (
<PageLayout title={t('title', {username: user.name})}>
<UserDetails user={user} />
</PageLayout>
);
}
These functions are available:
import {
getTranslations,
getFormatter,
getNow,
getTimeZone,
getMessages,
getLocale
} from 'next-intl/server';
const t = await getTranslations('ProfilePage');
const format = await getFormatter();
const now = await getNow();
const timeZone = await getTimeZone();
const messages = await getMessages();
const locale = await getLocale();
Non-async components
Components that aren't declared with the async
keyword and don't use interactive features like useState
, are referred to as shared components (opens in a new tab). These can render either as a Server or Client Component, depending on where they are imported from.
In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components.
import {useTranslations} from 'next-intl';
export default function UserDetails({user}) {
const t = useTranslations('UserProfile');
return (
<section>
<h2>{t('title')}</h2>
<p>{t('followers', {count: user.numFollowers})}</p>
</section>
);
}
If you import useTranslations
, useFormatter
, useLocale
, useNow
and useTimeZone
from a shared component, next-intl
will automatically provide an implementation that works best for the environment this component executes in (server or client).
How does the Server Components integration work?
next-intl
uses react-server
conditional exports (opens in a new tab) to load code that is optimized for the usage in Server or Client Components. While configuration for hooks like useTranslations
is read via useContext
on the client side, on the server side it is loaded via i18n.ts
.
Hooks are currently primarly known for being used in Client Components since they are typically stateful or don't apply to a server environment. However, hooks like useId
(opens in a new tab) can be used in Server Components too. Similarly, next-intl
provides a hooks-based API that looks identical, regardless of if it's used in a Server or Client Component.
The one restriction that currently comes with this pattern is that hooks can not be called from async
components. next-intl
therefore provides a separate set of awaitable APIs for this use case.
Should I use async or non-async functions for my components?
If you implement components that qualify as shared components, it can be beneficial to implement them as non-async functions. This allows to use these components either in a server or client environment, making them really flexible. Even if you don't intend to to ever run a particular component on the client side, this compatibility can still be helpful, e.g. for simplified testing.
However, there's no need to dogmatically use non-async functions exclusively for handling internationalization—use what fits your app best.
In regard to performance, async functions and hooks can be used very much interchangeably. The configuration from i18n.ts
is only loaded once upon first usage and both implementations use request-based caching internally where relevant. The only minor difference is that async functions have the benefit that rendering can be resumed right after an async function has been invoked. In contrast, in case a hook call triggers the initialization in i18n.ts
, the component will suspend until the config is resolved and will re-render subsequently, possibly re-executing component logic prior to the hook call. However, once config has been resolved as part of a request, hooks will execute synchronously without suspending, resulting in less overhead in comparison to async functions since rendering can be resumed without having to wait for the microtask queue to flush (see resuming a suspended component by replaying its execution (opens in a new tab) in the corresponding React RFC).
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'; // A Client Component
import FAQContent from './FAQContent';
export default function FAQEntry() {
// Call `useTranslations` in a Server Component ...
const t = useTranslations('FAQEntry');
// ... and pass translated content to a Client Component
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>
);
}
By doing this, 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)
Example: How can I implement a form?
Forms need client-side state for showing loading indicators and validation errors.
To keep internationalization on the server side, it can be helpful to structure your components in a way where the interactive parts are moved out to leaf components instead of marking the whole form with 'use client';
.
Example:
import {useTranslations} from 'next-intl';
// A Client Component, so that it can use `useFormState` to
// potentially display errors received after submission.
import RegisterForm from './RegisterForm';
// A Client Component, so that it can use `useFormStatus`
// to disable the input field during submission.
import FormField from './FormField';
// A Client Component, so that it can use `useFormStatus`
// to disable the submit button during submission.
import FormSubmitButton from './FormSubmitButton';
export default function RegisterPage() {
const t = useTranslations('RegisterPage');
function registerUser() {
'use server';
// ...
}
return (
<RegisterForm action={registerUser}>
<FormField label={t('firstName')} name="firstName" />
<FormField label={t('lastName')} name="lastName" />
<FormField label={t('email')} name="email" />
<FormField label={t('password')} name="password" />
<FormSubmitButton label={t('submit')} />
</RegisterForm>
);
}
Example: How can I implement a locale switcher?
If you implement a locale switcher as an interactive select, you can keep internationalization on the server side by rendering the labels from a Server Component and only marking the select element as a Client Component.
import {useLocale, useTranslations} from 'next-intl';
import {locales} from 'config';
// A Client Component that registers an event listener for
// the `change` event of the select, uses `useRouter`
// to change the locale and uses `useTransition` to display
// a loading state during the transition.
import LocaleSwitcherSelect from './LocaleSwitcherSelect';
export default function LocaleSwitcher() {
const t = useTranslations('LocaleSwitcher');
const locale = useLocale();
return (
<LocaleSwitcherSelect defaultValue={locale} label={t('label')}>
{locales.map((cur) => (
<option key={cur} value={cur}>
{t('locale', {locale: cur})}
</option>
))}
</LocaleSwitcherSelect>
);
}
Example implementation (opens in a new tab) (demo (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
To reduce bundle size, next-intl
doesn't automatically provide messages or formats to Client Components.
If you need to incorporate dynamic state into components that can not be moved to the server side, you can wrap these components with NextIntlClientProvider
and provide the relevant messages.
import pick from 'lodash/pick';
import {NextIntlClientProvider, useMessages} from 'next-intl';
import ClientCounter from './ClientCounter';
export default function Counter() {
// Receive messages provided in `i18n.ts` …
const messages = useMessages();
return (
<NextIntlClientProvider
messages={
// … and provide the relevant messages
pick(messages, 'ClientCounter')
}
>
<ClientCounter />
</NextIntlClientProvider>
);
}
How can I know the messages I need to provide to the client side?
Currently, the messages you select for being passed to the client side need to be picked based on knowledge about the implementation of the wrapped components. If this becomes too cumbersome, you can consider providing all messages to the client side.
An automatic, compiler-driven approach is being evaluated in next-intl#2
(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, useMessages} from 'next-intl';
import {notFound} from 'next/navigation';
export default 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 at the top of this page).
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.