ilingo vs. i18next
i18next is the most widely-used i18n library in the JavaScript ecosystem — mature (since 2011), framework-agnostic at its core, and surrounded by a large plugin ecosystem (backends, language detection, ICU, react-i18next, …). It's an excellent library and the closest functional peer to ilingo.
This page is an honest side-by-side: where ilingo is the better fit, where i18next is, and how the two map onto each other. No FUD — the numbers are sourced and the tradeoffs are real.
The one-paragraph version. ilingo is a smaller, faster, ESM/TypeScript-first core with batteries — lazy-loading stores, a file-system backend, locale negotiation — in the box. i18next is a larger, battle-tested ecosystem with a synchronous API, full ICU MessageFormat (via plugin), and first-party React bindings. Pick ilingo for a lean modern stack across Node / edge / browser / Vue; pick i18next for ecosystem depth, React, or full ICU.
What they share
It's worth clearing this up first, because it's a common misconception: modern i18next is not "non-standard" under the hood. Since v21 it selects plural forms with Intl.PluralRules, and since v21.3 it formats numbers / dates / lists with the Intl.* APIs — exactly like ilingo. So the difference is not "Intl-native vs. not". The real differences are bundle size, architecture, and the shape of the API.
Bundle size
Core packages, no plugins, measured with bundlephobia (minified, then min + gzip):
| Library | Minified | Min + gzip | Runtime deps |
|---|---|---|---|
| ilingo — full barrel | ~15.3 kB | ~6.0 kB | 2 (pathtrace, smob) |
ilingo — Ilingo + MemoryStore | — | ~3.3 kB¹ | — |
| i18next — core | ~42.4 kB | ~13.2 kB | 0 |
¹ ilingo's own CI-enforced budget (brotli) — see .size-limit.json and Performance → Bundle size.
ilingo's core is less than half i18next's core, gzipped — and the gap widens in a real app. A typical i18next deployment adds plugins: i18next-http-backend (loading), i18next-browser-languagedetector (negotiation), each shipping more bytes. ilingo's equivalents — LoaderStore, negotiateLocale / parseAcceptLanguage — are already counted inside that 6 kB. Tree-shaking goes further still: a defineCatalog-only import is ~1.2 kB.
ilingo declares "sideEffects": false, so a bundler drops every export you don't import.
Throughput
ilingo runs 1.6× – 2.3× faster than i18next per call across four representative workloads — despite ilingo's async get() paying a microtask per call. Full table, methodology, and hardware are on the Performance page; the ratios:
| Workload | ilingo / i18next |
|---|---|
| Cache hit, simple string leaf | 2.03× |
Plural lookup (count = 5) | 2.05× |
| Cache miss + 3-deep fallback chain | 2.33× |
Template + Intl.NumberFormat modifier | 1.63× |
These are micro-benchmarks — read the caveats. Per-call speed almost never dominates a real app (render and hydration do); treat it as evidence of a short resolution path, not a reason to switch on its own.
Feature by feature
| ilingo | i18next | |
|---|---|---|
| Maturity | newer, smaller community | since 2011, very large community |
| Core size (min + gzip) | ~6.0 kB | ~13.2 kB |
| Module format | ESM-only | ESM + CJS |
| TypeScript | first-party (TS source) | first-party types |
| Plural rules | Intl.PluralRules | Intl.PluralRules (v21+) |
| Number / date / list format | Intl.*, built-in | Intl.*, built-in (v21.3+) |
Full ICU MessageFormat (select, nested) | ✗ — ICU-lite (plurals + {{var, formatter}}) | ✓ via i18next-icu |
| Fallback chain | BCP-47 walk, built-in | fallbackLng, built-in |
| Pluggable storage | IStore port — memory / fs / lazy loader / custom | plugin backends |
| Lazy / code-split loading | LoaderStore, in-core | i18next-http-backend (plugin) |
| File-system backend | @ilingo/fs, in-core | i18next-fs-backend (plugin) |
| Locale negotiation | negotiateLocale / parseAcceptLanguage, in-core | i18next-browser-languagedetector (plugin) |
| Translate call | get() → Promise; getSync() → string when every store answers from memory | sync — t() returns a string |
| Vue | @ilingo/vue (first-party) | i18next-vue (official) |
| React | — (framework-agnostic core is usable) | react-i18next (first-party) |
| Plugin ecosystem | small, young | large, mature |
Two rows are the actual decision points — far more than bundle bytes:
- Sync vs. async. i18next's
t()is synchronous: call it in render, get a string back. ilingo's primaryget()returns aPromise— that async surface is precisely what lets any store be lazy- or network-backed without an API break, and it's what powersLoaderStoreandFSStore. Since #988 there is alsogetSync(), which resolves the identical lookup without awaiting whenever every store consulted can answer from memory (the common "catalog compiled into the bundle" case) and returnsundefinedotherwise. So the async surface is the contract, not a tax on every call site: in Vue it's invisible either way —@ilingo/vue'suseTranslationhides the await behind a reactiveRefand seeds thatRefsynchronously, so with an in-memory catalog the server-rendered markup and the first client render carry the real string. A store that still needs I/O (a coldLoaderStore/FSStore, a remote adapter) can't be seeded, so those keys start at the placeholder and settle a tick later — warm them if the first paint matters. - ICU MessageFormat. i18next +
i18next-icusupports the full ICU grammar (select, nested plurals, ordinals). ilingo is ICU-lite: CLDR plural categories viadefinePluralplusIntlformatters via{{value, number(...)}}— but noselect/ nested message grammar. If your translators rely on full ICU, stay on i18next.
Concept mapping
Porting a mental model — or a codebase — from i18next:
| i18next | ilingo |
|---|---|
t('common:greeting') | get({ namespace: 'common', key: 'greeting' }) |
t('key', { name }) | {{name}} in the message + data: { name } |
key_one / key_other suffixes | definePlural({ one, other }) |
namespaces (ns) | namespace argument / defineNamespace |
fallbackLng: ['de', 'en'] | fallback option (BCP-47 chain) |
i18next-http-backend | LoaderStore (loader: (l, ns) => import(...)) |
i18next-fs-backend | @ilingo/fs FSStore |
i18next-browser-languagedetector | negotiateLocale(supported, navigator.languages) |
addResourceBundle(...) | registerStore(new MemoryStore(...)) |
See Catalog Design, Stores, and Locales & Fallback for the details.
When not to choose ilingo
Honest disqualifiers — reach for i18next (or FormatJS) instead if:
- You have a large existing i18next codebase. The migration cost rarely pays back; both are good libraries.
- You need full ICU MessageFormat (
select, gender, nested plurals) — usei18next-icuor FormatJS. - You're React-first and want the most-used, best-documented bindings —
react-i18nexthas no ilingo equivalent today. - You depend on a specific i18next plugin (e.g. a locize workflow or a particular backend connector) with no ilingo store yet.
- You want a fully synchronous translate function with no async surface anywhere.
getSync()covers in-memory catalogs (including SSR first paint), butget()is still the authoritative path and the only one that can drive a lazy or remote store.
If none of those apply — and a small bundle and a framework-agnostic, modern core matter to you — ilingo is built for exactly that. Start with the Quick Start.