Skip to content
next-intl 3.0 is out! (announcement)
Docs
Environments
Server & Client Components

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.

app/[locale]/page.tsx
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:

  1. Your messages never leave the server and don't need to be serialized for the client side
  2. Library code for internationalization doesn't need to be loaded on the client side
  3. No need to split your messages, e.g. based on routes or components
  4. No runtime cost on the client side

Using internationalization in Server Components

Server Components can be declared in two ways:

  1. Async components
  2. 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.

[locale]/profile/page.tsx
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.

UserDetails.tsx
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.

[locale]/faq/page.tsx
import {useTranslations} from 'next-intl';
import Expandable from './Expandable';
 
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>
  );
}
Expandable.tsx
'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:

app/register/page.tsx
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.

LocaleSwitcher.tsx
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.

Pagination.tsx
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:

  1. Page or search params (opens in a new tab)
  2. Cookies (opens in a new tab)
  3. 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.

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.

Counter.tsx
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={
        // 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.

app/[locale]/layout.tsx
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:

  1. 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.
  2. 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 in NextIntlClientProvider 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:

  1. onError
  2. getMessageFallback
  3. 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:

MyCustomNextIntlClientProvider.tsx
'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.