Next Intl
"next-intl: Next.js internationalization, message catalogs, routing, middleware, server components, formatters, pluralization"
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 linesnext-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
IntlMessagestype 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
pathnamesin routing config for SEO-friendly localized slugs. - Set
timeZonein request config so date formatting is consistent across SSR and CSR. - Use
t.rich()for inline markup instead ofdangerouslySetInnerHTML. - Supply
generateStaticParamsreturning all locales for static generation. - Use
format.relativeTimewithuseNow({ 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
NextIntlClientProvidermultiple times; one at the layout root is sufficient. - Do not use
useTranslationsin async server functions; usegetTranslationsinstead. - 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/insidesrc/when using theimport()pattern — keep them at the project root or adjust paths consistently.
Install this skill directly: skilldb add i18n-services-skills
Related Skills
Crowdin
"Crowdin: translation management, OTA content delivery, API, CLI, GitHub integration, in-context editing, glossaries"
I18next
"i18next: internationalization framework, React (react-i18next), namespaces, interpolation, plurals, backends, language detection"
Lokalise
"Lokalise: translation management, API, SDKs, OTA updates, branching, screenshots, plural forms, CLI"
Phrase
"Phrase (formerly PhraseApp): translation management, API, CLI, OTA, in-context editor, branching, webhooks, glossaries"
Pontoon
"Mozilla Pontoon: open-source translation management, Fluent/FTL support, in-place editing, community localization, VCS sync"
Transifex
"Transifex: translation management, Native SDK, OTA delivery, API v3, CLI, GitHub/GitLab integration, ICU plurals"