# next-intl vs react-i18next: Which React i18n Library Should You Use?

> Confused about next-intl vs react-i18next? This React i18n comparison covers setup, performance, App Router support, and which library fits your stack.
- **Author**: Mohit Sapat
- **Published**: 2026-07-28
- **URL**: https://www.babelize.co/blogs/next-intl-vs-react-i18next

---

<p class="lead">
    If you're adding internationalization to a React or Next.js app, you will hit the same fork in the road almost immediately: next-intl or react-i18next? Both are mature, actively maintained, and used in production by thousands of teams. They just make very different assumptions about where your app runs and who renders your text.
</p>

<h2>The Short Answer</h2>

<p>
    <strong>Use next-intl if your app is a Next.js App Router project.</strong> It was designed around Server Components, locale-aware routing, and ICU MessageFormat, so the common path needs very little glue code.
</p>

<p>
    <strong>Use react-i18next if your app is not Next.js-only</strong> -- a Vite SPA, React Native, Remix, an Electron app, or a monorepo that shares translations across web and mobile -- or if your team already runs on i18next and its plugin ecosystem.
</p>

<p>
    The rest of this guide explains why, with real setup code for both, a feature-by-feature comparison, the pitfalls that catch teams in production, and what a migration looks like.
</p>

<h2>At a Glance (October 2026)</h2>

<p>
    Current releases and npm downloads, pulled from the npm registry on October 13, 2026. Download counts include installs pulled in by other packages, so read them as a measure of reach, not of how many apps use each library directly.
</p>

<table>
    <thead>
        <tr>
            <th></th>
            <th>next-intl</th>
            <th>react-i18next + i18next</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Latest version</td>
            <td>4.14.9</td>
            <td>react-i18next 17.0.16 - i18next 26.4.4</td>
        </tr>
        <tr>
            <td>Weekly npm downloads</td>
            <td>~7.1M</td>
            <td>~17.9M (react-i18next) - ~24.1M (i18next)</td>
        </tr>
        <tr>
            <td>Runs on</td>
            <td>Next.js 12-16 only</td>
            <td>Any React 16.8+ app, React Native, Node.js</td>
        </tr>
        <tr>
            <td>Message format</td>
            <td>ICU MessageFormat</td>
            <td>i18next JSON (ICU via <code>i18next-icu</code>)</td>
        </tr>
        <tr>
            <td>Server Components</td>
            <td>Built in (<code>getTranslations</code>)</td>
            <td>Per-request i18next instance; hooks are client-only</td>
        </tr>
        <tr>
            <td>Locale routing</td>
            <td>Built in (<code>defineRouting</code>, <code>proxy.ts</code>)</td>
            <td>Bring your own</td>
        </tr>
    </tbody>
</table>

<h2>What Are next-intl and react-i18next?</h2>

<h3>react-i18next</h3>

<p>
    <strong>react-i18next</strong> is the official React binding for <strong>i18next</strong>, a framework-agnostic internationalization core that has been around for over a decade. i18next itself knows nothing about React; react-i18next adds hooks (<code>useTranslation</code>), a <code>Trans</code> component for rich text, and a provider. Around that core sits a large ecosystem: language detectors, HTTP and filesystem backends, caching layers, an ICU plugin, and integrations with most translation management systems.
</p>

<p>
    Because the core is framework-agnostic, the same i18next configuration and the same translation files can power a React web app, a React Native app, and a Node.js backend that sends localized emails.
</p>

<h3>next-intl</h3>

<p>
    <strong>next-intl</strong> is built specifically for <strong>Next.js</strong>. It supports both the Pages Router and the App Router, but its design centers on the App Router: translations work in async Server Components, Client Components, Server Actions, metadata, and route handlers, and it ships its own locale-aware routing, middleware, and navigation helpers. Messages use <strong>ICU MessageFormat</strong> out of the box, so plurals, selects, and number and date formatting live inside the translation string.
</p>

<h2>Feature Comparison</h2>

<table>
    <thead>
        <tr>
            <th>Feature</th>
            <th>next-intl</th>
            <th>react-i18next</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Framework scope</td>
            <td>Next.js only</td>
            <td>Any React renderer (web, React Native, Electron)</td>
        </tr>
        <tr>
            <td>App Router &amp; Server Components</td>
            <td>First-class: <code>getTranslations</code> and <code>useTranslations</code> in server code</td>
            <td>Works, but hooks are client-only; server code needs a per-request i18next instance</td>
        </tr>
        <tr>
            <td>Pages Router</td>
            <td>Supported</td>
            <td>Supported (commonly via next-i18next)</td>
        </tr>
        <tr>
            <td>Message syntax</td>
            <td>ICU MessageFormat, built in</td>
            <td>i18next JSON format; ICU via the i18next-icu plugin</td>
        </tr>
        <tr>
            <td>Plurals</td>
            <td><code>{count, plural, one {...} other {...}}</code></td>
            <td>Suffixed keys: <code>key_one</code>, <code>key_other</code></td>
        </tr>
        <tr>
            <td>Routing &amp; middleware</td>
            <td>Built in: <code>defineRouting</code>, middleware, <code>createNavigation</code></td>
            <td>Not included; you write your own</td>
        </tr>
        <tr>
            <td>Type safety</td>
            <td>Module augmentation of <code>AppConfig</code></td>
            <td>Module augmentation of <code>CustomTypeOptions</code></td>
        </tr>
        <tr>
            <td>Namespaces &amp; lazy loading</td>
            <td>Nested message objects; load what you need in <code>request.ts</code></td>
            <td>Namespaces are a core concept; lazy loading via backends</td>
        </tr>
        <tr>
            <td>Language detection</td>
            <td>Middleware: URL prefix, cookie, <code>Accept-Language</code></td>
            <td>i18next-browser-languagedetector or custom detector</td>
        </tr>
        <tr>
            <td>Plugin ecosystem</td>
            <td>Small and focused</td>
            <td>Extensive</td>
        </tr>
        <tr>
            <td>Learning curve</td>
            <td>Gentle for Next.js developers</td>
            <td>Moderate: more configuration options and concepts</td>
        </tr>
    </tbody>
</table>

<h2>Setting Up next-intl in the App Router</h2>

<p>
    A current next-intl (v4) App Router setup is split into a few small files. First, a single routing definition that the middleware and navigation helpers share:
</p>

<pre><code>// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
    locales: ["en", "fr", "de"],
    defaultLocale: "en",
});</code></pre>

<p>
    The middleware negotiates the locale and redirects or rewrites requests to the right prefix. (In Next.js 16 the <code>middleware.ts</code> convention was renamed to <code>proxy.ts</code>; the content is the same.)
</p>

<pre><code>// src/proxy.ts  (src/middleware.ts on 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|.*\\..*).*)",
};</code></pre>

<p>
    Request configuration tells next-intl which messages to load for the current request:
</p>

<pre><code>// src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";

export default getRequestConfig(async ({ requestLocale }) =&gt; {
    const requested = await requestLocale;
    const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;

    return {
        locale,
        messages: (await import(`../../messages/${locale}.json`)).default,
    };
});</code></pre>

<p>
    Then the <code>[locale]</code> layout wraps the tree in <code>NextIntlClientProvider</code> so Client Components can read messages, and calls <code>setRequestLocale</code> so pages can be statically rendered:
</p>

<pre><code>// src/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";

export function generateStaticParams() {
    return routing.locales.map((locale) =&gt; ({ locale }));
}

export default async function LocaleLayout({ children, params }) {
    const { locale } = await params;
    if (!hasLocale(routing.locales, locale)) notFound();
    setRequestLocale(locale);

    return (
        &lt;html lang={locale}&gt;
            &lt;body&gt;
                &lt;NextIntlClientProvider&gt;{children}&lt;/NextIntlClientProvider&gt;
            &lt;/body&gt;
        &lt;/html&gt;
    );
}</code></pre>

<p>
    Using translations looks the same everywhere. In an async Server Component you <code>await getTranslations()</code>; in a regular component -- server or client -- you call <code>useTranslations()</code>:
</p>

<pre><code>// Server Component
import { getTranslations } from "next-intl/server";

export default async function Pricing() {
    const t = await getTranslations("Pricing");
    return &lt;h1&gt;{t("title")}&lt;/h1&gt;;
}

// Client Component
"use client";
import { useTranslations } from "next-intl";

export function SeatCounter({ count }: { count: number }) {
    const t = useTranslations("Pricing");
    return &lt;p&gt;{t("seats", { count })}&lt;/p&gt;;
}</code></pre>

<p>
    Finally, <code>createNavigation(routing)</code> gives you locale-aware <code>Link</code>, <code>redirect</code>, <code>usePathname</code>, and <code>useRouter</code> wrappers, so you never hand-build <code>/fr/pricing</code> URLs. For the full walkthrough from an empty project, see our <a href="/blogs/implement-i18n-nextjs-next-intl/">step-by-step guide to i18n in Next.js with next-intl</a>.
</p>

<h2>Setting Up react-i18next in the App Router</h2>

<p>
    react-i18next was designed before Server Components existed, and its hooks rely on React context, which only exists in Client Components. In the App Router that leaves two layers to configure.
</p>

<p>
    <strong>Server side:</strong> the common pattern is to create a fresh i18next instance per request, so concurrent requests in different languages never share state. <code>i18next-resources-to-backend</code> lets you lazy-load JSON files per language and namespace with dynamic imports:
</p>

<pre><code>// src/i18n/server.ts
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";

export async function getT(locale: string, ns = "common") {
    const i18n = createInstance();
    await i18n
        .use(initReactI18next)
        .use(resourcesToBackend((lng: string, ns: string) =&gt;
            import(`../locales/${lng}/${ns}.json`)))
        .init({ lng: locale, ns: [ns], defaultNS: ns, fallbackLng: "en" });

    return i18n.getFixedT(locale, ns);
}

// In a Server Component
const t = await getT(locale, "pricing");
return &lt;h1&gt;{t("title")}&lt;/h1&gt;;</code></pre>

<p>
    <strong>Client side:</strong> a separate, long-lived i18next instance initialized in a client module, with <code>useTranslation()</code> in Client Components. You keep the client instance's language in sync with the locale segment of the URL, often by passing it down from the layout.
</p>

<p>
    You also write locale routing yourself: a middleware that reads the cookie or <code>Accept-Language</code> header, redirects to a locale prefix, and a <code>[lng]</code> segment in your app directory. None of this is hard, but it is code you own and maintain. (If you are on the <strong>Pages Router</strong>, <code>next-i18next</code> wraps most of this for you; it does not target the App Router.)
</p>

<h2>Message Syntax: ICU vs i18next JSON</h2>

<p>
    This is the difference that matters most to translators, and it is easy to overlook. next-intl uses ICU MessageFormat:
</p>

<pre><code>{
    "Pricing": {
        "seats": "{count, plural, =0 {No seats} one {# seat} other {# seats}}"
    }
}</code></pre>

<p>
    i18next uses its own JSON format, where interpolation uses double braces and plurals are separate keys with CLDR category suffixes:
</p>

<pre><code>{
    "seats_zero": "No seats",
    "seats_one": "{{count}} seat",
    "seats_other": "{{count}} seats"
}</code></pre>

<p>
    Both handle languages with complex plural rules correctly, because both resolve the category with <code>Intl.PluralRules</code>. The i18next format is easier to read for simple strings; ICU keeps the whole sentence in one key, which translators and translation platforms generally handle better for gender and nested selects. If you want ICU with i18next, add the <code>i18next-icu</code> plugin -- but then you are using the ICU format, not the i18next one, so pick one per project. Our <a href="/blogs/icu-messageformat-plurals-guide/">ICU MessageFormat guide</a> covers plural and select syntax in depth.
</p>

<h2>Type Safety</h2>

<p>
    Both libraries can type-check your translation keys, and both do it the same way: TypeScript module augmentation pointing at your source-language JSON file. Neither generates code.
</p>

<pre><code>// next-intl: global.d.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;
    }
}

// react-i18next: i18next.d.ts
import common from "./locales/en/common.json";

declare module "i18next" {
    interface CustomTypeOptions {
        defaultNS: "common";
        resources: { common: typeof common };
    }
}</code></pre>

<p>
    With either setup, a typo like <code>t("pricng.title")</code> becomes a compile error. next-intl can additionally type ICU arguments in recent versions, so a missing <code>count</code> is caught as well.
</p>

<h2>Bundle Size and Performance</h2>

<p>
    Avoid picking either library based on a headline bundle number; what dominates in practice is <em>how many messages you ship to the browser</em>, not the size of the runtime.
</p>

<p>
    With next-intl, text rendered in Server Components is resolved on the server and arrives as HTML. The message catalog itself does not need to be sent at all for those components. For Client Components, <code>NextIntlClientProvider</code> passes messages down; on large apps you can pass only the namespaces a subtree needs (for example with <code>pick(messages, ["Pricing"])</code>) instead of the entire file.
</p>

<p>
    With react-i18next, server-rendered text works the same way when you use a per-request instance. Client Components need the resources loaded into the client instance, and the ecosystem's answer is namespaces plus a backend (HTTP or dynamic import) that loads <code>pricing.json</code> only when a component asks for the <code>pricing</code> namespace.
</p>

<p>
    Both approaches can produce lean pages. next-intl makes the lean path the default in the App Router; react-i18next makes it a configuration choice.
</p>

<figure class="blog-figure">
    <img src="/blog/inline/next-intl-vs-react-i18next-1.webp" alt="Diagram comparing where translation runs in the Next.js App Router. next-intl: createMiddleware handles locale detection, i18n/request.ts loads messages/${locale}.json per request, Server Components call getTranslations and render text to HTML, and NextIntlClientProvider passes picked messages to Client Components using useTranslations. react-i18next: your own middleware and [lng] segment, createInstance per request with resourcesToBackend loading locales/${lng}/${ns}.json and getT on the server, and a separate long-lived client instance using useTranslation with a backend that loads namespaces on demand." width="1500" height="990" loading="lazy" decoding="async" />
    <figcaption>Where translation runs: next-intl resolves text per request on the server; react-i18next uses a per-request server instance plus a client instance.</figcaption>
</figure>

<h2>Which One Should You Choose?</h2>

<p><strong>Choose next-intl if:</strong></p>

<ul>
    <li>The app is Next.js, and you have no plans to share translations with a non-Next.js client</li>
    <li>You use the App Router and want translations in Server Components without extra glue</li>
    <li>You want locale routing, middleware, and localized navigation handled for you</li>
    <li>You prefer ICU MessageFormat, or your translation vendor expects it</li>
</ul>

<p><strong>Choose react-i18next if:</strong></p>

<ul>
    <li>You ship React Native, Electron, Vite, or Remix apps alongside (or instead of) Next.js</li>
    <li>You want one translation setup shared by the frontend and a Node.js backend</li>
    <li>Your team already uses i18next and its file format, detectors, or backends</li>
    <li>You need a specific plugin, such as a CMS or translation-platform backend that loads strings at runtime</li>
</ul>

<p>
    A useful tiebreaker: <strong>whoever owns the most code that renders text wins.</strong> If 90% of your UI lives in a Next.js app, use next-intl there even if a small React Native app uses i18next -- sharing raw JSON between two formats is easier than fighting the wrong library in your main app.
</p>

<figure class="blog-figure">
    <img src="/blog/inline/next-intl-vs-react-i18next-2.webp" alt="Decision flowchart. Q1: Is the app a Next.js App Router project? No leads to react-i18next. Yes leads to Q2: do the same translations also need to run in React Native, Electron, Vite, Remix or Node.js? Yes leads to react-i18next. No leads to Q3: does your team already rely on i18next, its file format, detectors, backends or a plugin? Yes leads to react-i18next, No leads to next-intl. Tiebreaker: whoever owns the most code that renders text wins." width="1500" height="1000" loading="lazy" decoding="async" />
    <figcaption>Decision guide: next-intl for Next.js App Router apps, react-i18next for cross-platform setups or existing i18next teams.</figcaption>
</figure>

<h2>Migrating from react-i18next to next-intl</h2>

<p>
    Teams moving an existing Next.js app to the App Router often make this switch. The work splits into three parts:
</p>

<ul>
    <li><strong>Convert the message files.</strong> Merge namespaces into nested objects (or keep one file per namespace and merge them in <code>request.ts</code>), turn <code>{{name}}</code> into <code>{name}</code>, and collapse <code>key_one</code>/<code>key_other</code> pairs into a single ICU plural. Script this; do not do it by hand.</li>
    <li><strong>Replace the calls.</strong> <code>useTranslation("pricing")</code> becomes <code>useTranslations("Pricing")</code>, and <code>&lt;Trans&gt;</code> becomes <code>t.rich()</code> with tag callbacks.</li>
    <li><strong>Delete your routing glue.</strong> Custom locale middleware and link helpers get replaced by <code>defineRouting</code>, <code>createMiddleware</code>, and <code>createNavigation</code>. Keep your existing URL structure so you do not lose rankings; see our <a href="/blogs/hreflang-multilingual-seo-guide/">hreflang and multilingual SEO guide</a> before changing locale prefixes.</li>
</ul>

<p>
    Migrate route by route behind a feature branch, and run a pseudo-locale through the converted pages to catch any string that slipped through untranslated.
</p>

<h2>Common Pitfalls</h2>

<ul>
    <li><strong>Calling <code>useTranslation()</code> in a Server Component.</strong> With react-i18next, hooks need a Client Component. Use a per-request instance on the server instead of adding <code>"use client"</code> everywhere.</li>
    <li><strong>Sharing one global i18next instance on the server.</strong> Changing its language for one request can leak into another concurrent request. Create an instance per request.</li>
    <li><strong>Forgetting <code>setRequestLocale</code> with next-intl.</strong> Without it, pages that use translations opt into dynamic rendering, and you lose static generation.</li>
    <li><strong>Passing the whole catalog to the client.</strong> Wrapping the root in a provider with every message for every feature is convenient, but it ships text the page never shows.</li>
    <li><strong>Mixing message formats.</strong> ICU strings in an i18next project without the ICU plugin render literally, braces and all.</li>
    <li><strong>Inconsistent locale codes.</strong> <code>en_US</code> in file names, <code>en-US</code> in the URL, <code>en</code> in the HTML lang attribute. Pick BCP 47 tags once; our <a href="/blogs/locale-codes-bcp-47-explained/">locale codes guide</a> explains the rules.</li>
    <li><strong>Hand-formatting numbers and dates.</strong> Both libraries expose locale-aware formatting; pass raw values, never <code>toFixed(2)</code> strings.</li>
</ul>

<h2>Frequently Asked Questions</h2>

<h3>What is the difference between next-intl and react-i18next?</h3>
<p>
    next-intl is built specifically for Next.js, with first-class App Router and Server Component support, built-in locale routing, and ICU MessageFormat. react-i18next is the React binding for i18next, a framework-agnostic library that also runs in React Native, Vite, Remix, and Node.js, with a large plugin ecosystem.
</p>

<h3>Which library works best with the Next.js App Router?</h3>
<p>
    next-intl. Its getTranslations and useTranslations APIs work in Server Components, and it provides routing, middleware, and navigation helpers. react-i18next works in the App Router too, but its hooks are client-only, so server rendering requires creating an i18next instance per request.
</p>

<h3>Can I use react-i18next with React Native?</h3>
<p>
    Yes. react-i18next supports React Native, which makes it a strong choice for projects that share translations between web and mobile. next-intl only targets Next.js, so a React Native app would need a different library.
</p>

<h3>Does next-intl support ICU MessageFormat?</h3>
<p>
    Yes. next-intl uses ICU MessageFormat natively for plurals, selects, and number and date formatting. i18next uses its own JSON format with suffixed plural keys such as key_one and key_other, and supports ICU only through the i18next-icu plugin.
</p>

<h3>Is next-intl or react-i18next faster?</h3>
<p>
    Neither runtime is a meaningful bottleneck. What matters is how many messages reach the browser. next-intl renders Server Component text on the server by default, while react-i18next achieves similar results with per-request server instances and lazily loaded namespaces on the client.
</p>

<h2>The Short Version</h2>

<p>
    next-intl is the specialist: if your product is a Next.js App Router app, it gives you Server Component translations, routing, and ICU with the least code. react-i18next is the generalist: if your translations need to work across React Native, other frameworks, or a backend, its framework-agnostic core and ecosystem are hard to beat. Pick based on where most of your text renders, keep your message files in one format, and type your keys either way. If you are still deciding how to structure those files, start with our explanation of <a href="/blogs/json-translation-files-explained/">JSON translation files</a>.
</p>

<p>
    Whichever library you choose, the translation files still have to stay in sync as your source strings change. <a href="https://www.babelize.co/" target="_blank" rel="noopener noreferrer">Babelize</a> detects new and changed keys and generates translations for every locale in your repository, with guides for both the <a href="https://docs.babelize.co/sdk/nextjs" target="_blank" rel="noopener noreferrer">Next.js</a> and <a href="https://docs.babelize.co/sdk/react" target="_blank" rel="noopener noreferrer">React</a> SDKs.
</p>
---
- [All articles](https://www.babelize.co/blogs)