Skip to content

Internationalization in Next.js with next-intl

Internationalization in Next.js with next-intl

Internationalization in Next.js with next-intl

Internationalization (i18n) is essential for providing a multi-language experience for global applications. next-intl integrates well with Next.js’ App Router, handling i18n routing, locale detection, and dynamic configuration. This guide will walk you through setting up i18n in Next.js using next-intl for URL-based routing, user-specific settings, and domain-based locale routing.

Getting Started

First, create a Next.js app with the App Router and install next-intl:

npm install next-intl

Next, configure next-intl in the next.config.ts file to provide a request-specific i18n configuration for Server Components:

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

const withNextIntl = createNextIntlPlugin();

const nextConfig: NextConfig = {
  /* config options here */
};

export default withNextIntl(nextConfig);

Without i18n Routing

Setting up an app without i18n routing integration can be advantageous in scenarios where you want to provide a locale to next-intl based on user-specific settings or when your app supports only a single language. This approach offers the simplest way to begin using next-intl, as it requires no changes to your app’s structure, making it an ideal choice for straightforward implementations.

β”œβ”€β”€ translations
β”‚   β”œβ”€β”€ en.json
β”‚   └── es.json
β”œβ”€β”€ next.config.ts
└── src
    β”œβ”€β”€ i18n
    β”‚   └── request.ts
    └── app
        β”œβ”€β”€ layout.tsx
        └── page.tsx

Here’s a quick explanation of each file's role:

  • translations/: Stores different translations per language (e.g., en.json for English, es.json for Spanish). Organize this as needed, e.g., translations/en/common.json.
  • request.ts: Manages locale-based configuration scoped to each request.

Setup request.ts for Request-Specific Configuration

Since we will be using features from next-intl in Server Components, we need to add the following configuration in i18n/request.ts:

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

export default getRequestConfig(async () => {
  const locale = 'en';

  return {
    locale,
    messages: (await import(`../../translations/${locale}.json`)).default,
  };
});

Here, we define a static locale and use that to determine which translation file to import. The imported JSON data is stored in the message variable, and is returned together with the locale so that we can access them from various components in the application.

Using Translation in RootLayout

Inside RootLayout, we use getLocale() to retrieve the static locale and set the document language for SEO and pass translations to NextIntlClientProvider:

import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const locale = await getLocale();
  const messages = await getMessages();

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

Note that NextIntlClientProvider automatically inherits configuration from i18n/request.ts here, but messages must be explicitly passed.

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

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

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

In case of async components, you can use the awaitable getTranslations function instead:

import { getTranslations } from 'next-intl/server';
import { Link } from '@/i18n/routing';

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

And with that, you have i18n configured and working on your application!
Now, let’s take it a step further by introducing routing. \

With i18n Routing

To set up i18n routing, we need a file structure that separates each language configuration and translation file. Below is the recommended structure:

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

We updated the earlier structure to include some files that we require for routing:

  • routing.ts: Sets up locales, default language, and routing, shared between middleware and navigation.
  • middleware.ts: Handles URL rewrites and locale negotiation.
  • app/[locale]/: Creates dynamic routes for each locale like /en/about and /es/about.

Define Routing Configuration in i18n/routing.ts

The routing.ts file configures supported locales and the default locale, which is referenced by middleware.ts and other navigation functions:

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

export const routing = defineRouting({
  locales: ['en', 'es'], // Supported locales
  defaultLocale: 'en',    // Fallback locale if none matches
});

// Provides wrappers for Next.js navigation APIs to handle locale routing
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);

This configuration lets Next.js handle URL paths like /about, with locale management managed by next-intl.

Update request.ts for Request-Specific Configuration

We need to update the getRequestConfig function from the above implementation in i18n/request.ts.

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

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    messages: (await import(`../../translations/${locale}.json`)).default,
  };
});

Here, request.ts ensures that each request loads the correct translation files based on the user’s locale or falls back to the default.

Setup Middleware for Locale Matching

The middleware.ts file matches the locale based on the request:

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

export default createMiddleware(routing);

export const config = {
  matcher: ['/', '/(es|en)/:path*'], // Matches i18n paths only
};

Middleware handles locale matches and redirects to localized paths like /en or /es.

Updating the RootLayout file

Inside RootLayout, we use the locale from params (matched by middleware) instead of calling getLocale()

import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import '../globals.css';

export default async function RootLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  const { locale } = params;

  if (!routing.locales.includes(locale as any)) {
    notFound();
  }

  const messages = await getMessages();

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

The locale we get from the params was matched in the middleware.ts file and we use that here to set the document language for SEO purposes. Additionally, we used this file to pass configuration from i18n/request.ts to Client Components through NextIntlClientProvider.

Note: When using the above setup with i18n routing, next-intl will currently opt into dynamic rendering when APIs like useTranslations are used in Server Components. next-intl provides a temporary API that can be used to enable static rendering.

Static Rendering for i18n Routes

For apps with dynamic routes, use generateStaticParams to pass all possible locale values, allowing Next.js to render at build time:

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

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

next-intl provides an API setRequestLocale 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. You need to call this function in every layout/page that you intend to enable static rendering for since Next.js can render layouts and pages independently.

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

export default async function RootLayout({ children, params: { locale } }) {
  if (!routing.locales.includes(locale as any)) {
    notFound();
  }

  setRequestLocale(locale);

  return (
    // ...
  );
}

Note: Call setRequestLocale before invoking useTranslations or getMessages or any next-intl functions.

Domain Routing

For domain-specific locale support, use the domains setting to map domains to locales, such as us.example.com/en or ca.example.com/fr.

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

export const routing = defineRouting({
  locales: ['en', 'fr'],
  defaultLocale: 'en',
  domains: [
    { domain: 'us.example.com', defaultLocale: 'en', locales: ['en'] },
    { domain: 'ca.example.com', defaultLocale: 'en' },
  ],
});

This setup allows you to serve localized content based on domains. Read more on domain routing here.

Conclusion

Setting up internationalization in Next.js with next-intl provides a modular way to handle URL-based routing, user-defined locales, and domain-specific configurations. Whether you need URL-based routing or a straightforward single-locale setup, next-intl adapts to fit diverse i18n needs.

With these tools, your app will be ready to deliver a seamless multi-language experience to users worldwide.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co