Docs
Next.js 13
Server Components (beta)

Usage in Server Components (beta)

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 within the app directory.

⚠️

The App Router is currently in beta, patterns are still emerging and APIs may change. Please use this at your own risk, knowing that you might have to face a migration effort as the App Router becomes stable.

Current beta version

npm install next-intl@2.13.0-beta.1

This beta version was tested with next@13.2.4.

Roadmap

FeatureStatus
Usage of all next-intl APIs in Server Components
Middleware for i18n routing
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 createIntlMiddleware from 'next-intl/middleware';
 
export default createIntlMiddleware({
  // 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.

Routing

Link

next-intl provides a drop-in replacement for next/link that will automatically prefix the href with the current locale as necessary. If the default locale is matched, the href remains unchanged and no prefix is added.

import {Link} from 'next-intl';
 
// When the user is on `/en`, the link will point to `/en/about`
<Link href="/about">About</Link>
 
// You can override the `locale` to switch to another language
<Link href="/" locale="de">Switch to German</Link>

useRouter

If you need to navigate programmatically (e.g. in response to a form submission), next-intl provides a convience API that wraps useRouter from Next.js and automatically applies the locale of the user.

'use client';
 
import {useRouter} from 'next-intl/client';
 
const router = useRouter();
 
// When the user is on `/en`, the router will navigate to `/en/about`
router.push('/about');

usePathname

To retrieve the pathname without a potential locale prefix, you can call usePathname.

'use client';
 
import {usePathname} from 'next-intl/client';
 
// When the user is on `/en`, this will be `/`
const pathname = usePathname();

redirect

If you want to interrupt the render of a Server Component and redirect to another page, you can invoke the redirect function from next-intl. This wraps the redirect function from Next.js (opens in a new tab) and automatically applies the current locale.

import {redirect} from 'next-intl/server';
 
export default async function Profile() {
  const user = await fetchUser();
 
  if (!user) {
    // When the user is on `/en/profile`, this will be `/en/login`
    redirect('/login');
  }
 
  // ...
}

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>
  );
}

This way your messages never leave the server and the client only needs to load the code that is necessary for initializing your interactive components.

If you need to integrate your translations with interactively-controlled state, you can still manage your translations on the server side by using one of the following techniques.

Examples for state that can be read on the server side:

There's an article 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).

If you absolutely need to use functionality from next-intl on the client side, you can wrap the respective components with NextIntlClientProvider (example code (opens in a new tab)). Note however that this is a performance tradeoff since the relevant messages need to be serialized into the document and the used APIs from next-intl need to be included in the client-side bundle.

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 if necessary
  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 of Server Components in your app.

Middleware configuration

The middleware handles redirects and rewrites based on the detected user locale.

There are two strategies for detecting the locale:

  1. Prefix-based routing (default)
  2. Domain-based routing

Once a locale is detected, it will be saved in a cookie.

Prefix-based routing (default)

Since your pages are nested within a [locale] folder, all routes are prefixed with one of your supported locales (e.g. /de/about). To keep the URL short, requests for the default locale are rewritten internally to work without a locale prefix.

Request examples:

  • //en
  • /about/en/about
  • /de/about/de/about

Locale detection

The locale is detected based on these priorities:

  1. A locale prefix is present in the pathname (e.g. /de/about)
  2. A cookie is present that contains a previously detected locale
  3. The accept-language header (opens in a new tab) is matched against the available locales
  4. The defaultLocale is used

To change the locale, users can visit a prefixed route. This will take precedence over a previously matched locale that is saved in a cookie or the accept-language header.

Example workflow:

  1. A user requests / and based on the accept-language header, the de locale is matched.
  2. The de locale is saved in a cookie and the user is redirected to /de.
  3. The app renders <Link locale="en" href="/">Switch to English</Link> to allow the user to change the locale to en.
  4. When the user clicks on the link, a request to /en is initiated.
  5. The middleware will update the cookie value to en and subsequently redirects the user to /.

Domain-based routing

If you want to serve your localized content based on different domains, you can provide a list of mappings between domains and locales to the middleware.

Example:

  • us.example.com (default: en)
  • ca.example.com (default: en)
  • ca.example.com/fr (fr)
import createIntlMiddleware from 'next-intl/middleware';
 
export default createIntlMiddleware({
  // All locales across all domains
  locales: ['en', 'fr'],
 
  // Used when no domain matches (e.g. on localhost)
  defaultLocale: 'en',
 
  domains: [
    {
      domain: 'us.example.com',
      defaultLocale: 'en',
      // Optionally restrict the locales managed by this domain. If this
      // domain receives requests for another locale (e.g. us.example.com/fr),
      // then the middleware will redirect to a domain that supports it.
      locales: ['en']
    },
    {
      domain: 'ca.example.com',
      defaultLocale: 'en'
      // If there are no `locales` specified on a domain,
      // all global locales will be supported here.
    }
  ]
});

The middleware rewrites the requests internally, so that requests for the defaultLocale on a given domain work without a locale prefix (e.g. us.example.com/about/en/about). If you want to include a prefix for the default locale as well, you can add localePrefix: 'always'.

Locale detection

To match the request against the available domains, the host is read from the x-forwarded-host header, with a fallback to host.

The locale is detected based on these priorities:

  1. A locale prefix is present in the pathname and the domain supports it (e.g. ca.example.com/fr)
  2. If the host of the request is configured in domains, the defaultLocale of the domain is used
  3. As a fallback, the locale detection of prefix-based routing applies
💡

Since unknown domains will be handled with prefix-based routing, this strategy can be used for local development where the host is localhost.

Since the middleware is aware of all your domains, the domain will automatically be switched when the user requests to change the locale.

Example workflow:

  1. The user requests us.example.com and based on the defaultLocale of this domain, the en locale is matched.
  2. The app renders <Link locale="fr" href="/">Switch to French</Link> to allow the user to change the locale to fr.
  3. When the link is clicked, a request to us.example.com/fr is initiated.
  4. The middleware recognizes that the user wants to switch to another domain and responds with a redirect to ca.example.com/fr.

Further configuration

Always use a locale prefix

If you want to include a prefix for the default locale as well, you can configure the middleware accordingly.

import createIntlMiddleware from 'next-intl/middleware';
 
export default createIntlMiddleware({
  locales: ['en', 'de'],
  defaultLocale: 'en',
  localePrefix: 'always' // Defaults to 'as-needed'
});

In this case, requests without a prefix will be redirected accordingly (e.g. /about to /en/about).

Note that this will affect both prefix-based as well as domain-based routing.

Disable automatic locale detection

If you want to disable locale detection based on the accept-language header, you can opt-out of this mechanism.

import createIntlMiddleware from 'next-intl/middleware';
 
export default createIntlMiddleware({
  locales: ['en', 'de'],
  defaultLocale: 'en',
  localeDetection: false // Defaults to `true`
});

Note that in this case other detection mechanisms will remain in place regardless (e.g. based on a locale prefix in the pathname or a matched domain).

Disable alternate links

The middleware automatically sets the link header (opens in a new tab) to inform search engines that your content is available in different languages. Note that this automatically integrates with your routing strategy and will generate the correct links based on your configuration.

If you prefer to include these links yourself, you can opt-out of this behaviour.

middleware.ts
import createIntlMiddleware from 'next-intl/middleware';
 
export default createIntlMiddleware({
  // ... other config
 
  // Default: `true`
  alternateLinks: false
});

Localizing pathnames

If you want to localize the pathnames of your app, you can accomplish this by using appropriate rewrites (opens in a new tab).

next.config.js
const withNextIntl = require('next-intl/plugin')();
 
module.exports = withNextIntl({
  experimental: {appDir: true},
  rewrites() {
    return [
      {
        source: '/de/über',
        destination: '/de/about'
      }
    ];
  }
});

Since next-intl isn't aware of the rewrites you've configured, you likely want to make some adjustments:

  1. Turn off the alternateLinks option in the middleware and provide search engine hints about localized versions of your content (opens in a new tab) by yourself.
  2. Translate the pathnames you're passing to the routing APIs from next-intl (example (opens in a new tab)).

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';

CDN caching

Since next-intl is currently SSR-only, it's a good idea to use CDN caching (opens in a new tab) via headers (opens in a new tab) in next.config.js to get the same level of performance from dynamic rendering as you'd get from static rendering (learn more (opens in a new tab)). Also note that the Next.js Cache (opens in a new tab) improves the performance for dynamically rendered pages.

next.config.js
const ms = require('ms');
const withNextIntl = require('next-intl/plugin')();
 
module.exports = withNextIntl({
  // ... Other config
 
  headers() {
    return [
      {
        // Cache all content pages
        source: '/((?!_next|assets|favicon.ico).*)',
        headers: [
          {
            key: 'Cache-Control',
            value: [
              `s-maxage=` + ms('1d') / 1000,
              `stale-while-revalidate=` + ms('1y') / 1000
            ].join(', ')
          }
        ],
 
        // If you're deploying on a host that doesn't support the `vary` header (e.g. Vercel),
        // make sure to disable caching for prefetch requests for Server Components.
        missing: [
          {
            type: 'header',
            key: 'Next-Router-Prefetch'
          }
        ]
      }
    ];
  }
});

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