Web i18n next intl
Skill agents-inc/skills/dist/plugins/web-i18n-next-intl/skills/web-i18n-next-intl
The official skills marketplace for Agents Inc, an agent composition framework that builds stacks and compiles specialized subagents for Claude Code
npx -y skills add agents-inc/skills --skill web-i18n-next-intlAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 18 stars18 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Type-safe i18n for Next.js App Router
SKILL.md
17.9 KB, as published. Nobody here has run it
next-intl Internationalization Patterns
Quick Guide: Use next-intl for type-safe internationalization in Next.js App Router.
useTranslationsfor messages,useFormatterfor dates/numbers, middleware for locale detection. CallsetRequestLocale(locale)for static rendering.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST call setRequestLocale(locale) at the top of ALL page/layout components for static rendering)
(You MUST validate locale against routing.locales before using it)
(You MUST use NextIntlClientProvider in the root layout to enable client-side hooks)
(You MUST use named constants for locale codes - NO inline locale strings)
</critical_requirements>
Auto-detection: next-intl, useTranslations, useFormatter, useLocale, NextIntlClientProvider, i18n routing, locale detection, ICU message format
When to use:
- Implementing internationalization in Next.js App Router
- Rendering localized messages with interpolation and pluralization
- Formatting dates, numbers, and relative time per locale
- Setting up locale-based routing and middleware
- Generating static pages for multiple locales
Key patterns covered:
- Project setup with routing.ts, request.ts, and middleware
- useTranslations hook for messages with ICU syntax
- useFormatter hook for dates, numbers, and lists
- Static rendering with generateStaticParams and setRequestLocale
- TypeScript integration for type-safe translation keys
When NOT to use:
- Simple single-locale applications (skip i18n complexity)
- Pages Router (different API - use Pages Router docs)
- Non-Next.js React applications (use react-intl instead)
Detailed Resources:
- For code examples, see examples/ (core.md, formatting.md, pluralization.md, markup.md)
- For decision frameworks and anti-patterns, see reference.md
<philosophy>
Philosophy
next-intl follows the principle of type-safe, locale-aware rendering with ICU message format support. Translations are organized as namespaced JSON objects, loaded per-request for Server Components and provided via context for Client Components. The middleware handles locale detection automatically, while setRequestLocale enables static rendering at build time.
Core principles:
- Server-first: Load translations in Server Components for better performance
- Type-safe keys: TypeScript augmentation catches missing translations at compile time
- ICU standard: Use industry-standard ICU message syntax for pluralization and formatting
- Static-friendly: Support static generation with explicit locale parameters
<patterns>
Core Patterns
Pattern 1: Project Setup
Set up next-intl with the App Router using the standard file structure.
File Structure
src/
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 translations
Note: In Next.js 16+,
middleware.tswas renamed toproxy.ts. If using Next.js 15 or earlier, usemiddleware.ts.
Configuration Files
// 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];
Why good: named constants for locales enable type-safe usage throughout app, exported Locale type enables type checking of locale parameters
// 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,
};
});
Why good: validates locale against supported list, falls back to default for invalid locales, dynamically imports only needed translation file
// src/i18n/navigation.ts
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);
Why good: wraps Next.js navigation APIs with locale awareness, Link automatically includes locale prefix
// 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|.*\\..*).*)",
};
Why good: proxy/middleware handles locale detection from URL, cookies, and Accept-Language header, matcher excludes API routes and static files. Add additional exclusions for your API framework routes as needed.
Pattern 2: Root Layout with Provider
Wrap the application with NextIntlClientProvider and validate the locale.
// 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>
);
}
Why good: validates locale and returns 404 for invalid locales, setRequestLocale enables static rendering, generateStaticParams pre-renders all locale variants, explicit messages prop ensures Client Components receive translations, html lang attribute improves accessibility
Note: In next-intl v4.0+,
NextIntlClientProviderauto-inherits messages from server config. Passingmessagesexplicitly is optional but recommended for clarity.
Pattern 3: useTranslations Hook
Use the useTranslations hook for rendering localized messages.
Basic Usage
// 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."
}
}
Why good: namespaced translations keep messages organized, setRequestLocale at top of component enables static rendering
With Interpolation
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"
Why good: named placeholders are explicit and refactorable, TypeScript can validate placeholder names with augmentation
Pattern 4: Pluralization with ICU Syntax
Use ICU plural syntax for count-based 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}}"
}
}
Plural categories by language:
- English:
one,other - Russian:
one,few,many,other - Arabic:
zero,one,two,few,many,other
Why good: ICU plural syntax handles locale-specific plural rules automatically, # is replaced with the formatted count
Ordinal Pluralization
// Message: "It's your {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!"
t("birthday", { year: 21 }); // "It's your 21st birthday!"
Pattern 5: Rich Text Formatting
Use t.rich() for messages containing markup.
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>."
}
}
Why good: keeps translation strings complete and translatable, markup tags are defined by developers and can be React components
Pattern 6: useFormatter Hook
Use useFormatter for locale-aware formatting of dates, numbers, and lists.
Date and Time Formatting
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>
);
}
Why good: uses Intl.DateTimeFormat under the hood, respects locale-specific date formats automatically
Relative Time with useNow
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>;
}
Why good: useNow provides a reactive "now" value that updates on interval, relative time updates automatically
Number and Currency Formatting
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>
);
}
Why good: handles locale-specific number formatting (1,234.56 vs 1.234,56), currency symbols and positions vary by locale
Pattern 7: Static Rendering with generateStaticParams
Enable static generation for all locale variants.
// 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
}
Why good: generates all combinations of locales and slugs at build time, setRequestLocale enables next-intl to work in static context
Pattern 8: Locale Switching
Implement a locale switcher component using next-intl navigation.
"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>
);
}
Why good: uses next-intl navigation APIs to preserve current path, aria-label provides accessibility, type-safe locale handling
Pattern 9: TypeScript Integration
Enable type-safe translation keys with TypeScript augmentation using the AppConfig interface (next-intl v4.0+).
Configuration
// 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
}
}
Why good: typos in translation keys become compile-time errors, IDE autocomplete for translation keys, strictly-typed locales prevent invalid locale strings, Formats registration enables type-safe formatting
Optional: Type-Safe Message Arguments (Experimental)
For automatic type inference of ICU message arguments, configure createMessagesDeclaration:
// next.config.mjs
import { createNextIntlPlugin } from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin({
experimental: {
createMessagesDeclaration: "./messages/en.json",
},
});
export default withNextIntl({});
This generates type declarations enabling autocomplete for message arguments like {name} or {count, plural, ...}.
Pattern 10: Server Actions and Metadata
Use async getTranslations for Server Actions and Metadata.
// 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>;
}
Why good: getTranslations works in async contexts like generateMetadata, locale parameter is required for metadata since it runs outside component tree
</patterns><integration>
Integration Notes
- Server Components first: Load translations in Server Components for performance; use
useTranslations(sync) orgetTranslations(async) - Client Components: Wrap with
NextIntlClientProviderfor hooks access; locale switching and interactive features live here - Locale state: Managed entirely by next-intl - read with
useLocale(), never store separately
<red_flags>
RED FLAGS
High Priority Issues:
- Missing
setRequestLocale(locale)in page/layout components -- breaks static rendering - Not awaiting
paramsin App Router -- params is a Promise in Next.js 15+, causes runtime errors - Missing
NextIntlClientProviderin root layout -- Client Components cannot access translations - Hardcoded locale strings -- use named constants from routing.ts
- Not validating locale against
routing.locales-- invalid locales cause cryptic errors - Using
middleware.tson Next.js 16+ -- must rename toproxy.ts
Medium Priority Issues:
- Missing
generateStaticParamsfor static routes -- forces dynamic rendering - Using
t()instead oft.rich()for messages with markup -- returns string, not ReactNode - Missing namespace in
useTranslations-- all keys become global, conflicts likely - Using
useTranslationsingenerateMetadata-- usegetTranslationsinstead
Gotchas & Edge Cases:
setRequestLocale(locale)must be called at the TOP of components, before any hooksgenerateMetadataruns outside the component tree -- requires explicit locale param togetTranslationst.rich()tag functions receivechunks(ReactNode[]), not a single elementuseNow()only updates on client -- SSR shows initial value until hydration- v4.0+ GDPR cookie changes: locale cookies now expire when browser closes and are only set when user switches locale
- Next.js 16:
proxy.tsruns on Node.js runtime, not Edge
For full anti-patterns with code examples, see reference.md.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST call setRequestLocale(locale) at the top of ALL page/layout components for static rendering)
(You MUST validate locale against routing.locales before using it)
(You MUST use NextIntlClientProvider in the root layout to enable client-side hooks)
(You MUST use named constants for locale codes - NO inline locale strings)
Failure to follow these rules will break static generation and cause runtime errors with invalid locales.
</critical_reminders>