next-intl (App Router)
Setup and usage of next-intl with prefix-based locale routing (e.g. /en/about , /ja/about ). Use this skill in any Next.js App Router project.
Example code: Copy-paste examples live in this skill's examples/ folder. See examples/README.md for where each file goes in your project.
File layout
Keep this structure:
├── messages/ │ ├── en.json │ ├── ja.json │ └── ... ├── next.config.ts └── src/ ├── i18n/ │ ├── request.ts │ ├── routing.ts │ └── navigation.ts ├── proxy.ts # Next.js 16+ (was middleware.ts) └── app/ ├── layout.tsx # Root layout, no NextIntlClientProvider here └── [locale]/ ├── layout.tsx ├── page.tsx └── ...
Root layout does not wrap with NextIntlClientProvider ; only app/[locale]/layout.tsx does.
- Next config
Wire the plugin (default path ./i18n/request.ts ):
// next.config.ts import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin";
const nextConfig: NextConfig = { /* ... */ }; const withNextIntl = createNextIntlPlugin(); export default withNextIntl(nextConfig);
Custom path: createNextIntlPlugin('./src/i18n/request.ts') .
- Routing config
Central config in src/i18n/routing.ts :
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({ locales: ["en", "ja", "zh-CN", "zh-TW"], defaultLocale: "en", });
- Request config
src/i18n/request.ts : resolve locale from the [locale] segment and load messages.
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,
};
});
- Proxy / middleware (Next.js 16)
Next.js 16 uses proxy.ts instead of middleware.ts . Same API:
// src/proxy.ts import createMiddleware from "next-intl/middleware"; import { routing } from "./i18n/routing";
export const proxy = createMiddleware(routing);
export const config = { matcher: "/((?!api|trpc|_next|_vercel|.\..).*)", };
Matcher: all pathnames except /api , /trpc , /_next , /_vercel , and paths containing a dot (e.g. favicon.ico ).
- Navigation helpers
Use project navigation wrappers so links keep the current locale:
// src/i18n/navigation.ts import { createNavigation } from "next-intl/navigation"; import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);
In components: import Link (and others) from @/i18n/navigation , not from next/navigation or next/link , for locale-aware URLs. Example: examples/Nav-client.tsx, examples/BackToHomeButton.tsx.
- Locale layout and static rendering
app/[locale]/layout.tsx must (full file: examples/app-locale-layout.tsx):
-
Validate locale with hasLocale → notFound() if invalid.
-
Call setRequestLocale(locale) for static rendering.
-
Wrap children with NextIntlClientProvider and getMessages() .
// app/[locale]/layout.tsx import { NextIntlClientProvider, hasLocale } from "next-intl"; import { setRequestLocale } from "next-intl/server"; import { notFound } from "next/navigation"; import { routing } from "@/i18n/routing"; import { getMessages } from "next-intl/server";
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 ( <NextIntlClientProvider messages={messages}> {children} </NextIntlClientProvider> ); }
- Pages under [locale]
For static rendering, every page under [locale] that uses next-intl must call setRequestLocale(locale) (and use use(params) if needed). Examples: app-locale-page.tsx, app-locale-about-page.tsx. (and use use(params) if needed). Layout already sets it; pages that render server components using locale should set it too.
// app/[locale]/page.tsx import { use } from "react"; import { setRequestLocale } from "next-intl/server";
export default function IndexPage({ params, }: { params: Promise<{ locale: string }>; }) { const { locale } = use(params); setRequestLocale(locale); return <TokyoPage />; }
// app/[locale]/about/page.tsx import { use } from "react"; import { setRequestLocale } from "next-intl/server"; import AboutContainer from "./components/AboutContainer";
export default function AboutPage({ params, }: { params: Promise<{ locale: string }>; }) { const { locale } = use(params); setRequestLocale(locale); return <AboutContainer />; }
Call setRequestLocale before any next-intl APIs in that layout/page.
- Using translations
Client components: useTranslations(namespace) :
"use client"; import { useTranslations } from "next-intl"; import { Link } from "@/i18n/navigation";
export default function BackToHomeButton() { const t = useTranslations("BackToHomeButton"); return ( <Link href="/"> <span>{t("buttonText")}</span> </Link> ); }
"use client"; import { useTranslations } from "next-intl"; import { Link } from "@/i18n/navigation";
export default function Nav() { const t = useTranslations("Navigation"); return <Link href="/about">{t("links.about")}</Link>; }
Server components: use getTranslations from next-intl/server (await with locale/namespace as needed).
- Messages format
One JSON file per locale under messages/ . Nested keys map to namespaces and keys:
{ "HomePage": { "title": "Hello world!" }, "LandingPage": { "title": "Tokyo Sounds", "navbar": { "home": "Home", "about": "About" } }, "BackToHomeButton": { "buttonText": "Back to Home", "tooltip": "Return to the main page" } }
-
useTranslations("LandingPage") → t("title") , t("navbar.about") .
-
Interpolation: "selectColor": "Select {color} color" → t("selectColor", { color: "Blue" }) .
Checklist
-
next.config.ts : createNextIntlPlugin() wraps config.
-
src/i18n/routing.ts : defineRouting with locales and defaultLocale .
-
src/i18n/request.ts : getRequestConfig
- hasLocale
- dynamic messages/${locale}.json .
-
src/proxy.ts (or middleware.ts ): createMiddleware(routing) and matcher.
-
src/i18n/navigation.ts : createNavigation(routing) and re-export Link , etc.
-
app/[locale]/layout.tsx : hasLocale → notFound , setRequestLocale , generateStaticParams , NextIntlClientProvider
- getMessages() .
-
Each app/[locale]/**/page.tsx : setRequestLocale(locale) when using static rendering.
-
Client components: useTranslations("Namespace") ; links use Link from @/i18n/navigation .
Reference
-
Copy-paste examples: examples/ — standalone files for use in any project.
-
Extended config (localePrefix, pathnames, etc.): reference.md
-
Official: next-intl App Router, Routing setup