Next.js i18n: next-intl vs react-i18next vs i18next
Three libraries dominate Next.js localization: next-intl, react-i18next, and i18next. Each uses JSON locale files — the only difference is how that JSON is loaded, typed, and delivered to components. This guide covers the practical differences so you can pick one and start shipping.
At a glance
All three libraries read locale JSON files. The differences are in App Router compatibility, TypeScript ergonomics, and how much configuration they need out of the box.
| Feature | next-intl | react-i18next | i18next |
|---|---|---|---|
| App Router (RSC) support | Native — works in Server Components without a provider | Yes, with extra setup — hooks only work in Client Components | Yes — use the core API directly in server code |
| Pages Router support | Yes (limited — App Router is the focus) | First-class | First-class |
| TypeScript safety | Type-safe keys from your locale file with one type declaration | Available via i18next-typescript plugin; more setup required | Same as react-i18next |
| Plural format | ICU message syntax | Key suffixes (_one, _other, _many…) | Key suffixes (_one, _other, _many…) |
| Variable interpolation | ICU — {name} | Mustache — {{name}} | Mustache — {{name}} |
| Locale routing middleware | Built in — one function call | Write your own or use next-i18n-router | Write your own |
| Namespace / code splitting | Single file per locale (all keys loaded together) | Multiple namespaces — load only what a page needs | Multiple namespaces — load only what a page needs |
| Ecosystem / plugins | Next.js-focused | Large — HTTP backend, caching, language detection, formatters | Same ecosystem as react-i18next |
| Bundle size (approx.) | ~14 kB gzipped | ~8 kB + i18next ~15 kB = ~23 kB gzipped | ~15 kB gzipped |
| Non-React usage (API routes, email) | Limited — designed for React rendering | Use the i18next core directly | Yes — primary use case |
next-intl
npm install next-intl
next-intl is built specifically for Next.js. It is the only library in this comparison that works natively in React Server Components without a provider wrapper — messages are loaded on the server and passed to client components only when needed.
File format
Messages live in one JSON file per locale, typically at messages/en.json. Keys can be nested to any depth:
// messages/en.json
{
"nav": {
"home": "Home",
"about": "About"
},
"home": {
"title": "Welcome, {name}",
"items": "{count, plural, one {# item} other {# items}}"
}
}Usage in components
In a Server Component, call getTranslations(). In a Client Component, use the useTranslations() hook:
// app/[locale]/page.tsx — Server Component
import { getTranslations } from "next-intl/server";
export default async function HomePage() {
const t = await getTranslations("home");
return <h1>{t("title", { name: "Alice" })}</h1>;
}
// components/Nav.tsx — Client Component
"use client";
import { useTranslations } from "next-intl";
export function Nav() {
const t = useTranslations("nav");
return <nav>{t("home")}</nav>;
}Middleware for locale routing
next-intl handles locale routing through Next.js middleware. A minimal setup redirects /about to /en/about automatically:
// middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: ["/((?!api|_next|.*\..*).*)"],
};
// i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["en", "de", "fr", "ja"],
defaultLocale: "en",
});Intl browser API. This means plural rules are correct for all languages without extra configuration.TypeScript safety
next-intl can generate types from your default locale file so that t("typo") is a TypeScript error at build time. Add one type declaration:
// global.d.ts — wire up type-safe messages
import en from "@/messages/en.json";
declare global {
type IntlMessages = typeof en;
}react-i18next
npm install react-i18next i18next
react-i18next is the React binding layer for i18next — the most widely used i18n library in the JavaScript ecosystem. It has the largest community, the most plugins, and the longest track record. If your team already uses it in a non-Next.js project, carrying it into a Next.js app is straightforward.
File format
Locale files are organized by namespace — one JSON file per feature area or page. The default namespace is translation:
// public/locales/en/translation.json
{
"nav_home": "Home",
"nav_about": "About"
}
// public/locales/en/checkout.json
{
"title": "Checkout",
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}Keys are flat by convention, though nesting is supported. Plurals use a key-suffix pattern (_one, _other) rather than ICU syntax.
Usage in components
"use client";
import { useTranslation } from "react-i18next";
export function Nav() {
const { t } = useTranslation(); // default namespace
return <nav>{t("nav_home")}</nav>;
}
// Loading a specific namespace
export function CheckoutSummary({ count }: { count: number }) {
const { t } = useTranslation("checkout");
return <p>{t("items", { count })}</p>;
}App Router setup
react-i18next works with the App Router, but Server Components cannot use hooks. The recommended approach is a server-side helper that calls the i18next core API directly:
// i18n.ts — shared config
import i18next from "i18next";
import { initReactI18next } from "react-i18next/initReactI18next";
i18next.use(initReactI18next).init({
lng: "en",
fallbackLng: "en",
resources: {
en: { translation: require("./public/locales/en/translation.json") },
de: { translation: require("./public/locales/de/translation.json") },
},
});
export default i18next;
// In a Server Component — use the core API directly
import i18n from "@/i18n";
const t = i18n.getFixedT("de");
return <h1>{t("nav_home")}</h1>;The Trans component for rich content
react-i18next’s Trans component handles translations that embed React elements — links, bold text, or icons inside a sentence:
// en/translation.json
{
"tos_prompt": "By signing up you agree to our <1>Terms of Service</1>."
}
// Component
import { Trans } from "react-i18next";
<Trans
i18nKey="tos_prompt"
components={[<span />, <Link href="/terms" />]}
/>next-intl offers a similar rich() API, but the Trans component is more expressive for complex markup.
i18next (without react-i18next)
npm install i18next i18next-resources-to-backend
i18next is the core library that react-i18next wraps. You can use it directly in Next.js Server Components and API routes without any React binding layer. This is useful when you need i18n in server-only code — middleware, API handlers, email templates, or PDF generation — without bringing in React hooks.
// lib/i18n-server.ts
import i18next from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
const i18n = i18next.createInstance();
await i18n
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`../public/locales/${language}/${namespace}.json`),
),
)
.init({
lng: "en",
fallbackLng: "en",
defaultNS: "translation",
});
export default i18n;The file format is identical to react-i18next. The same JSON locale files work with both. Switching from standalone i18next to react-i18next later requires no locale file changes.
Pluralization: ICU vs key suffixes
The biggest difference in how you write locale files is pluralization syntax.
next-intl: ICU message format
// messages/en.json
{
"items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
// messages/pl.json — Polish has four plural forms
{
"items": "{count, plural, one {# element} few {# elementy} many {# elementów} other {# elementu}}"
}All plural forms live in one key. ICU rules handle every language automatically — no special configuration for Polish, Arabic, or Russian plural forms.
react-i18next / i18next: key suffix pattern
// public/locales/en/translation.json
{
"items_zero": "No items",
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}
// public/locales/pl/translation.json — Polish
{
"items_one": "{{count}} element",
"items_few": "{{count}} elementy",
"items_many": "{{count}} elementów",
"items_other": "{{count}} elementu"
}Each plural form is a separate key. More verbose, but easier to hand to translators who are unfamiliar with ICU syntax.
Locale routing in the App Router
Both next-intl and react-i18next work with the app/[locale]/ dynamic segment. The locale is a route parameter; middleware detects the preferred locale and redirects accordingly.
// Recommended folder structure for App Router
app/
[locale]/
layout.tsx ← wraps pages with locale context
page.tsx
about/
page.tsx
middleware.ts ← detects and redirects to locale
messages/ ← locale files (next-intl convention)
en.json
de.json
fr.json
// — OR —
public/
locales/ ← locale files (react-i18next convention)
en/
translation.json
de/
translation.jsonnext-intl provides a middleware factory out of the box. With react-i18next you write the middleware yourself — typically a small function that reads Accept-Language, matches a supported locale, and rewrites the URL.
Which one should you pick?
next-intl
Best for: New App Router projects
- –Native React Server Component support — no provider needed in server layouts
- –Built-in middleware for locale routing with one function call
- –Type-safe message keys with zero extra tooling
- –ICU pluralization handles all language edge cases correctly out of the box
- –Smallest configuration surface for a greenfield Next.js app
Not ideal for: Projects already running react-i18next, or codebases that share i18n config across React Native or non-Next.js apps
react-i18next
Best for: Migrations from Pages Router, or multi-platform codebases
- –Largest ecosystem — plugins for HTTP backends, caching, language detection
- –Trans component for complex markup embedded inside translations
- –Same config works across React, React Native, and non-Next.js React apps
- –Namespace-based code splitting keeps bundle size down for large apps
- –Familiar API for teams already experienced with i18next
Not ideal for: App Router projects starting fresh — RSC integration requires more setup than next-intl
i18next (standalone)
Best for: Server-only code that needs i18n
- –No React dependency — works in middleware, API routes, email templates
- –Same locale file format as react-i18next — easy to unify across your codebase
- –Initialize once, import and call anywhere on the server
Not ideal for: Component trees — use react-i18next or next-intl for anything rendered in React
Locale file format summary
All three libraries read plain JSON. The structural conventions differ:
| Library | Default path | Key style | Plural format |
|---|---|---|---|
| next-intl | messages/{locale}.json | Nested objects | ICU syntax |
| react-i18next | public/locales/{locale}/{ns}.json | Flat or nested | Key suffixes (_one, _other) |
| i18next | Configurable | Flat or nested | Key suffixes (_one, _other) |
Migrating between libraries
The most common migration path is from next-i18next (Pages Router) to next-intl (App Router). Locale JSON files are reusable — next-intl reads nested JSON natively, so flat key structures from next-i18next can be imported directly. The main changes are in the calling code, not the locale files:
next-i18next useTranslation()next-intl useTranslations() — same call signature, different importi18next {{count}} interpolation in locale filesnext-intl {count} ICU interpolation — update your locale stringsnext-i18next serverSideTranslations() in getStaticPropsnext-intl getTranslations() — called directly inside the Server Componentnext.config.js i18n block (Pages Router only)next-intl middleware + routing config — the next.config.js i18n key is unused in App RouterTranslate your Next.js locale files with LocalePack
LocalePack translates the JSON locale files used by next-intl, react-i18next, and i18next. Upload your source locale file — flat or nested, ICU or key-suffix plural format — and download a ready-to-use ZIP with one file per target language. No platform setup, no subscription. Upload once, pay once, download.
Translate Next.js locale files →Related guides
messages.json format explained (with placeholders)
The WebExtension messages.json format reference — different from Next.js locale JSON but useful context if you build both.
Validate messages.json before shipping
Validation scripts for required fields, missing keys across locales, and malformed placeholder syntax.