next-intl
next-intl is an internationalization toolkit built specifically for Next.js. It renders ICU messages with interpolation, plurals, selects, and rich React content; formats dates, numbers, lists, and relative times; loads request-scoped messages in Server Components; and provides locale-aware routing, middleware, and navigation wrappers. Version 4.13.5 supports the App Router and Pages Router across Next.js 12 through 16, with current setup guidance centered on Next.js 16 proxy files and next/root-params.
The strongest integrated choice for serious i18n in a Next.js App Router project, especially when routing and Server Components matter. It is excessive for simple formatting, and its Next-version-sensitive setup plus experimental extraction deserve deliberate adoption.
Use it if
- You are building a Next.js application and want translations, formatting, Server Components, and locale routing designed as one system
- You need ICU plural and select rules instead of hand-written singular versus plural conditionals
- You want localized pathnames, locale negotiation, alternate-language links, and type-aware navigation wrappers
- You keep a canonical locale catalog and want TypeScript autocomplete for locale names, message keys, and format names
- Your app is not on Next.js: this package declares Next as a peer and much of its value comes from Next plugins, request configuration, routing, and Server Components
- You only need browser locale formatting or a handful of static strings; the platform Intl APIs avoid message catalogs, provider setup, a Next config plugin, and request wiring
- You want locale routing without middleware or a proxy while keeping unprefixed routes: the static-export setup requires locale prefixes and additional route constraints
- You need automatic message extraction to be a settled production contract: useExtracted and the extractor are explicitly experimental, and the manual API is named unstable_extractMessages
- You cannot accept build-time native tooling in the dependency tree: 4.13.5 directly depends on @swc/core and @parcel/watcher in addition to its runtime helpers
Setup reality
The first translated string requires more than npm install. For the App Router you normally create messages for each locale, an i18n/request.ts file using getRequestConfig, add createNextIntlPlugin to next.config, and wrap client consumers in NextIntlClientProvider. Locale-prefixed routing adds i18n/routing.ts, a top-level app/[locale] segment, a proxy.ts file using createMiddleware, and navigation wrappers created from the same routing object. Next.js called proxy.ts middleware.ts before version 16, so copy the example for your installed Next major. The current recommended routing setup reads next/root-params, which is available by default in Next.js 16.3 and later; earlier compatible versions need experimental.rootParams or the legacy setRequestLocale path. Static generation still needs generateStaticParams for the locale segment. The proxy matcher must exclude API routes, Next internals, monitoring endpoints, and static files, while explicitly including valid dotted paths such as user names with periods. Client Components only see messages passed through NextIntlClientProvider, so sending the entire catalog can inflate the response; pass a selected subtree when practical. ICU apostrophe escaping surprises newcomers, and translation keys are type-safe only after optional module augmentation. Message catalogs, translator workflow, fallback policy, and CMS synchronization remain your responsibility. The package is ESM-first but exposes a CommonJS plugin entry, and its install includes @swc/core and @parcel/watcher platform packages because extraction and build integration live in the main distribution.
Patterns
Load messages for each requestconfigure-request
// src/i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async () => {
const locale = 'en';
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});The conventional path is src/i18n/request.ts or i18n/request.ts; pass a custom path to the plugin if you move it.
Connect request configuration to Next.jsenable-next-plugin
// next.config.ts
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {};
export default withNextIntl(nextConfig);The plugin locates the conventional request file; supply its relative path to createNextIntlPlugin when using another location.
Expose request configuration to Client Componentsprovide-client-messages
import { NextIntlClientProvider } from 'next-intl';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<body>
<NextIntlClientProvider>{children}</NextIntlClientProvider>
</body>
</html>
);
}Client Components require the provider; consider passing a selected messages object instead of shipping every namespace.
Interpolate and pluralize a translationrender-icu-message
// messages/en.json
// { "Profile": { "followers": "{count, plural, =0 {No followers} =1 {One follower} other {# followers}}" } }
import { useTranslations } from 'next-intl';
function Followers({ count }: { count: number }) {
const t = useTranslations('Profile');
return <p>{t('followers', { count })}</p>;
}ICU selects the plural branch using locale rules; # expands to the formatted count inside a plural clause.
Map trusted ICU tags to React elementsrender-rich-text
// messages/en.json: { "About": { "cta": "Read the <link>documentation</link>." } }
const t = useTranslations('About');
return t.rich('cta', {
link: (chunks) => <a href='/docs'>{chunks}</a>,
});Use t.rich for React elements; plain t returns a string and t.raw bypasses message processing.
Translate inside an async Server Componenttranslate-server-component
import { getTranslations } from 'next-intl/server';
export default async function Page() {
const t = await getTranslations('HomePage');
return <h1>{t('title')}</h1>;
}Async components use the awaitable server API; hooks cannot be called after an await in an async component.
Declare supported and default localesdefine-locale-routing
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'de'] as const,
defaultLocale: 'en',
localePrefix: 'as-needed',
});Keep this object shared by the proxy and navigation wrappers so redirects and generated links follow one policy.
Negotiate and rewrite locale routesconfigure-locale-proxy
// src/proxy.ts for Next.js 16
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\..*).*)',
};Next.js used middleware.ts before version 16; the matcher excludes dotted paths, so add explicit entries when dots are valid route data.
Create locale-aware navigation wrapperscreate-localized-navigation
// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);
// In a component:
// <Link href='/about' locale='de'>Deutsch</Link>Import these generated wrappers instead of next/link and next/navigation when the destination should follow locale routing.
Reject unsupported locale segmentsvalidate-route-locale
import { hasLocale, NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
export default async function LocaleLayout({ children, params }) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) notFound();
return <NextIntlClientProvider>{children}</NextIntlClientProvider>;
}Validate a dynamic locale before using it for message imports or request configuration.
Pre-render each configured localegenerate-locale-pages
import { routing } from '@/i18n/routing';
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}A top-level [locale] dynamic segment needs generateStaticParams for the locales you want rendered at build time.
Type locale names and message keysaugment-message-types
// global.ts
import { routing } from '@/i18n/routing';
import messages from './messages/en.json';
declare module 'next-intl' {
interface AppConfig {
Locale: (typeof routing.locales)[number];
Messages: typeof messages;
}
}Use a canonical locale catalog for the Messages type; very large catalogs can add TypeScript and editor work.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| next-i18next | npm | Choose it when an existing i18next catalog, plugin, or translation-service workflow must remain the center of the app |
| react-intl | npm | Choose it for framework-neutral React ICU messages and formatting without next-intl's routing layer |
| @lingui/react | npm | Choose it when compile-time message extraction and a broader React ecosystem matter more than Next-specific routing |