Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
If you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
How to pick the right React i18n library
React ships no i18n primitive. The library you pick on day one decides how translations are stored, how they reach the bundle, and how much of the work stays yours for the next few years. Most teams pick by popularity, then discover the trade-offs at 2,000 keys.
This guide goes the other way: answer a few questions about your project first, then map the answers to the libraries that fit. It focuses on plain React (Vite, React Router, TanStack Start). Next.js has its own constraints, covered in the Next.js comparison.

Table of Contents
Six questions to answer before comparing libraries
A feature table is useless without knowing which rows matter to you. Go through these first.
- How is the app rendered? SPA only, SSR with hydration, or React Server Components. Context-based hooks work everywhere in an SPA. With RSC, a hook forces
"use client"on every component that renders text, so you will need a server-side API too. - Who writes the translations? Developers, an in-house team using a TMS, an agency delivering ICU files, or an AI pipeline. This dictates the catalog format more than any API detail.
- How many locales and pages? Two locales and five pages can afford to ship everything. Ten locales and fifty routes cannot, and the loading strategy becomes the main cost.
- Do you need types on keys? A typo in
t("checkout.totl")compiles in every key-based library unless you wire the types yourself. Decide whether that is acceptable. - What does the string contain? Plain text, plurals, or sentences with a
<Link>in the middle. Rich content is where most APIs get awkward. - How long will the project live? A three-month prototype and a five-year product do not need the same amount of build tooling.
Write the answers down. Everything below refers back to them.
The landscape in one picture
Fifteen years of JavaScript i18n fit in four architectural waves, and the React libraries you will compare come from different ones.

JSON catalogs loaded in memory, t("a.b") looked up at runtime, ICU or a custom syntax parsed in the browser. Largest ecosystems, heaviest runtimes, types are opt-in.
Messages extracted at build, compiled to compact catalogs, typed arguments. An extra build step (extract, compile) in exchange for smaller bundles.
Designed around SSR and Server Components. Render on the server, hydrate only what the client needs. Still key-based and centralized.
Content is compiled into tree-shakable functions or per-component dictionaries. Types are generated, missing translations fail the build, and AI translation runs from the CLI.
The history of JavaScript i18n details how each wave answered the previous one's problems.
The decision that matters most: where content lives and when it loads
Every React i18n library has the same shape: a store, a provider, a hook. Whatever the provider receives ends up in the client bundle or in the hydration payload. So the two structural choices are:
- Centralized or scoped content. One
en.jsonfor the app, or one declaration per component (or per namespace). - Static or dynamic import. Everything bundled at startup, or the active locale and route fetched on demand.
The graph below estimates the payload for a theoretical app of 1 to 10 pages, translated into 1 to 10 locales, with about 30 KB of text per page.

Centralized content with static imports grows with both axes: 10 pages times 10 locales is 300 KB of text on every page. Dynamic imports remove the locale axis. Scoping removes the page axis. Only the combination stays flat.
This is not a library property, it is a discipline property. react-i18next can be scoped with namespaces and lazy backends. use-intl can be split per route. But nothing enforces it, and a shared <Button> reaching for t("common:cta") quietly makes common a dependency of every route. The benchmark measures this as "leakage from other routes" and "leakage from other locales", and it is where most of the gap between libraries comes from.
If your answer to question 3 was "many locales, many pages", weigh this section more than any API preference. The per-component vs centralized i18n post goes deeper on the maintenance side of the same choice.
The candidates
Library sizes come from the TanStack Start benchmark: provider plus hook in an empty component, after bundling, tree-shaking and minification, 10 pages and 10 locales. Content is measured separately.
Open the table in a modal to view all data content clearly
| Library | Wave | Content model | Types on keys | Message format | Library size |
|---|---|---|---|---|---|
react-i18next | Runtime | Central JSON, namespaces | Opt-in (CustomTypeOptions) | i18next (suffix plurals) | ~18.4 kB |
react-intl (FormatJS) | Runtime | Central JSON, ICU | Opt-in (extraction + union) | ICU | ~15.3 kB |
use-intl | Server-first | Central JSON, ICU | Opt-in (declaration merging) | ICU | ~14.1 kB |
@tolgee/react | Runtime | Central, in-context editing | No | ICU | ~11.1 kB |
| Lingui | Macro | Source text in code, compiled catalogs | Good, from the compiler | ICU via macros | Small |
| Paraglide | Compiler | inlang project, generated functions | Generated | Own | Near zero |
| Intlayer | Compiler | .content.ts per component | Generated, on by default | Helpers (plural, enu) | Baseline |
Numbers are a snapshot at the benchmark's versions and change with releases. Run the benchmark on your own app before deciding on size alone.
Two things the table does not show. Paraglide ships almost no library because it generates code into your repo, which means a regeneration step before every commit and merge conflicts on generated files. And Intlayer requires a bundler plugin (vite-intlayer or equivalent), so it cannot run in a no-build setup.
Match your answers to a library
Pick the simplest thing that works and do not over-invest. react-i18next with a single JSON per locale is fine, and the decade of Stack Overflow answers will save you time. Skip namespaces until you need them. If the prototype becomes a product, budget a migration to scoped content; the react-i18next compat adapter makes that incremental.
Your catalog format is decided for you. react-intl is ICU-native and the FormatJS extraction tooling is built for that pipeline. use-intl also reads ICU. react-i18next needs the ICU plugin and its own plural keys otherwise. Intlayer's ICU support is still partial, so if you receive ICU strings today, treat that as a blocker until it lands.
Prefer scoped content and dynamic loading by default, not by convention. Lingui and Paraglide get there through compilation. Intlayer gets there through per-component declarations, and the compiler ships only what a route renders. With react-i18next or use-intl, plan the namespace and lazy-loading strategy on day one and enforce it in review, because the tooling will not.
Every key-based library can be typed, and almost none is by default. If you do not want to maintain declaration merging that has to survive lazily loaded namespaces, pick a library where types are generated from the content: Lingui, Paraglide, or Intlayer. The detecting missing translations post compares what each catches at build time.
Rich nodes are where t() returning a string breaks down. react-i18next and Lingui have <Trans>, react-intl has rich text tags, all of them more awkward than the plain string case. Intlayer's content nodes accept JSX, markdown and nested objects directly, which is the better fit if content is more than UI labels.
Then a centralized JSON is no longer a requirement, since there is no TMS to import into. Colocated content plus a CLI that fills missing locales is the shorter path. Intlayer's fill command runs against your own API key (OpenAI, Anthropic, Mistral, Gemini) and only translates what changed. Paraglide and Tolgee offer hosted equivalents with their own plans.
React context does not cross the server/client boundary. Libraries built on a client hook alone (react-i18next, react-intl) will need a parallel server API the day you adopt RSC. use-intl (as next-intl) and Intlayer (as next-intlayer) already have that split. Read the Next.js i18n post before standardizing a pattern.
Where each library falls short
Honest limits, since every option has them.
react-i18next: heaviest of the set, its own plural format, types are your wiring to maintain, dead keys accumulate silently.react-intl: verbose DX (useIntl()thenformatMessage({ id })), global instance tied to many nodes.use-intl: simple to start, painful to optimize. Namespaces, dynamic loading and types together slow development a lot.Lingui: extraextract/compilebuild step, several overlapping syntaxes (t(), tagged template,i18n.t(),<Trans>) that confuse both humans and AI assistants.Paraglide: generated files in the repo, tree-shaking did not take effect in the React benchmark, and the locale is read from storage on every node rather than from a store.Tolgee: no key types, harder onboarding, in-context editing is the selling point.Intlayer: mandatory build plugin, smaller ecosystem, partial ICU support, content spread across the codebase by design so exporting one JSON for a translator needs tooling.gt-react,lingo.dev: not recommended in the benchmark: quota errors at build, vendor lock-in, and reactivity issues that required forcing provider re-renders.
What each option looks like in code
The same component, a cart summary with a title and a plural, written with each candidate. The interesting part is not the component, it is where the content lives and what the type checker knows about it.
Copy the code to the clipboard
Copy the code to the clipboard
Plurals are suffix keys resolved through Intl.PluralRules. t is (key: string) => string unless you declare CustomTypeOptions, so t("titel") compiles.
Copy the code to the clipboard
Copy the code to the clipboard
ICU end to end, which is what most TMS platforms export. Types on id come from the formatjs extraction step plus a generated union, not out of the box.
Copy the code to the clipboard
Copy the code to the clipboard
Same shape as next-intl without the Next.js bindings. Keys are typed once you augment AppConfig with the messages type; namespaces are yours to split.
Copy the code to the clipboard
Copy the code to the clipboard
The source language lives in the component; other locales live in .po files under hashed ids after lingui extract. Forgetting extract or compile silently falls back to English.
Copy the code to the clipboard
Copy the code to the clipboard
Every message is a generated, typed function, so a missing key is an import error. The paraglide/ folder is generated into your repo and regenerated on every change.
Copy the code to the clipboard
Copy the code to the clipboard
All locales in one file beside the component. Types are generated at build, so title autocompletes and a typo fails tsc with no declaration merging. Deleting the folder deletes the strings.
Already on react-i18next, react-intl or Lingui? The compat adapters (react-i18next, react-intl, Lingui) alias the imports at the bundler level so the existing API keeps working while you move component by component. The migration guide covers the rest.
Before you commit
A feature table tells you what a library does today. These points tell you what living with it will be like.
Check repository activity.
Commits, issue response time, and whether the last minor release was this year. A sound design with no maintainer is a migration in waiting.
Do not pick by npm downloads.
The most installed library is the one that shipped first, not the one that fits a 2026 React codebase. Downloads measure history, not fit.

Ask who pays the maintainer, and what they sell.
i18next is backed by Locize. next-intl / use-intl, vue-i18n, svelte-i18n and Lingui are backed by Crowdin. Tolgee, Paraglide (inlang) and Intlayer each run their own platform. A vendor whose revenue is hosted translation has little reason to make translation free inside your toolchain. Intlayer is the only one of the set that ships AI translation through the CLI with your own API key, and a CMS you can self-host.
Is it AI-agent ready?
Agents still struggle with i18n: they forget locales, invent keys, and mix message syntaxes. Does the library ship Agent Skills or an MCP server so the agent can list, fill and test content? And is content loading optimized by default, or does someone have to review namespaces and lazy imports every quarter?
Type safety out of the box.
Not "can be typed with extra wiring" but "a wrong key fails tsc on a fresh install". Check what happens with a key that does not exist, and with a locale that is missing one translation.
Detection of unused content.
Catalogs only grow. Intlayer's build purges unused fields and logs them (build.purge). Paraglide gets there by architecture, since an uncalled message function is tree-shaken. Everything else leaves the sweep to you.
Developer experience.
Setup time to first translated string, an LSP or VS Code extension that shows the translation on hover and jumps to the declaration, a CLI for fill, test and push, and a way for non-developers to edit content (visual editor or CMS) without a pull request.
Frequently Asked Questions
Yes for most teams. It has the largest ecosystem and the most answers online. Its costs are real but predictable: the heaviest runtime, a custom plural format, and type safety plus scoping that you have to set up and defend yourself.
Only if bundle size, generated types or build-time missing-key checks are among your requirements. For a small app with two locales, a runtime library is simpler. The compiler vs declarative i18n post explains what compilers give you and what they can get wrong.
Partially. Key-based libraries share enough shape that a compat adapter can alias one API to another, which is how the Intlayer adapters work. Message formats (ICU vs i18next vs helpers) do not convert automatically, so plurals and interpolation are the part you will touch.
Indirectly. What crawlers see is decided by routing, hreflang, <html lang> and whether text is in the server-rendered HTML. Some libraries ship helpers for that, most leave it to you. See the hreflang guide.
Going further
- i18n library benchmark: bundle size, leakage and locale-switch timings and the TanStack Start report
- React i18n: how the provider model works and what it costs
- react-i18next vs react-intl vs Intlayer, feature by feature
- next-i18next vs next-intl vs Intlayer
- The history of JavaScript i18n
- Compiler vs declarative i18n
- Per-component vs centralized i18n
- How bundle optimization works at build time
- Set up i18n in a Vite + React app
- Same guide for Vue, Svelte and Solid
Comments
No comments yet. Be the first to share your thoughts.
