i18n React

Lingui vs i18next in 2026: which i18n library for React and React Native

Lingui and i18next compared from their docs and npm data: message format, macros, compile step, bundle size, plurals, TypeScript and React Native.

By Mateusz Muszarski Updated 6 min read Po polsku

Pick i18next if you want the largest ecosystem, keys you name yourself and translations loaded as JSON at runtime. Pick Lingui if you want to write the source text where it renders, keep plurals inside one ICU message and compile catalogs at build time, with a core of about 2 kB gzipped. I use Lingui in Linkoglot (30 languages, Expo) and on this site.

TL;DR

  • i18next has the reach: about 15 times Lingui's weekly npm downloads, a long plugin list and typed keys in TypeScript.
  • Lingui moves the work to build time: macros make the IDs, catalogs compile to JavaScript, and a bundler plugin can fail the build on a missing translation.
  • Both run in React Native on Hermes with Intl polyfills. Before you choose, test a plural in your hardest language on a real device.

Do you name keys or write source text?

The difference shows in the first line of code. With i18next you invent a key, put the text in a JSON file and look the key up at runtime:

{ "greeting": "Hello {{name}}" }
const { t } = useTranslation();
return <p>{t("greeting", { name })}</p>;

With Lingui you write the English text where it is displayed. A macro turns it into a message with a short generated ID at build time:

import { t } from "@lingui/core/macro";

t`Hello ${name}`;
// after the macro runs (example from the Lingui docs):
// i18n._({ id: "mVmaLu", message: "Hello {name}", values: { name } })

Neither model is wrong. Named keys stay stable when the English copy changes, and with TypeScript they autocomplete. Source text means there is nothing to name, and identical strings merge into one message. The flip side: an edited English sentence gets a new ID, so translators see it as new.

Merging has a catch too. My catalog has "Stop" twice: once as a button, once as a step on the How I work loop, where Polish needs "Etap". A context keeps them apart:

const stopLabel = t({ message: "Stop", context: "a stop on the product loop, as in a bus stop" });
msgid "Stop"
msgstr "Stop"

msgctxt "a stop on the product loop, as in a bus stop"
msgid "Stop"
msgstr "Etap"
The first step of the How I work loop in English, labelled Stop 01, Client en
The same step in Polish, labelled Etap 01, Klient pl
The same step on the home page in English and Polish. Without the context, the step and the button would share one Polish word.

Is the message format ICU in both?

Not by default. Lingui uses ICU MessageFormat throughout: placeholders, plurals and selects live inside one message, such as {count, plural, one {# file} other {# files}}. In i18next, {{name}} interpolates and each plural form is a separate key with a suffix. Interpolated values are escaped by default to mitigate XSS.

You can get ICU in i18next through the i18next-icu plugin, built on formatjs' intl-messageformat. Its README warns that with the plugin only the ICU format is respected, so {{name}} stops working. Pick one syntax per project.

How do macros, extraction and compiling work?

  1. 01 Write A macro wraps the English text where it renders.
  2. 02 Extract lingui extract collects the messages into one .po catalog per language.
  3. 03 Translate A person, a script or an LLM fills each catalog, with comments and context.
  4. 04 Compile lingui compile, the Vite plugin or the Metro transformer turns .po into JavaScript.
  5. 05 Ship The bundle gets compiled messages and no message parser.

Macros run at build time (Babel, SWC, or the native transform in @lingui/vite-plugin). Per the docs, they generate the short IDs, keep components type-checked as ordinary code, and strip translator comments and default messages from production builds. The extractor finds macros, explicit i18n._() calls and expressions marked with an /** i18n */ comment; it cannot extract a message stored in a variable.

i18next skips the compile step. JSON is bundled inline or loaded through backend plugins, and namespaces split translations so they load on demand. Extraction lives in i18next-cli, an SWC-based toolchain that extracts keys, generates TypeScript definitions, lints for hardcoded strings, syncs locale files and reports translation status.

How big are they?

From bundlephobia on 2026-10-05, minified and minified plus gzip (1 kB = 1,000 bytes):

Package Version Minified Gzipped
i18next 26.4.2 43.6 kB 13.7 kB
react-i18next 17.0.15 25.9 kB 10.2 kB
@lingui/core 6.9.0 5.0 kB 2.1 kB
@lingui/react 6.9.0 3.8 kB 1.8 kB

The core rows match Lingui's own comparison page. react-i18next declares i18next as a peer dependency, so its row leaves i18next out. Your catalogs add bytes either way and tree-shaking changes the totals, so measure your own build before you quote a number.

How do they handle plurals?

Both follow the CLDR plural categories (zero, one, two, few, many, other) and both rely on Intl.PluralRules. The shape of the data differs. English needs two forms; Polish needs four. In i18next that is four keys, and the variable must be called count:

{
  "files_one": "{{count}} plik",
  "files_few": "{{count}} pliki",
  "files_many": "{{count}} plików",
  "files_other": "{{count}} pliku"
}

In Lingui the developer writes only the English forms with the plural macro, and the Polish translator fills one message:

msgid "{count, plural, one {# file} other {# files}}"
msgstr "{count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}}"

The code does not change when a language with more forms arrives. That is the practical argument for ICU: each language's grammar stays in that language's file.

What about TypeScript?

i18next offers type-safe keys and return types. You declare your resources in a CustomTypeOptions augmentation, and t checks and autocompletes keys. The newest versions require TypeScript 5, and the docs warn that type safety can slow compilation on big projects (an optimized selector API exists for that).

Lingui has no keys to type. Macros are ordinary template literals and JSX, so TypeScript checks the values you interpolate like any other code. You lose key autocomplete and gain no key list to keep in sync.

Do they work in React Native and Expo?

Both work, and both need the same polyfills on Hermes. The i18next plural docs say Hermes still does not implement Intl.PluralRules. Since i18next v24 there is no fallback: without a polyfill only the English-style _one and _other forms resolve. Lingui's React Native tutorial (Lingui 5 or newer, React Native 0.76 or Expo 52, Hermes) has you polyfill Intl.Locale and Intl.PluralRules with the formatjs packages.

react-i18next calls itself a framework for React and React Native. Lingui uses the same @lingui/react on both, adds a Metro transformer for .po files, and a defaultComponent option so translated text renders inside Text. Whichever you pick, test a plural on a real device: a Polish plural stuck in the other form is easy to miss in English-first QA.

How big is each ecosystem?

i18next is far bigger. In the week from 2026-09-28 to 2026-10-04, npm counted about 27.8 million downloads of i18next and about 1.9 million of @lingui/core. Beyond React, i18next has integrations for Angular and Vue, runs on Node.js and Deno, and has a long list of plugins (backends, language detection, converters from gettext, CSV and RESX). Its maintainers also build Locize, a translation management service made for i18next.

Lingui covers React, React Native, Vue, SolidJS, Astro, Svelte and Node.js on top of the framework-agnostic @lingui/core. Its catalogs are PO files by default, the gettext format: readable for long messages, with comments, contexts and metadata, and supported by many localization tools. The comment and context you pass to a macro land in the catalog, which helps a human translator and an LLM alike.

How do I use Lingui?

Linkoglot is my language-learning app, built with Expo and React Native. With 30 catalogs, the file format matters as much as the API. PO files with ICU messages keep each language's plural rules in its own file, and plain text is easy to process with a script. That script later became lingui-translate-ai, a CLI that fills Lingui catalogs with an LLM, described in Translating Lingui catalogs with an LLM.

This site runs on Astro 7 with lingui-for-astro, a community integration (not made by the Lingui team) that transforms macros in .astro files and extracts their messages. The Vite plugin compiles each .po file on import, so no compiled catalog is committed, and production builds set failOnMissing:

lingui({
  macroTransform: true,
  failOnMissing: BUILD ? "catalog" : false,
  failOnCompileError: true,
});

With "catalog", the build fails if the Polish or Spanish catalog misses a message, checked before fallback locales apply. A missing Polish string cannot reach production as silent English. The routes, hreflang and language switch around it are in a multilingual Astro site with Lingui.

Which one should you pick?

If you need Lean towards
The largest ecosystem, or a hosted service from the same maintainers (Locize) i18next
Translations loaded from a server at runtime i18next
Named keys with TypeScript autocomplete i18next
Source text in the code, no keys to name Lingui
Plurals and selects inside one ICU message Lingui (or i18next with i18next-icu)
The smallest core runtime Lingui
A build that fails on a missing translation Lingui
PO catalogs for translators and scripts Lingui
React Native on Hermes Either, with the Intl polyfills

Limits

  • It is not a benchmark. Every behaviour comes from the official docs; I did not build the same app with both.
  • The numbers are a snapshot. Sizes from bundlephobia and downloads from the npm API, read on 5 October 2026; check the current ones before you quote them.

If you are choosing for a team, try both on one real screen with a plural in it, in the language with the most plural forms you support. The projects where I use Lingui are on my work page.

Sources

  1. Lingui: Introduction (opens in a new tab) (accessed )
  2. Lingui vs i18next (opens in a new tab) (accessed )
  3. Lingui: Macros reference (opens in a new tab) (accessed )
  4. Lingui: Message extraction (opens in a new tab) (accessed )
  5. Lingui: Pluralization (opens in a new tab) (accessed )
  6. Lingui: Catalog formats (opens in a new tab) (accessed )
  7. Lingui: React Native tutorial (opens in a new tab) (accessed )
  8. Lingui: Vite plugin (opens in a new tab) (accessed )
  9. i18next documentation (opens in a new tab) (accessed )
  10. i18next: Plurals (opens in a new tab) (accessed )
  11. i18next: Interpolation (opens in a new tab) (accessed )
  12. i18next: TypeScript (opens in a new tab) (accessed )
  13. i18next-icu (GitHub) (opens in a new tab) (accessed )
  14. i18next-cli (GitHub) (opens in a new tab) (accessed )
  15. react-i18next documentation (opens in a new tab) (accessed )
  16. @lingui/core on npm (opens in a new tab) (accessed )
  17. i18next on npm (opens in a new tab) (accessed )
  18. lingui-for-astro on npm (opens in a new tab) (accessed )
  19. Bundlephobia: i18next 26.4.2 (opens in a new tab) (accessed )
  20. Bundlephobia: react-i18next 17.0.15 (opens in a new tab) (accessed )
  21. Bundlephobia: @lingui/core 6.9.0 (opens in a new tab) (accessed )
  22. Bundlephobia: @lingui/react 6.9.0 (opens in a new tab) (accessed )
  23. npm downloads API: i18next, last week (opens in a new tab) (accessed )
  24. npm downloads API: @lingui/core, last week (opens in a new tab) (accessed )

All posts

Article 6 min read

Translating Lingui catalogs with an LLM

How to machine-translate Lingui .po catalogs with an LLM, what breaks (placeholders, plurals, tone) and the checks to run before a catalog ships.