React Internationalization: next-intl in Practice
AI generated
</>
{ }
React · next-intl · i18n · Next.js · Translations
React Internationalization:
next-intl in the Next.js App Router in Practice

Internationalization in React is often underestimated, right up until the first multi-language requirement lands in the backlog. next-intl offers a complete i18n solution for the Next.js App Router: type-safe translations, ICU pluralization, locale-aware date and number formatting, language routing and full Server Component support without any client bundle overhead.

16 min read useTranslations · useFormatter · Middleware · Server Components · ICU next-intl 3.x · Next.js 14+ · TypeScript

1. Why next-intl for React i18n?

React internationalization with next-intl differs fundamentally from older i18n solutions such as react-i18next or i18next. The decisive difference: next-intl was designed explicitly for the Next.js App Router and the Server Component model. That means translations can be retrieved in Server Components without any client bundle overhead whatsoever - no JavaScript library is sent to the client when text is only rendered server-side. That is a significant advantage, particularly for SEO-relevant content.

Another advantage: next-intl natively uses the ICU Message Format and the ECMA-402 standard for date, number and currency formatting. That means you get Intl.DateTimeFormat and Intl.NumberFormat automatically configured for the current locale, without any external libraries. Type-safe translation keys via TypeScript and automatic locale routing through middleware round out the package. next-intl is the most complete React internationalization solution currently available that is integrated with Next.js this deeply.

2. Setup: middleware, routing and message files

The next-intl setup consists of three parts: middleware for locale detection and routing, a routing configuration and message files per language. The middleware reads the currently active locale from the URL, the Accept-Language header or a cookie and redirects accordingly. The routing configuration defines which locales are supported and which one is the default locale. The message files are JSON files, typically located at messages/de.json and messages/en.json.

The directory structure in the App Router follows a fixed pattern: all localized pages live inside a [locale] segment, for example app/[locale]/page.tsx. The middleware intercepts requests that do not yet contain a locale segment and redirects them accordingly. The next.config.js wires in the next-intl plugin, which automatically makes the message files available under the correct route. These three pieces together form the foundation that every other next-intl feature builds on.


// middleware.ts - locale detection and routing
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  // Match all routes except static files and API routes
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
};

// i18n/routing.ts - central routing configuration
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['de', 'en', 'fr'],
  defaultLocale: 'de',
  // Optional: prefix-based routing strategy
  localePrefix: 'as-needed', // 'de' as root, 'en' as /en/...
});

// i18n/request.ts - provide messages to server components
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  const locale = await requestLocale;
  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default,
  };
});

3. useTranslations: retrieving text safely and type-safely

The useTranslations hook is the primary interface for translations in Client Components. It accepts an optional namespace parameter that targets a nested section of the JSON message file. The result is a function t that is called with a key and returns the translated text. With TypeScript and the next-intl TypeScript plugin, keys get full autocompletion; a typo in a key becomes a TypeScript error rather than a silent failure at runtime.

The t function supports rich text via React components: t.rich('key', { strong: (chunks) => <strong>{chunks}</strong> }) renders parts of a translation in bold without writing any HTML into the JSON file. That cleanly separates styling from content and keeps the message files readable for translators. For HTML entities and plain text there is t.markup and t.raw. The namespace approach structures translations by component or page area and prevents a single giant JSON file from becoming unmanageable.

4. ICU Message Format: pluralization and variables

The ICU Message Format is the industry standard for multi-language text with dynamic values and pluralization. next-intl supports it fully. A simple variable is embedded with curly braces: "greeting": "Hello, {name}!", called with t('greeting', { name: 'Max' }). Pluralization follows ICU syntax: "items": "{count, plural, =0 {No items} one {One item} other {# items}}". The # placeholder is replaced with the actual count.

Advanced ICU features include select for enumerated values, for example to generate gender-specific text, and nested plural blocks for complex grammatical structures. This is particularly relevant for languages such as Russian or Polish, which have several plural forms. next-intl internally uses the ECMA-402 pluralizer, which knows the correct plural rules for every language. Developers only need to specify the ICU syntax correctly in the JSON files; the right rule is applied automatically.


// messages/en.json - ICU message format examples
// {
//   "Cart": {
//     "title": "Cart",
//     "itemCount": "{count, plural, =0 {Empty} one {# item} other {# items}}",
//     "welcome": "Hello, {name}! You have {count, plural, one {# new} other {# new}} messages.",
//     "discount": "You save {amount, number, ::currency/EUR}",
//     "lastSeen": "Last seen: {date, date, long}"
//   }
// }

'use client';
import { useTranslations, useFormatter } from 'next-intl';

function Cart({ itemCount, userName }: { itemCount: number; userName: string }) {
  const t = useTranslations('Cart');
  const format = useFormatter();

  return (
    <div>
      <h1>{t('title')}</h1>
      {/* Pluralization handled automatically via ICU */}
      <p>{t('itemCount', { count: itemCount })}</p>

      {/* Variable interpolation with plural */}
      <p>{t('welcome', { name: userName, count: 3 })}</p>

      {/* Rich text: render parts with React components */}
      <p>
        {t.rich('discount', {
          amount: 12.5,
          // Wrap amount in strong - styling stays in component
          number: (chunks) => <strong className="text-green-600">{chunks}</strong>,
        })}
      </p>

      {/* Date formatting using the active locale automatically */}
      <p>{format.dateTime(new Date(), { dateStyle: 'long' })}</p>
    </div>
  );
}

5. useFormatter: dates, numbers and currencies by locale

useFormatter gives access to locale-aware formatting functions without having to manually configure Intl.DateTimeFormat or Intl.NumberFormat. The current locale is picked up automatically from the next-intl context. format.dateTime formats date and time according to regional conventions: in Germany that is DD.MM.YYYY, in the US it is MM/DD/YYYY. format.number formats numbers with the correct decimal and thousands separators. format.relativeTime outputs relative time expressions such as "3 minutes ago" or "in 2 hours" in the language of the current locale.

For currencies, format.number offers the style: 'currency' mode: the number is formatted according to ECMA-402, the currency symbol is placed correctly and the separator is set in a locale-conformant way. That matters a great deal in e-commerce applications, where prices need to be displayed correctly across different markets: 1.234,56 EUR in Germany, GBP 1,234.56 in the UK, USD 1,234.56 in the United States. next-intl ensures that the same number is formatted correctly in every locale, without manually passing the locale to every formatting function.

6. Translations in Server Components

The decisive advantage of next-intl over react-i18next in the Next.js App Router: translations can be retrieved directly in Server Components, without any client-side code. Instead of useTranslations, the async getTranslations function from next-intl/server is used. This function reads the current locale from the request context and returns the translation function, awaited synchronously within the Server Component context. That means: all i18n code stays on the server, and no i18n library ends up in the client bundle.

For metadata and SEO, next-intl also offers getTranslations inside Next.js's generateMetadata function. Page titles, descriptions and Open Graph tags can thus be translated directly, without the client needing to load any JavaScript for it. For statically generated pages (generateStaticParams), next-intl returns all configured locales so that every page can be statically built for every language, which is ideal for Core Web Vitals and Time to First Byte.


// app/[locale]/products/page.tsx - Server Component with translations
import { getTranslations } from 'next-intl/server';
import { setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next';

// Generate static params for all locales (static site generation)
export function generateStaticParams() {
  return [{ locale: 'de' }, { locale: 'en' }, { locale: 'fr' }];
}

// Translated metadata - no client JS needed
export async function generateMetadata({ params }: { params: { locale: string } }): Promise<Metadata> {
  const t = await getTranslations({ locale: params.locale, namespace: 'ProductsPage' });
  return {
    title: t('metaTitle'),
    description: t('metaDescription'),
  };
}

// Server Component - translations on the server, zero client bundle
export default async function ProductsPage({ params }: { params: { locale: string } }) {
  setRequestLocale(params.locale); // enable static rendering
  const t = await getTranslations('ProductsPage');

  return (
    <main>
      <h1>{t('heading')}</h1>
      <p>{t('intro')}</p>
      {/* Client component receives only the data it needs - not translation strings */}
      <ProductList />
    </main>
  );
}

7. Language routing: Link and useRouter with locale

next-intl exports its own versions of Link and useRouter that automatically insert the current locale into URLs. Instead of using Next.js's own Link and useRouter, you import them from next-intl, or more precisely from the generated navigation module produced by createNavigation. That way all internal links correctly land under /en/products or /de/produkte, without the locale having to be passed explicitly.

There is no built-in language switcher component for switching languages; that is intentional, because the UI design of a language switcher is application-specific. The implementation is simple: with next-intl's useRouter, you can call router.replace(pathname, { locale: 'en' }) to open the current page in a different language. The locale is stored in the URL or a cookie, the middleware detects it on the next request and renders the page accordingly. An alternative approach: link directly to the localized URL via Link and its locale prop.

8. Type-safe translation keys with TypeScript

With the TypeScript plugin, next-intl offers complete type safety for translation keys. After a one-time configuration in global.d.ts, every key passed to t() is checked against the message files. Typos in keys, missing translations for individual languages and incorrect parameter types are caught at compile time. That is a substantial improvement over the usual pattern where missing translations only show up at runtime as an empty string.

Type safety also extends to parameters: if a key expects a name parameter according to the JSON file, omitting that parameter results in a TypeScript error. If a key expects a number for pluralization, passing a string as the parameter is an error. This precision is especially valuable in larger teams, where translation keys are used by different developers. The JSON message file effectively becomes a type definition that improves the entire development experience.

9. next-intl vs. react-i18next compared

react-i18next is the older, more mature solution with a huge ecosystem. next-intl is younger, but built explicitly for the Next.js App Router and offers decisive advantages in that context. A direct comparison shows where the differences lie.

Feature react-i18next next-intl Winner
Server Components Limited, workarounds needed Full support (getTranslations) next-intl
TypeScript Types via i18next.d.ts Plugin, fully integrated next-intl
Ecosystem Very large, many plugins Smaller, Next.js focused react-i18next
Routing integration Manual / next-i18next needed Built in, middleware included next-intl
ICU format Plugin required Native support next-intl

For new Next.js App Router projects, next-intl is the recommended choice. For existing projects that already work with react-i18next and have no need for Server Component translations, migration is only worthwhile if the advantages mentioned above are genuinely needed. react-i18next remains the better choice for framework-agnostic React applications outside of Next.js.

Mironsoft

React internationalization, next-intl setup and multi-language Next.js apps

Building a multi-language React app with next-intl?

We implement complete i18n infrastructure with next-intl for the Next.js App Router: locale routing, type-safe translations, Server Component support, pluralization and formatting for every target market.

i18n setup

Set up middleware, routing configuration and message file structure

Migration

Migrate existing react-i18next projects to next-intl

TypeScript integration

Type-safe keys, automatic type inference and CI checks

10. Summary

next-intl is the most complete React internationalization solution for the Next.js App Router. Setup via middleware and routing configuration is clearly structured. useTranslations for Client Components and getTranslations for Server Components offer consistent APIs across both rendering contexts. ICU Message Format solves pluralization and dynamic text without proprietary syntax. useFormatter formats dates, numbers and currencies correctly per locale without manual configuration. Type-safe keys via the TypeScript plugin catch errors at compile time.

The most important practical rules: split translations by component into namespaces to avoid large monolithic JSON files. Use getTranslations in Server Components to keep i18n code out of the client bundle. Use ICU format for all dynamic text with variables and pluralization. Call setRequestLocale in Server Components to enable static rendering. Use the generated navigation module from createNavigation instead of Next.js's own Link and useRouter.

next-intl React Internationalization - The Essentials at a Glance

Setup

Middleware for locale detection, routing configuration with defineRouting, message files per language under messages/[locale].json.

Server vs. Client

useTranslations for Client Components, getTranslations (async) for Server Components. Server-side i18n produces no client bundle overhead.

ICU & formatting

ICU Message Format for pluralization and variables. useFormatter for dates, numbers and currencies by active locale, no manual Intl needed.

TypeScript

Enable the plugin for type-safe keys and parameters. Missing translations and typos are caught at compile time.

11. FAQ: React Internationalization with next-intl

1What is next-intl and why use it for React i18n?
i18n library for the Next.js App Router: Server Component support, ICU format, built-in locale routing, TypeScript integration. The most complete React i18n solution for Next.js 13+.
2How does locale routing work?
Middleware reads the locale from the URL, Accept-Language or a cookie. All pages live under app/[locale]/. Configured once in middleware.ts via createMiddleware.
3useTranslations vs. getTranslations?
useTranslations for Client Components (hook). getTranslations for Server Components and generateMetadata (async function). Both return the same t function.
4How does pluralization work?
ICU Message Format in JSON: {count, plural, =0 {None} one {One} other {#}}. ECMA-402 plural rules applied automatically for every language.
5How do I implement a language switcher?
router.replace(pathname, { locale: 'en' }) with next-intl's useRouter. Or link directly to the localized URL with Link from next-intl and the locale prop.
6Can I use next-intl without Next.js?
No, next-intl relies on Next.js-specific APIs. For other frameworks: react-intl (Format.js) or react-i18next is recommended.
7Lazy loading translations?
Load messages dynamically via import() in getRequestConfig. Split namespaces into separate JSON files and import them only when needed.
8What is setRequestLocale?
Must be called in Server Components so Next.js knows the locale for static generation. Without the call: dynamic rendering instead of static.
9Formatting dates and currency?
useFormatter provides format.dateTime(), format.number() and format.relativeTime(), automatically using the active locale. No manual Intl needed.
10Enabling type-safe translation keys?
Derive the Messages type from JSON in global.d.ts and register it as IntlMessages. After that, all keys in useTranslations are type-safe and autocompleted.