Docs
Next.js 13
Server Components (beta)

Next.js 13: Internationalization (i18n) in Server Components

Next.js 13 introduces support for React Server Components (opens in a new tab) with the App Router. next-intl is adopting the new capabilities and is currently offering a beta version to early adopters, who are already building apps with the app directory.

⚠️

Support for React Server Components is currently in beta. Please use it at your own risk, knowing that you may have to migrate upon a stable release.

Current beta version

npm install next-intl@2.15.0-beta.5

This beta version was tested with next@13.4.0.

Roadmap

FeatureStatus
Usage of all next-intl APIs in Server Components
Dynamic rendering
Static rendering (i.e. generateStaticParams)🏗️
💡

While the support for static rendering is pending, consider CDN caching to get the same performance characteristics from dynamic rendering or use Client Components for the time being.

For details, see the pending pull request for Server Components support (opens in a new tab).

Getting started

If you haven't done so already, create a Next.js 13 app that uses the App Router (opens in a new tab). All pages should be moved within a [locale] folder so that we can use this segment to provide content in different languages (e.g. /en, /en/about, etc.).

Start by creating the following file structure:

├── messages (1)
│   ├── en.json
│   └── ...
├── i18n.ts (2)
├── next.config.js (3)
├── middleware.ts (4)
└── app
    └── [locale]
        ├── layout.tsx (5)
        └── page.tsx (6)

Now, set up the files as follows:

messages/en.json

Messages can be provided locally or loaded from a remote data source (e.g. a translation management system). Use whatever suits your workflow best.

The simplest option is to create JSON files locally based on locales, e.g. en.json.

messages/en.json
{
  "Index": {
    "title": "Hello world!"
  }
}

i18n.ts

next-intl creates a configuration once per request and makes it available to all Server Components. Here you can provide messages depending the locale of the user.

i18n.ts
import {getRequestConfig} from 'next-intl/server';
 
export default getRequestConfig(async ({locale}) => ({
  messages: (await import(`./messages/${locale}.json`)).default
}));

next.config.js

Now, set up the plugin and provide the path to your configuration.

next.config.js
const withNextIntl = require('next-intl/plugin')(
  // This is the default (also the `src` folder is supported out of the box)
  './i18n.ts'
);
 
module.exports = withNextIntl({
  // Other Next.js configuration ...
  experimental: {appDir: true}
});

middleware.ts

The middleware matches a locale for the request and handles redirects and rewrites accordingly.

middleware.ts
import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  // A list of all locales that are supported
  locales: ['en', 'de'],
 
  // If this locale is matched, pathnames work without a prefix (e.g. `/about`)
  defaultLocale: 'en'
});
 
export const config = {
  // Skip all paths that should not be internationalized
  matcher: ['/((?!api|_next|.*\\..*).*)']
};

app/[locale]/layout.tsx

The locale that was matched by the middleware is available via useLocale and can be used to configure the document language.

app/[locale]/layout.tsx
import {useLocale} from 'next-intl';
import {notFound} from 'next/navigation';
 
export default function LocaleLayout({children, params}) {
  const locale = useLocale();
 
  // Show a 404 error if the user requests an unknown locale
  if (params.locale !== locale) {
    notFound();
  }
 
  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

app/[locale]/page.tsx

Use translations in your page components or anywhere else!

app/[locale]/page.tsx
import {useTranslations} from 'next-intl';
 
export default function Index() {
  const t = useTranslations('Index');
  return <h1>{t('title')}</h1>;
}

That's all it takes! Now you can internationalize your apps on the server side.

If you've encountered an issue, you can explore the code for a working example (opens in a new tab) (demo (opens in a new tab)).

If you're in a transitioning phase, either from the pages directory to the app directory, or from Client Components to the Server Components beta, you can apply NextIntlClientProvider additionally (example (opens in a new tab)).

Using translations in Client Components

If you need to use translations or other functionality from next-intl in Client Components, the best approach is to pass the 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 can see, we can use interactive features from React like useState on translated content, even though the translation only runs on the server side.

Benefits

  1. Your messages never leave the server and don't need to be serialized for the client side.

  2. next-intl doesn't need to be loaded on the client side
  3. No need to split your messages based on routes or components

Using interactive state in translations

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 page- or search params (opens in a new tab). There's an article on Smashing Magazine about using next-intl in Server Components (opens in a new tab) which explores this topic in more detail, specifically the section about adding interactivity (opens in a new tab).

Apart from page- or search params, you can also use cookies (opens in a new tab) or database state (opens in a new tab) for storing state that can be read on the server side.

If you absolutely need to use functionality from next-intl on the client 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';
 
async function Counter() {
  const locale = useLocale();
  const messages = (await import(`../../../../messages/${locale}.json`))
    .default;
 
  return (
    <NextIntlClientProvider
      locale={locale}
      messages={
        // Only provide the minimum of messages
        pick(messages, 'ClientCounter')
      }
    >
      <ClientCounter />
    </NextIntlClientProvider>
  );
}

(working example (opens in a new tab))

Note however that this is a performance tradeoff (see the bullet points above).

💡

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

Global request configuration

next-intl supports the following global configuration:

  • formats
  • defaultTranslationValues
  • timeZone
  • now
  • onError
  • getMessageFallback

For the usage in Server Components, these can be configured in i18n.ts.

i18n.ts
import {headers} from 'next/headers';
import {getRequestConfig} from 'next-intl/server';
 
export default getRequestConfig(async ({locale}) => ({
  messages: (await import(`../messages/${locale}.json`)).default,
 
  // You can read from headers or cookies here
  timeZone: headers().get('x-time-zone') ?? 'Europe/Berlin'
}));

Note that the configuration object will be created once for each request and will then be made available to all Server Components in your app.

Using internationalization outside of components

If you need to use translated messages in functions like generateMetadata, you can import awaitable versions of the functions that you usually call as hooks from next-intl/server.

app/[locale]/layout.tsx
import {getTranslations} from 'next-intl/server';
 
export async function generateMetadata() {
  const t = await getTranslations('Metadata');
 
  return {
    title: t('title'),
    description: t('description')
  };
}

These functions are available from next-intl/server for usage outside of components:

import {
  getTranslations, // like `useTranslations`
  getFormatter, // like `useFormatter`
  getLocale, // like `useLocale`
  getNow, // like `useNow`
  getTimeZone // like `useTimeZone`
} from 'next-intl/server';

Note however that the useTranslations hook is the primary API to translate messages in your app. Related: How (not) to use translations outside of React components

CDN caching

Since the support for using next-intl in React Server Components is currently SSR-only, it's a good idea to use CDN caching (opens in a new tab) to get the same performance characteristics as SSG.

Unfortunately, Next.js currently ignores response headers for content routes that are defined via next.config.js or middleware.ts, therefore the only way to achieve this currently seems to be to patch Next.js accordingly (opens in a new tab).

⚠️

Important: This is a big hack, make sure you know what you're doing if you apply this and test the result properly. This is considered a stopgap solution and will eventually be replaced with a reliable alternative.

dist/server/send-payload/revalidate-headers.js
  function setRevalidateHeaders(res, options) {
       if (options.private || options.stateful) {
-        if (options.private || !res.getHeader("Cache-Control")) {
-            res.setHeader("Cache-Control", `private, no-cache, no-store, max-age=0, must-revalidate`);
-        }
+        if (options.private) {
+            res.setHeader("Cache-Control", `private, no-cache, no-store, max-age=0, must-revalidate`);
+        } else {
+            // IMPORTANT: This matches all dynamic content routes and applies the same
+            // caching headers (10min CDN caching with infinite stale-while-revalidate).
+            // You can also use `res.req.url` to match specific routes.
+            res.setHeader("Cache-Control", "public, s-maxage=600, stale-while-revalidate=31557600");
+         }
     } else if (typeof options.revalidate === "number") {
               if (options.revalidate < 1) {
                       throw new Error(`invariant: invalid Cache-Control duration provided: ${options.revalidate} < 1`);

Providing feedback

If you have feedback about using next-intl in the app directory, feel free to leave feedback in the PR which implements the React Server Components support (opens in a new tab).