Skip to content
3.0 preview (see announcement)
Docs
Environments
Server & Client Components

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.

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

  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
  5. 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.

[locale]/faq/page.tsx
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>
  );
}
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>
  );
}

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.

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} 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} 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:

  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.