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

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