EmpireUI
Get Pro
← Blog9 min read#i18n#react#internationalisation

React Internationalisation (i18n): next-intl, react-i18next in 2026

A practical comparison of next-intl and react-i18next for React apps in 2026 — routing, pluralisation, ICU, and when to pick each one.

Keyboard with globe key representing world language internationalisation

Why i18n Breaks Most React Apps (Eventually)

Internationalisation isn't glamorous. You ship your product, it works, and then one day a French user shows up — or your PM wants to expand to Germany — and suddenly you're staring at a codebase where "Hello, user!" is hardcoded in 47 components. Not ideal.

The good news: React's ecosystem in 2026 is genuinely excellent for i18n. The bad news: there are at least four credible libraries fighting for your attention, and picking the wrong one early stings later. This article focuses on the two most widely deployed — next-intl (tight Next.js integration) and react-i18next (the battle-tested workhorse that runs everywhere else).

Quick aside: i18n stands for internationalization (18 letters between the i and n). l10n is localization — same idea, different scope. i18n is your framework. l10n is the actual translated content plus locale-specific formatting. You need both, and they're different problems.

Worth noting: if you're also building out your UI component library alongside this work, the way you style locale-specific elements matters too. Components on Empire UI like the glassmorphism components or form inputs need to handle RTL text direction gracefully — something worth testing early.

next-intl in 2026: What's Changed

next-intl hit v4 in early 2026 and it's a meaningfully better library than the v2 most tutorials still show. The biggest shift: it leans fully into Next.js App Router's async server components. Translations are now fetched at the server level, zero client-side hydration cost for static text. That's a real win for performance.

Setup is still a bit ceremony-heavy, but less so than before. You configure a routing.ts file, wrap your layout, and you're done. Here's the minimum viable setup for a Next.js 15 app: ``ts // i18n/routing.ts import { defineRouting } from 'next-intl/routing'; export const routing = defineRouting({ locales: ['en', 'fr', 'de'], defaultLocale: 'en', localePrefix: 'as-needed' }); ` The localePrefix: 'as-needed' flag is subtle but important — your default locale (/en) stays clean at /, while /fr and /de` get their prefix. Saves you a 301 redirect for most of your traffic.

The useTranslations hook API is clean and TypeScript-first. If you generate types from your message files (which you should), you get full autocomplete on translation keys — a genuinely underrated DX improvement: ``tsx import { useTranslations } from 'next-intl'; export function WelcomeBanner({ name }: { name: string }) { const t = useTranslations('Home'); return <h1>{t('welcome', { name })}</h1>; } ` `json // messages/en.json { "Home": { "welcome": "Welcome, {name}!" } } ``

Honestly, if you're building a Next.js app and you know you need i18n from day one, next-intl is the obvious pick. The routing integration alone — locale-aware Link, redirect, useRouter — saves hours of manual plumbing. The middleware handles locale detection automatically based on Accept-Language headers.

One more thing — next-intl's date and number formatting is solid and ties directly into the JavaScript Intl API. format.dateTime(new Date(), { dateStyle: 'long' }) gives you locale-appropriate output without any extra config. No moment.js, no day.js dependency creep.

react-i18next: Still the Most Flexible Option

react-i18next (backed by i18next) is older, more opinionated about almost nothing, and runs in literally any React environment — Vite, CRA, Remix, Electron, React Native. If you're not on Next.js, this is your library.

The setup in 2026 looks roughly the same as it did in 2022, which is either comforting or a red flag depending on your perspective. You initialise i18next once, configure your backend plugin for loading translations, and wrap your app: ``ts // i18n.ts import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import HttpBackend from 'i18next-http-backend'; import LanguageDetector from 'i18next-browser-languagedetector'; i18n .use(HttpBackend) .use(LanguageDetector) .use(initReactI18next) .init({ fallbackLng: 'en', ns: ['common', 'dashboard'], defaultNS: 'common', interpolation: { escapeValue: false }, }); export default i18n; ``

The namespace system is react-i18next's killer feature for large apps. You split translations into logical chunks — common.json, dashboard.json, checkout.json — and lazy-load them only when needed. For an app with 40+ screens and 5 locales, that namespace splitting keeps your initial bundle sane. next-intl handles this differently (per-page message imports on the server), and honestly both approaches are fine at scale.

In practice, the useTranslation hook is slightly more verbose than next-intl's equivalent but it's predictable: ``tsx import { useTranslation } from 'react-i18next'; export function PricingCard({ price }: { price: number }) { const { t, i18n } = useTranslation('dashboard'); const formatted = new Intl.NumberFormat(i18n.language, { style: 'currency', currency: 'USD', }).format(price); return ( <div> <p>{t('pricing.perMonth', { price: formatted })}</p> </div> ); } ``

That explicit i18n.language in the Intl.NumberFormat call is something you'll do a lot — react-i18next gives you access to the current locale but doesn't auto-format numbers and dates for you. That's a legitimate footgun if you forget it. next-intl's format utility handles this automatically.

Pluralisation, ICU Messages, and the Edge Cases That Bite You

Pluralisation is where i18n gets genuinely hard. English has two plural forms (one item, two items). Arabic has six. Russian has three and the rules are... not simple. Your library needs to handle this, and most tutorials skip straight past it.

Both libraries support the Intl.PluralRules API under the hood. Here's how plurals look in react-i18next using the built-in plural suffix convention: ``json // en.json { "itemCount_one": "You have {{count}} item", "itemCount_other": "You have {{count}} items" } ` `tsx t('itemCount', { count: 5 }) // → "You have 5 items" t('itemCount', { count: 1 }) // → "You have 1 item" ` next-intl uses ICU message syntax instead, which is more verbose but explicit: `json { "itemCount": "{count, plural, one {You have # item} other {You have # items}}" } ``

ICU syntax is the standard for professional translation workflows. Most translation management systems — Phrase, Lokalise, Crowdin — export ICU by default. If you're working with a dedicated translation team rather than just running strings through an LLM, next-intl's ICU support is a real operational advantage.

What about gender? ICU handles that too with select. What about nested plurals — like "3 users posted 5 comments"? That's when things get genuinely messy, and both libraries handle it but neither makes it beautiful. Realistically you'll extract these cases into separate translation keys when they get complex enough.

Quick aside: don't sleep on the Trans component in react-i18next for strings with embedded JSX — things like "Click <Link>here</Link> to continue". Trying to handle those with string interpolation gets ugly fast: ``tsx import { Trans } from 'react-i18next'; <Trans i18nKey="clickHere" components={{ link: <a href="/docs" /> }}> Click <link>here</link> to continue </Trans> ``

Type Safety and Developer Experience in 2026

TypeScript support has gotten meaningfully better across both libraries. next-intl generates types from your message files automatically if you add a next-intl.d.ts configuration — you get compile-time errors when you misspell a translation key. After years of runtime-only translation errors, this is huge.

For react-i18next, you get the same via i18next's type augmentation system. It's a bit more manual to set up but works well: ``ts // i18next.d.ts import 'i18next'; import type ns from './public/locales/en/common.json'; declare module 'i18next' { interface CustomTypeOptions { defaultNS: 'common'; resources: { common: typeof ns }; } } ``

Once that's wired up, your editor will autocomplete translation keys and flag missing ones. Worth the 15-minute setup cost. Honestly, any codebase with more than two locales that isn't using typed translations is running on luck.

Look, both libraries also have good devtools extensions. The i18next browser extension shows you which keys are loaded, lets you switch locales, and highlights untranslated strings in yellow directly on the page. That alone speeds up QA for new locales considerably — you don't need to manually compare JSON files line by line.

One more thing — if you're building a component library or a UI kit (say, something you want to open-source or share across projects), react-i18next is the safer bet because consumers can drop it into any framework. next-intl is great but it's a Next.js library, full stop. Keep that in mind if you're building something reusable.

Routing, SEO, and the URL Question

Localised URLs are a real SEO concern, not just a UX nicety. Google's guidance since at least 2018 has been consistent: use separate URLs per locale, prefer subdirectories (/fr/) over query params (?lang=fr), and use hreflang tags to signal relationships between locale variants. Both libraries can get you there, but next-intl makes it almost automatic.

With next-intl, your [locale] segment in the app router handles everything. Each locale gets a real URL. You add alternates to your Next.js metadata and you're done: ``tsx // app/[locale]/layout.tsx export async function generateMetadata({ params }: { params: { locale: string } }) { return { alternates: { canonical: https://example.com/${params.locale}, languages: { 'en': 'https://example.com/en', 'fr': 'https://example.com/fr', }, }, }; } ``

With react-i18next in a non-Next.js app, you handle routing yourself. React Router 7 works fine here — you add a :lang param to your routes and match it to i18next's changeLanguage function. More boilerplate, but total control. If you're already doing something complex with routing (like the patterns covered in the nextjs-app-router-guide), next-intl slots in cleanly.

That said, both approaches work. The SEO delta between a well-implemented react-i18next setup and next-intl is basically zero. The difference is how much code you write to get there. If you're looking at react performance as part of the same project, server-side translation loading with next-intl gives you a meaningful head start on Core Web Vitals for content-heavy pages.

Picking One: A Practical Decision Tree

Here's the honest version of the decision. You're building on Next.js 14 or 15? Use next-intl. Full stop. The App Router integration, server-component support, and built-in routing make it the obvious choice. The TypeScript DX is excellent, ICU syntax integrates cleanly with professional translation platforms, and the performance story on static content is hard to beat.

You're building on Vite, Remix, Electron, React Native, or any non-Next.js React app? Use react-i18next. It's been solving this problem since 2015, has an ecosystem of backend plugins (HTTP, Fetch, local storage, Webpack bundle splitting), and the namespace system gives you fine-grained control over bundle size at scale.

You're building a shared component library that other apps will consume? Also react-i18next, or better yet — design your components to accept translated strings as props and stay framework-agnostic at the component level. Let the consuming app own the i18n layer. This is the same principle behind how the Empire UI component library handles content — components receive text as props rather than embedding translation logic.

The one scenario where neither is a slam dunk: microfrontend architectures where different teams own different route segments. There you might end up with next-intl for the shell app and react-i18next in federated modules, and you'll need to align on a shared locale state mechanism. Intl APIs are your friend there — they're native and need no coordination.

Both libraries are actively maintained as of mid-2026, both have excellent documentation, and both will be around in three years. You genuinely can't make a catastrophically wrong choice here. You can only make an ill-suited one — so match the library to your stack, not the other way around.

FAQ

What's the difference between i18n and l10n in React?

i18n (internationalisation) is the framework you build — routing, translation loading, plural rules. l10n (localisation) is the actual translated content and locale-specific formatting like dates and currencies. You need both, but they're separate concerns in your codebase.

Can I use next-intl with the Pages Router?

Technically yes, but it's a degraded experience. next-intl is built for the App Router and most of its performance advantages (server-component translations, zero client hydration) don't apply in Pages Router. You'd be better served by react-i18next if you're still on Pages Router.

How do I handle missing translations without crashing the app?

Both libraries fall back to the fallback locale (usually 'en') when a key is missing. Configure fallbackLng in react-i18next or fallbackLocale in next-intl. In production, also wire up an onMissingKey callback to log to your error tracker so you catch gaps before users do.

Is there a performance cost to loading translations client-side?

Yes, if you load all translations upfront as a JSON bundle. next-intl avoids this by loading translations server-side per route. With react-i18next, use namespace splitting and lazy-load namespaces only when the relevant route mounts — this keeps your initial JS payload lean.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Next.js App Router in 2026: What's Changed and What Still Trips People UpNext.js Server Actions in 2026: Forms, Mutations and the Right PatternsLanguage Switcher in React: Dropdown, Flag Icons, i18n RoutingNext.js vs Remix in 2026: Which One Should You Use?