Skip to content

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):

LibraryMinifiedMin + gzipRuntime deps
ilingo — full barrel~15.3 kB~6.0 kB2 (pathtrace, smob)
ilingoIlingo + MemoryStore~3.3 kB¹
i18next — core~42.4 kB~13.2 kB0

¹ 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:

Workloadilingo / i18next
Cache hit, simple string leaf2.03×
Plural lookup (count = 5)2.05×
Cache miss + 3-deep fallback chain2.33×
Template + Intl.NumberFormat modifier1.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

ilingoi18next
Maturitynewer, smaller communitysince 2011, very large community
Core size (min + gzip)~6.0 kB~13.2 kB
Module formatESM-onlyESM + CJS
TypeScriptfirst-party (TS source)first-party types
Plural rulesIntl.PluralRulesIntl.PluralRules (v21+)
Number / date / list formatIntl.*, built-inIntl.*, built-in (v21.3+)
Full ICU MessageFormat (select, nested)✗ — ICU-lite (plurals + {{var, formatter}})✓ via i18next-icu
Fallback chainBCP-47 walk, built-infallbackLng, built-in
Pluggable storageIStore port — memory / fs / lazy loader / customplugin backends
Lazy / code-split loadingLoaderStore, in-corei18next-http-backend (plugin)
File-system backend@ilingo/fs, in-corei18next-fs-backend (plugin)
Locale negotiationnegotiateLocale / parseAcceptLanguage, in-corei18next-browser-languagedetector (plugin)
Translate callget()Promise; getSync() → string when every store answers from memorysynct() returns a string
Vue@ilingo/vue (first-party)i18next-vue (official)
React— (framework-agnostic core is usable)react-i18next (first-party)
Plugin ecosystemsmall, younglarge, 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 primary get() returns a Promise — that async surface is precisely what lets any store be lazy- or network-backed without an API break, and it's what powers LoaderStore and FSStore. Since #988 there is also getSync(), which resolves the identical lookup without awaiting whenever every store consulted can answer from memory (the common "catalog compiled into the bundle" case) and returns undefined otherwise. So the async surface is the contract, not a tax on every call site: in Vue it's invisible either way — @ilingo/vue's useTranslation hides the await behind a reactive Ref and seeds that Ref synchronously, 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 cold LoaderStore / 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-icu supports the full ICU grammar (select, nested plurals, ordinals). ilingo is ICU-lite: CLDR plural categories via definePlural plus Intl formatters via {{value, number(...)}} — but no select / nested message grammar. If your translators rely on full ICU, stay on i18next.

Concept mapping

Porting a mental model — or a codebase — from i18next:

i18nextilingo
t('common:greeting')get({ namespace: 'common', key: 'greeting' })
t('key', { name }){{name}} in the message + data: { name }
key_one / key_other suffixesdefinePlural({ one, other })
namespaces (ns)namespace argument / defineNamespace
fallbackLng: ['de', 'en']fallback option (BCP-47 chain)
i18next-http-backendLoaderStore (loader: (l, ns) => import(...))
i18next-fs-backend@ilingo/fs FSStore
i18next-browser-languagedetectornegotiateLocale(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) — use i18next-icu or FormatJS.
  • You're React-first and want the most-used, best-documented bindings — react-i18next has 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), but get() 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.

Released under the MIT License.