Skip to content

Latest commit

Β 

History

History
409 lines (290 loc) Β· 13.2 KB

with-i18n-routing.mdx

File metadata and controls

409 lines (290 loc) Β· 13.2 KB

import {Tabs} from 'nextra/components'; import Callout from '@/components/Callout'; import Steps from '@/components/Steps'; import Details from '@/components/Details';

App Router setup with i18n routing

In order to use unique pathnames for every language that your app supports, next-intl can be used to handle the following routing setups:

  1. Prefix-based routing (e.g. /en/about)
  2. Domain-based routing (e.g. en.example.com/about)

In either case, next-intl integrates with the App Router by using a top-level [locale] dynamic segment that can be used to provide content in different languages.

Getting started

If you haven't done so already, create a Next.js app that uses the App Router and run:

npm install next-intl

Now, we're going to create the following file structure:

β”œβ”€β”€ messages
β”‚   β”œβ”€β”€ en.json
β”‚   └── ...
β”œβ”€β”€ next.config.ts
└── src
    β”œβ”€β”€ i18n
    β”‚   β”œβ”€β”€ routing.ts
    β”‚   β”œβ”€β”€ navigation.ts
    β”‚   └── request.ts
    β”œβ”€β”€ middleware.ts
    └── app
        └── [locale]
            β”œβ”€β”€ layout.tsx
            └── page.tsx

In case you're migrating an existing app to next-intl, you'll typically move your existing pages into the [locale] folder as part of the setup.

Let's set up the files:

messages/en.json [#messages]

Messages represent the translations that are available per language and can be provided either locally or loaded from a remote data source.

The simplest option is to add JSON files in your local project folder:

{
  "HomePage": {
    "title": "Hello world!",
    "about": "Go to the about page"
  }
}

next.config.ts [#next-config]

Now, set up the plugin which creates an alias to provide a request-specific i18n configuration like your messages to Server Componentsβ€”more on this in the following steps.

<Tabs items={['next.config.ts', 'next.config.js']}> <Tabs.Tab>

import {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

const nextConfig: NextConfig = {};

const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

</Tabs.Tab> <Tabs.Tab>

const createNextIntlPlugin = require('next-intl/plugin');

const withNextIntl = createNextIntlPlugin();

/** @type {import('next').NextConfig} */
const nextConfig = {};

module.exports = withNextIntl(nextConfig);

</Tabs.Tab>

src/i18n/routing.ts [#i18n-routing]

We'll integrate with Next.js' routing in two places:

  1. Middleware: Negotiates the locale and handles redirects & rewrites (e.g. / β†’ /en)
  2. Navigation APIs: Lightweight wrappers around Next.js' navigation APIs like <Link />

This enables you to work with pathnames like /about, while i18n aspects like language prefixes are handled behind the scenes.

To share the configuration between these two places, we'll set up routing.ts:

import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  // A list of all locales that are supported
  locales: ['en', 'de'],

  // Used when no locale matches
  defaultLocale: 'en'
});

Depending on your requirements, you may wish to customize your routing configuration laterβ€”but let's finish with the setup first.

src/i18n/navigation.ts [#i18n-navigation]

Once we have our routing configuration in place, we can use it to set up the navigation APIs.

import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';

// Lightweight wrappers around Next.js' navigation
// APIs that consider the routing configuration
export const {Link, redirect, usePathname, useRouter, getPathname} =
  createNavigation(routing);

src/middleware.ts [#middleware]

Additionally, we can use our routing configuration to set up the middleware.

import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  // Match all pathnames except for
  // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
  // - … the ones containing a dot (e.g. `favicon.ico`)
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
How can I match pathnames that contain dots like `/users/jane.doe`?

If you have pathnames where dots are expected, you can match them with explicit entries:

// ...

export const config = {
  matcher: [
    // Match all pathnames except for
    // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
    // - … the ones containing a dot (e.g. `favicon.ico`)
    '/((?!api|trpc|_next|_vercel|.*\\..*).*)'

    // Match all pathnames within `{/:locale}/users`
    '/([\\w-]+)?/users/(.+)'
  ];
}

This will match e.g. /users/jane.doe, also optionally with a locale prefix.

src/i18n/request.ts [#i18n-request]

When using features from next-intl in Server Components, the relevant configuration is read from a central module that is located at i18n/request.ts by convention. This configuration is scoped to the current request and can be used to provide messages and other options based on the user's locale.

import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({requestLocale}) => {
  // Typically corresponds to the `[locale]` segment
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default
  };
});
Can I move this file somewhere else?

This file is supported out-of-the-box as ./i18n/request.ts both in the src folder as well as in the project root with the extensions .ts, .tsx, .js and .jsx.

If you prefer to move this file somewhere else, you can optionally provide a path to the plugin:

const withNextIntl = createNextIntlPlugin(
  // Specify a custom path here
  './somewhere/else/request.ts'
);

src/app/[locale]/layout.tsx [#layout]

The locale that was matched by the middleware is available via the locale param and can be used to configure the document language. Additionally, we can use this place to pass configuration from i18n/request.ts to Client Components via NextIntlClientProvider.

import {NextIntlClientProvider, hasLocale} from 'next-intl';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';
import type { PropsWithChildren } from 'react';

export default async function LocaleLayout({
  children,
  params
}: PropsWithChildren<{ readonly params: Promise<{ locale: string }> }) {
  // Ensure that the incoming `locale` is valid
  const {locale} = await params;
  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

src/app/[locale]/page.tsx [#page]

And that's it!

Now you can use translations and other functionality from next-intl in your components:

import {useTranslations} from 'next-intl';
import {Link} from '@/i18n/navigation';

export default function HomePage() {
  const t = useTranslations('HomePage');
  return (
    <div>
      <h1>{t('title')}</h1>
      <Link href="/about">{t('about')}</Link>
    </div>
  );
}

In case you ran into an issue, have a look at the App Router example to explore a working app.

Next steps:

  • [Usage guide](/docs/usage): Learn how to format messages, dates and times
  • [Routing](/docs/routing): Set up localized pathnames, domain-based routing & more
  • [Workflows](/docs/workflows): Integrate deeply with TypeScript and other tools

Static rendering

When using the setup with i18n routing, next-intl will currently opt into dynamic rendering when APIs like useTranslations are used in Server Components. This is a limitation that we aim to remove in the future, but as a stopgap solution, next-intl provides a temporary API that can be used to enable static rendering.

Add generateStaticParams

Since we are using a dynamic route segment for the [locale] param, we need to pass all possible values to Next.js via generateStaticParams so that the routes can be rendered at build time.

Depending on your needs, you can add generateStaticParams either to a layout or pages:

  1. Layout: Enables static rendering for all pages within this layout (e.g. app/[locale]/layout.tsx)
  2. Individual pages: Enables static rendering for a specific page (e.g. app/[locale]/page.tsx)

Example:

import {routing} from '@/i18n/routing';

export function generateStaticParams() {
  return routing.locales.map((locale) => ({locale}));
}

Add setRequestLocale to all relevant layouts and pages

next-intl provides an API that can be used to distribute the locale that is received via params in layouts and pages for usage in all Server Components that are rendered as part of the request.

import {setRequestLocale} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';

export default async function LocaleLayout({children, params}) {
  const {locale} = await params;
  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }

  // Enable static rendering
  setRequestLocale(locale);

  return (
    // ...
  );
}
import {setRequestLocale} from 'next-intl/server';

export default function IndexPage({params}) {
  const {locale} = use(params);

  // Enable static rendering
  setRequestLocale(locale);

  // Once the request locale is set, you
  // can call hooks from `next-intl`
  const t = useTranslations('IndexPage');

  return (
    // ...
  );
}

Keep in mind that:

  1. The locale that you pass to setRequestLocale should be validated (e.g. in your root layout).
  2. You need to call this function in every page and every layout that you intend to enable static rendering for since Next.js can render layouts and pages independently.
  3. setRequestLocale needs to be called before you invoke any functions from next-intl like useTranslations or getMessages.
How does setRequestLocale work?

next-intl uses cache() to create a mutable store that holds the current locale. By calling setRequestLocale, the current locale will be written to the store, making it available to all APIs that require the locale.

Note that the store is scoped to a request and therefore doesn't affect other requests that might be handled in parallel while a given request resolves asynchronously.

Why is this API necessary?

Next.js currently doesn't provide an API to read route params like locale at arbitrary places in Server Components (see vercel/next.js#58862). The locale is fundamental to all APIs provided by next-intl, therefore passing this as a prop throughout the tree doesn't stand out as particularly ergonomic.

Due to this, next-intl uses its middleware to attach an x-next-intl-locale header to the incoming request, holding the negotiated locale as a value. This technique allows the locale to be read at arbitrary places via headers().get('x-next-intl-locale').

However, the usage of headers opts the route into dynamic rendering.

By using setRequestLocale, you can provide the locale that is received in layouts and pages via params to next-intl. All APIs from next-intl can now read from this value instead of the header, enabling static rendering.

Use the locale param in metadata

In addition to the rendering of your pages, also page metadata needs to qualify for static rendering.

To achieve this, you can forward the locale that you receive from Next.js via params to the awaitable functions from next-intl.

import {getTranslations} from 'next-intl/server';

export async function generateMetadata({params}) {
  const {locale} = await params;
  const t = await getTranslations({locale, namespace: 'Metadata'});

  return {
    title: t('title')
  };
}