next-intl provides internationalization for Next.js App Router — createNavigation({ locales, defaultLocale }) generates typed Link, redirect, usePathname, and useRouter that handle locale prefixes automatically. useTranslations("namespace") returns a typed t() function in Client and Server Components. t("key", { count }) handles ICU pluralization: { count, plural, one {# item} other {# items} }. useFormatter() returns locale-aware format.dateTime(date), format.number(n, { style: "currency" }), and format.list(arr) functions. Messages live in messages/en.json and messages/de.json. createNextIntlPlugin() wraps next.config.js. Middleware with createMiddleware detects browser locale from Accept-Language and redirects to the correct prefix. TypeScript autocompletion works with useTranslations<Messages>(). Claude Code generates next-intl routing configuration, message catalogs, typed translation hooks, pluralization patterns, and locale-aware formatters.
CLAUDE.md for next-intl
## next-intl Stack
- Version: next-intl >= 3.14
- Config: createNextIntlPlugin() in next.config.ts — i18n.ts file with locales + defaultLocale
- Routing: createNavigation({ locales, defaultLocale }) → typed Link, redirect, useRouter, usePathname
- Server: import { getTranslations } from "next-intl/server"; const t = await getTranslations("namespace")
- Client: const t = useTranslations("namespace") — re-renders on locale change
- Format: const format = useFormatter(); format.dateTime(date, { dateStyle: "short" })
- ICU: "{count, plural, =0 {no items} one {# item} other {# items}}"
- Types: TypeScript autocomplete via messages/en.json interface declaration
- Middleware: createMiddleware({ locales, defaultLocale }) in middleware.ts
Project Structure
src/
i18n/
routing.ts — locales, defaultLocale, pathnames
navigation.ts — createNavigation exports
request.ts — getRequestConfig
app/
[locale]/
layout.tsx — NextIntlClientProvider
page.tsx
middleware.ts — locale detection + redirect
messages/
en.json
de.json
fr.json
ja.json
i18n Configuration
// src/i18n/routing.ts — locale configuration
import { defineRouting } from "next-intl/routing"
export const routing = defineRouting({
locales: ["en", "de", "fr", "ja"] as const,
defaultLocale: "en",
// Prefix strategy: "always" (default) | "as-needed" (no prefix for default)
localePrefix: "as-needed",
// Typed pathnames — same path, different URL per locale
pathnames: {
"/": "/",
"/about": {
en: "/about",
de: "/uber-uns",
fr: "/a-propos",
ja: "/について",
},
"/blog": "/blog",
"/blog/[slug]": "/blog/[slug]",
"/products": {
en: "/products",
de: "/produkte",
fr: "/produits",
ja: "/製品",
},
},
})
export type AppLocale = (typeof routing.locales)[number]
export type AppPathnames = keyof typeof routing.pathnames
// src/i18n/navigation.ts — typed navigation utilities
import { createNavigation } from "next-intl/navigation"
import { routing } from "./routing"
// Typed Link, redirect, useRouter, usePathname
export const { Link, redirect, useRouter, usePathname, getPathname } =
createNavigation(routing)
// src/i18n/request.ts — server-side locale resolution
import { getRequestConfig } from "next-intl/server"
import { routing } from "./routing"
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
// Validate that the locale is supported
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
// Time zone for server-side date formatting
timeZone: "UTC",
}
})
Middleware
// middleware.ts — locale detection and routing
import createMiddleware from "next-intl/middleware"
import { routing } from "./src/i18n/routing"
export default createMiddleware(routing)
export const config = {
// Match all routes except static files
matcher: [
"/",
"/(de|fr|ja|en)/:path*",
"/((?!_next|_vercel|.*\\..*).*)".
],
}
Locale Layout
// app/[locale]/layout.tsx — provide messages to Client Components
import { NextIntlClientProvider } from "next-intl"
import { getMessages, getLocale } from "next-intl/server"
import { notFound } from "next/navigation"
import { routing } from "@/i18n/routing"
import type { AppLocale } from "@/i18n/routing"
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 AppLocale)) {
notFound()
}
// Pass all messages to Client Components
const messages = await getMessages()
return (
<html lang={locale} dir={locale === "ar" ? "rtl" : "ltr"}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
Message Catalogs
// messages/en.json
{
"nav": {
"home": "Home",
"about": "About",
"products": "Products",
"blog": "Blog",
"pricing": "Pricing",
"signIn": "Sign in",
"signUp": "Get started"
},
"common": {
"loading": "Loading...",
"error": "Something went wrong",
"retry": "Try again",
"cancel": "Cancel",
"save": "Save changes",
"delete": "Delete",
"edit": "Edit",
"back": "Back"
},
"cart": {
"title": "Shopping cart",
"empty": "Your cart is empty",
"itemCount": "{count, plural, =0 {No items} one {# item} other {# items}}",
"subtotal": "Subtotal",
"total": "Total",
"checkout": "Checkout",
"addedToCart": "{name} added to cart",
"removeItem": "Remove {name} from cart"
},
"product": {
"addToCart": "Add to cart",
"outOfStock": "Out of stock",
"reviewCount": "{count, plural, =0 {No reviews} one {# review} other {# reviews}}",
"rating": "Rated {rating} out of 5",
"price": "{price, number, ::currency/USD}"
},
"errors": {
"required": "{field} is required",
"minLength": "{field} must be at least {min} characters",
"invalidEmail": "Please enter a valid email address",
"404": "Page not found",
"500": "Server error"
}
}
Server and Client Components
// app/[locale]/products/page.tsx — Server Component translations
import { getTranslations } from "next-intl/server"
import { setRequestLocale } from "next-intl/server"
export default async function ProductsPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
// Enable static rendering with locale
setRequestLocale(locale)
const t = await getTranslations("product")
return (
<div>
<h1>Products</h1>
<p>{t("addToCart")}</p>
</div>
)
}
// components/CartBadge.tsx — Client Component translations
"use client"
import { useTranslations, useFormatter } from "next-intl"
export function CartBadge({ count, total }: { count: number; total: number }) {
const t = useTranslations("cart")
const format = useFormatter()
return (
<div>
{/* ICU pluralization */}
<span>{t("itemCount", { count })}</span>
{/* Locale-aware currency formatting */}
<span>{format.number(total / 100, { style: "currency", currency: "USD" })}</span>
</div>
)
}
// components/LocaleSwitcher.tsx — language picker
"use client"
import { useLocale, useTranslations } from "next-intl"
import { useRouter, usePathname } from "@/i18n/navigation"
import { routing } from "@/i18n/routing"
export function LocaleSwitcher() {
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
const localeNames: Record<string, string> = {
en: "English",
de: "Deutsch",
fr: "Français",
ja: "日本語",
}
return (
<select
value={locale}
onChange={e => router.replace(pathname, { locale: e.target.value })}
className="input text-sm"
>
{routing.locales.map(loc => (
<option key={loc} value={loc}>
{localeNames[loc]}
</option>
))}
</select>
)
}
For the react-i18next alternative when an established i18n library with a larger plugin ecosystem, lazy loading of translation namespaces with Suspense, and compatibility with frameworks beyond Next.js is preferred — react-i18next is framework-agnostic and works in React Native, Gatsby, and Vite apps while next-intl is Next.js-specific, see the react-i18next guide. For the Paraglide.js alternative when compile-time message optimization generating typed message functions with zero-runtime overhead is preferred — Paraglide statically extracts translations at build time with smaller bundles than next-intl’s runtime JSON loading, see the Paraglide guide. The Claude Skills 360 bundle includes next-intl skill sets covering routing, message catalogs, and formatting. Start with the free tier to try Next.js i18n generation.