Skip to main content
Technology & EngineeringI18n Services268 lines

Next Intl

"next-intl: Next.js internationalization, message catalogs, routing, middleware, server components, formatters, pluralization"

Quick Summary18 lines
next-intl provides first-class internationalization for Next.js App Router applications. It embraces server components by default, delivering translations without shipping message bundles to the client. The library integrates tightly with Next.js routing conventions, using middleware to negotiate locales and rewrite URLs transparently. Messages live in JSON catalogs keyed by locale, and formatting leverages the ICU MessageFormat standard for plurals, selects, and rich text. The goal is zero-config i18n that feels native to the Next.js developer experience while keeping bundle size minimal.

## Key Points

- Call `setRequestLocale(locale)` at the top of every page and layout for static rendering support.
- Use namespaced messages (`HomePage.title`) to keep catalogs organized and tree-shakeable.
- Provide `IntlMessages` type declaration for compile-time key checking.
- Keep client components minimal; prefer server components that never ship translation bundles.
- Use `localePrefix: "as-needed"` to keep URLs clean for the default locale.
- Define `pathnames` in routing config for SEO-friendly localized slugs.
- Set `timeZone` in request config so date formatting is consistent across SSR and CSR.
- Use `t.rich()` for inline markup instead of `dangerouslySetInnerHTML`.
- Supply `generateStaticParams` returning all locales for static generation.
- Use `format.relativeTime` with `useNow({ updateInterval })` for live-updating relative timestamps.
- Avoid importing the entire messages object on the client; pass only needed namespaces via `NextIntlClientProvider`.
- Do not hardcode locale strings in components; always derive them from `useLocale()` or params.
skilldb get i18n-services-skills/Next IntlFull skill: 268 lines
Paste into your CLAUDE.md or agent config

next-intl

Core Philosophy

next-intl provides first-class internationalization for Next.js App Router applications. It embraces server components by default, delivering translations without shipping message bundles to the client. The library integrates tightly with Next.js routing conventions, using middleware to negotiate locales and rewrite URLs transparently. Messages live in JSON catalogs keyed by locale, and formatting leverages the ICU MessageFormat standard for plurals, selects, and rich text. The goal is zero-config i18n that feels native to the Next.js developer experience while keeping bundle size minimal.

Setup

Installation and project structure

// Install
// npm install next-intl

// Directory layout:
// messages/
//   en.json
//   fr.json
//   de.json
// src/
//   i18n/
//     request.ts
//     routing.ts
//   middleware.ts
//   app/
//     [locale]/
//       layout.tsx
//       page.tsx

Define routing configuration

// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";

export const routing = defineRouting({
  locales: ["en", "fr", "de"],
  defaultLocale: "en",
  localePrefix: "as-needed", // omit prefix for default locale
  pathnames: {
    "/about": {
      en: "/about",
      fr: "/a-propos",
      de: "/ueber-uns",
    },
    "/blog/[slug]": {
      en: "/blog/[slug]",
      fr: "/blogue/[slug]",
      de: "/blog/[slug]",
    },
  },
});

export const { Link, redirect, usePathname, useRouter } =
  createNavigation(routing);

Middleware for locale detection

// src/middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

export default createMiddleware(routing);

export const config = {
  matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

Request-scoped configuration

// src/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(`../../messages/${locale}.json`)).default,
    timeZone: "Europe/London",
    now: new Date(),
  };
});

Root layout with provider

// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { routing } from "@/i18n/routing";
import { notFound } from "next/navigation";

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

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  if (!routing.locales.includes(locale as any)) notFound();
  setRequestLocale(locale);
  const messages = await getMessages();

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

Key Techniques

Message catalogs with ICU MessageFormat

// messages/en.json
{
  "HomePage": {
    "title": "Welcome, {name}!",
    "itemCount": "You have {count, plural, =0 {no items} one {# item} other {# items}} in your cart.",
    "greeting": "{gender, select, male {He} female {She} other {They}} will arrive at {time, time, short}.",
    "richText": "Please read the <link>terms of service</link> before continuing."
  },
  "Navigation": {
    "home": "Home",
    "about": "About",
    "blog": "Blog"
  }
}

Server components — direct usage

// src/app/[locale]/page.tsx
import { useTranslations } from "next-intl";
import { setRequestLocale } from "next-intl/server";

export default function HomePage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = React.use(params);
  setRequestLocale(locale);
  const t = useTranslations("HomePage");

  return (
    <main>
      <h1>{t("title", { name: "Alice" })}</h1>
      <p>{t("itemCount", { count: 3 })}</p>
      <p>
        {t.rich("richText", {
          link: (chunks) => <a href="/terms">{chunks}</a>,
        })}
      </p>
    </main>
  );
}

Client components with selective hydration

"use client";
import { useTranslations, useFormatter, useNow, useLocale } from "next-intl";

export function PriceDisplay({ amount, currency }: { amount: number; currency: string }) {
  const format = useFormatter();
  const locale = useLocale();
  const now = useNow({ updateInterval: 60_000 });
  const t = useTranslations("Product");

  return (
    <div>
      <span>{format.number(amount, { style: "currency", currency })}</span>
      <span>{format.relativeTime(new Date("2026-01-01"), now)}</span>
      <span>{format.dateTime(now, { dateStyle: "full" })}</span>
      <span>
        {format.list(["Red", "Blue", "Green"], { type: "conjunction" })}
      </span>
    </div>
  );
}

Typed message keys

// global.d.ts
import en from "./messages/en.json";

type Messages = typeof en;

declare global {
  interface IntlMessages extends Messages {}
}

Async server-only usage outside components

import { getTranslations, getFormatter } from "next-intl/server";

export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: "Metadata" });
  return {
    title: t("title"),
    description: t("description"),
  };
}

Best Practices

  • Call setRequestLocale(locale) at the top of every page and layout for static rendering support.
  • Use namespaced messages (HomePage.title) to keep catalogs organized and tree-shakeable.
  • Provide IntlMessages type declaration for compile-time key checking.
  • Keep client components minimal; prefer server components that never ship translation bundles.
  • Use localePrefix: "as-needed" to keep URLs clean for the default locale.
  • Define pathnames in routing config for SEO-friendly localized slugs.
  • Set timeZone in request config so date formatting is consistent across SSR and CSR.
  • Use t.rich() for inline markup instead of dangerouslySetInnerHTML.
  • Supply generateStaticParams returning all locales for static generation.
  • Use format.relativeTime with useNow({ updateInterval }) for live-updating relative timestamps.

Anti-Patterns

  • Avoid importing the entire messages object on the client; pass only needed namespaces via NextIntlClientProvider.
  • Do not hardcode locale strings in components; always derive them from useLocale() or params.
  • Do not skip the middleware matcher — without it, static assets get locale-prefixed 404s.
  • Avoid nesting NextIntlClientProvider multiple times; one at the layout root is sufficient.
  • Do not use useTranslations in async server functions; use getTranslations instead.
  • Avoid splitting a single logical page's messages across unrelated namespaces — keep co-located content together.
  • Do not use string concatenation to build translation keys dynamically — this breaks static analysis and type checking.
  • Avoid placing messages/ inside src/ when using the import() pattern — keep them at the project root or adjust paths consistently.

Install this skill directly: skilldb add i18n-services-skills

Get CLI access →