Loading...
Loading...
Type-safe i18n for Next.js App Router
npx skill4agent add agents-inc/skills web-i18n-next-intlQuick Guide: Use next-intl for type-safe internationalization in Next.js App Router.for messages,useTranslationsfor dates/numbers, middleware for locale detection. CalluseFormatterfor static rendering.setRequestLocale(locale)
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,, named constants)import type
setRequestLocale(locale)routing.localesNextIntlClientProvidersetRequestLocalesrc/
i18n/
routing.ts # Locale configuration
request.ts # Server-side locale resolution
navigation.ts # Locale-aware Link, useRouter
proxy.ts # Locale detection and routing (middleware.ts before Next.js 16)
app/
[locale]/
layout.tsx # Root layout with NextIntlClientProvider
page.tsx # Pages within locale segment
messages/
en.json # English translations
de.json # German translationsNote: In Next.js 16+,was renamed tomiddleware.ts. If using Next.js 15 or earlier, useproxy.ts.middleware.ts
// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const SUPPORTED_LOCALES = ["en", "de", "fr"] as const;
export const DEFAULT_LOCALE = "en";
export const routing = defineRouting({
locales: SUPPORTED_LOCALES,
defaultLocale: DEFAULT_LOCALE,
});
export type Locale = (typeof routing.locales)[number];// src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});// src/i18n/navigation.ts
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);// src/proxy.ts (Next.js 16+) or src/middleware.ts (Next.js 15 and earlier)
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: "/((?!api|_next|_vercel|.*\\..*).*)",
};// src/app/[locale]/layout.tsx
import { NextIntlClientProvider, hasLocale } from "next-intl";
import { notFound } from "next/navigation";
import { getMessages, setRequestLocale } from "next-intl/server";
import { routing, type Locale } from "@/i18n/routing";
type Props = {
children: React.ReactNode;
params: Promise<{ locale: string }>;
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
setRequestLocale(locale);
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}Note: In next-intl v4.0+,auto-inherits messages from server config. PassingNextIntlClientProviderexplicitly is optional but recommended for clarity.messages
// src/app/[locale]/about/page.tsx
import { useTranslations } from "next-intl";
import { setRequestLocale } from "next-intl/server";
type Props = {
params: Promise<{ locale: string }>;
};
export default async function AboutPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = useTranslations("About");
return (
<article>
<h1>{t("title")}</h1>
<p>{t("description")}</p>
</article>
);
}// messages/en.json
{
"About": {
"title": "About Us",
"description": "Learn more about our company."
}
}const t = useTranslations("Profile");
// Message: "Hello, {name}!"
t("greeting", { name: user.name }); // "Hello, Jane!"
// Message: "You have {count} unread messages"
t("unreadCount", { count: messages.length }); // "You have 5 unread messages"const t = useTranslations("Notifications");
// Renders correct plural form based on locale rules
t("itemCount", { count: items.length });// messages/en.json
{
"Notifications": {
"itemCount": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
}oneotheronefewmanyotherzeroonetwofewmanyother#// Message: "It's your {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!"
t("birthday", { year: 21 }); // "It's your 21st birthday!"t.rich()const t = useTranslations("Legal");
const content = t.rich("terms", {
link: (chunks) => <a href="/terms">{chunks}</a>,
bold: (chunks) => <strong>{chunks}</strong>,
});
return <p>{content}</p>;{
"Legal": {
"terms": "By signing up, you agree to our <link>Terms of Service</link> and <bold>Privacy Policy</bold>."
}
}import { useFormatter } from "next-intl";
function EventDate({ date }: { date: Date }) {
const format = useFormatter();
return (
<time dateTime={date.toISOString()}>
{format.dateTime(date, {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
);
}import { useFormatter, useNow } from "next-intl";
const UPDATE_INTERVAL_MS = 60000;
function RelativeTime({ date }: { date: Date }) {
const format = useFormatter();
const now = useNow({ updateInterval: UPDATE_INTERVAL_MS });
return <time>{format.relativeTime(date, now)}</time>;
}import { useFormatter } from "next-intl";
function Price({ amount, currency }: { amount: number; currency: string }) {
const format = useFormatter();
return (
<span>
{format.number(amount, {
style: "currency",
currency,
})}
</span>
);
}// src/app/[locale]/blog/[slug]/page.tsx
import { setRequestLocale } from "next-intl/server";
import { routing } from "@/i18n/routing";
type Props = {
params: Promise<{ locale: string; slug: string }>;
};
export function generateStaticParams() {
const slugs = ["getting-started", "advanced-features", "faq"];
return routing.locales.flatMap((locale) =>
slugs.map((slug) => ({ locale, slug })),
);
}
export default async function BlogPost({ params }: Props) {
const { locale, slug } = await params;
setRequestLocale(locale);
// Component implementation
}"use client";
import { useLocale } from "next-intl";
import { usePathname, useRouter } from "@/i18n/navigation";
import { routing, type Locale } from "@/i18n/routing";
const LOCALE_LABELS: Record<Locale, string> = {
en: "English",
de: "Deutsch",
fr: "Francais",
};
export function LocaleSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const handleChange = (newLocale: Locale) => {
router.replace(pathname, { locale: newLocale });
};
return (
<select
value={locale}
onChange={(e) => handleChange(e.target.value as Locale)}
aria-label="Select language"
>
{routing.locales.map((loc) => (
<option key={loc} value={loc}>
{LOCALE_LABELS[loc]}
</option>
))}
</select>
);
}AppConfig// src/i18n/types.ts (or global.d.ts)
import type en from "../../messages/en.json";
import { routing } from "./routing";
import type { formats } from "./request";
declare module "next-intl" {
interface AppConfig {
Locale: (typeof routing.locales)[number];
Messages: typeof en;
Formats: typeof formats;
}
}// tsconfig.json (add to compilerOptions)
{
"compilerOptions": {
"allowArbitraryExtensions": true
}
}createMessagesDeclaration// next.config.mjs
import { createNextIntlPlugin } from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin({
experimental: {
createMessagesDeclaration: "./messages/en.json",
},
});
export default withNextIntl({});{name}{count, plural, ...}// src/app/[locale]/page.tsx
import { getTranslations, setRequestLocale } from "next-intl/server";
type Props = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({ params }: Props) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Metadata" });
return {
title: t("title"),
description: t("description"),
};
}
export default async function HomePage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("Home");
return <h1>{t("welcome")}</h1>;
}useTranslationsgetTranslationsNextIntlClientProvideruseLocale()setRequestLocale(locale)paramsNextIntlClientProviderrouting.localesmiddleware.tsproxy.tsgenerateStaticParamst()t.rich()useTranslationsuseTranslationsgenerateMetadatagetTranslationssetRequestLocale(locale)generateMetadatagetTranslationst.rich()chunksuseNow()proxy.tsFor full anti-patterns with code examples, see reference.md.
All code must follow project conventions in CLAUDE.md
setRequestLocale(locale)routing.localesNextIntlClientProvider