Skip to main content

i18n-keyless vs react-intl (FormatJS): a 2026 comparison

· 8 min read
Founder of i18n-keyless

react-intl (part of the FormatJS ecosystem) is the i18n library that takes the ICU MessageFormat standard seriously. If you've ever needed to write {count, plural, one {# item} other {# items}} and have it Just Work in Polish, Arabic, and Russian, react-intl is what you reach for.

i18n-keyless takes a different bet: most product strings don't need ICU. They need to be translated, cached, and not require a JSON file. So we optimize for that path.

This is an honest comparison. Both libraries are good at different things.

TL;DR​

i18n-keylessreact-intl
Translation keysNone — source string is the keyRequired (id or defaultMessage)
Plurals, ordinals, gendercount / ordinal / select, forms written by the modelHand-authored ICU MessageFormat
Number / date / currency formattingDIY (use Intl.* directly)Built-in via FormattedNumber / FormattedDate
AI translationBuilt inDIY
Locale filesNoneOne per locale
Setup time~5 minutes~1 day
Best forProduct UI in early-stage appsContent-heavy apps with FormatJS tooling

Pick i18n-keyless for SaaS UI, MVPs, and products where you'd rather not maintain JSON files. Pick react-intl if you need relative-time formatting components or want every plural branch in a JSON file you control.

What react-intl is great at​

Let's start with what react-intl does well, because it's a genuinely good library.

ICU MessageFormat is the standard​

react-intl's <FormattedMessage> accepts ICU MessageFormat syntax:

<FormattedMessage
id="cart.summary"
defaultMessage="{count, plural, =0 {Your cart is empty} one {# item, {total}} other {# items, {total}}}"
values={{ count: cart.length, total: <FormattedNumber value={total} style="currency" currency="USD" /> }}
/>

This handles plural forms (zero, one, two, few, many, other) per CLDR rules — Polish has different forms from English from Arabic from Russian. i18n-keyless covers the same CLDR rules from the other direction: you write one source form, and the model produces every branch each language needs. The difference is authorship, not coverage.

Excellent number, date, and currency formatting​

<FormattedNumber>, <FormattedDate>, <FormattedRelativeTime>, <FormattedList> — all backed by Intl.* APIs. Locale-aware out of the box, well-typed, well-documented.

Strong tooling​

@formatjs/cli extracts messages, validates ICU syntax, integrates with TMS pipelines. The FormatJS team takes correctness seriously.

Type safety​

react-intl + TypeScript gives you typed message IDs and values. Refactor a message ID and the type-checker catches every reference.

What i18n-keyless is great at​

No keys​

The source string is the lookup. <I18nKeylessText>Confirm payment</I18nKeylessText> — that's the entire translation system from the engineer's perspective.

// react-intl
<FormattedMessage id="checkout.confirm" defaultMessage="Confirm payment" />

// i18n-keyless
<I18nKeylessText>Confirm payment</I18nKeylessText>

The id field is the heart of react-intl. Every message has one. You name them, organize them, refactor them. With i18n-keyless, there's nothing to name.

Auto-translation​

When react-intl encounters a message in a locale where it doesn't have a translation, it falls back to defaultMessage — and you (or your TMS) need to fill in the gap. With i18n-keyless, the SDK queues an AI-generated translation in the background, caches it, and serves it to every future user in that locale.

Setup time​

# react-intl
npm install react-intl
# Then: write a babel/SWC plugin config, set up message extraction,
# pick a key naming strategy, write locale loaders, configure IntlProvider...

# i18n-keyless
npm install i18n-keyless-react
# Then: paste API key, wrap strings.

See the quick-setup guide.

One source of truth, no JSON files​

react-intl wants you to maintain en.json, fr.json, de.json — extracted by tooling, often round-tripped through a TMS. i18n-keyless keeps translations in our backend, accessed via API, cached client-side. Nothing in your repo to keep in sync.

The ICU question​

The biggest real difference between these libraries is who writes the ICU.

If your app has copy like:

"You have 3 unread messages from 2 contacts in the last 24 hours."

…and that copy needs to be correct in 8 locales including ones with non-trivial plural rules — react-intl handles that elegantly, and so does i18n-keyless, from opposite directions.

With i18n-keyless you write one source form with {count} and pass the number as count:

<I18nKeylessText count={cart.length}>{"{count} items in your cart"}</I18nKeylessText>

The API stores one ICU MessageFormat message per language with exactly that language's CLDR plural categories — French 2 forms, Russian and Polish 4, Arabic 6 — validated against Intl.PluralRules before it's saved. The SDK picks the branch with the same Intl.PluralRules. Gender and other closed sets work the same way with select. See the plurals and genders guide.

i18n-keyless covers all CLDR plural categories for every supported language. Zero-specific display text ("Your cart is empty" instead of "0 items") is a conditional in the component — that's display logic, not a plural rule.

Number / date formatting​

react-intl gives you <FormattedNumber>, <FormattedDate>, etc. We don't — but you don't really need a library for this in 2026:

// Native JS
new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }).format(total);
new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(date);
new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(-1, "day");

That's the same engine react-intl wraps. The wrapper is a convenience, not a capability. If you find react-intl's <FormattedNumber> valuable, write a 3-line component yourself.

When to choose react-intl​

✅ You want every plural branch in a JSON file you control. ✅ Your team already uses FormatJS tooling (@formatjs/cli, message extraction). ✅ You want strict typed message IDs. ✅ You have a TMS workflow that consumes ICU MessageFormat. ✅ You work on a content-heavy site (news, marketplace) with editorial copy.

When to choose i18n-keyless​

✅ Your app is a SaaS or product UI dominated by buttons, labels, short paragraphs. ✅ You want to ship multilingual without setting up tooling, extractors, or locale files. ✅ You add languages frequently and don't want a translation project each time. ✅ Engineers write the copy directly and you want it readable in the JSX. ✅ You'd rather review AI translations than write them from scratch.

Migration path​

If you're on react-intl and considering keyless, the migration is similar to the i18next path: dual-run during a sprint, keyless handles new strings, react-intl continues serving extracted messages, then sweep through and replace <FormattedMessage> calls.

See Migrate from react-intl to i18n-keyless.

A note on hybrid setups​

Some teams use both: react-intl for strings deeply integrated into FormatJS tooling, and i18n-keyless for the rest of the UI. There's no conflict between them — they're independent libraries that don't fight for the same DOM.

FAQ​

Can I use FormatJS's Intl.* polyfills with i18n-keyless?​

Yes — they're orthogonal. FormatJS polyfills don't depend on react-intl, and i18n-keyless doesn't depend on Intl.*. Mix freely.

Does i18n-keyless extract messages at build time?​

No. Messages aren't extracted because they aren't keys — the source string in your JSX is sent to the backend at runtime (and cached). If build-time extraction is a hard requirement (e.g., for offline TMS export), keyless isn't the right tool.

Is FormatJS / react-intl deprecated?​

No, react-intl is actively maintained and an excellent choice for the use cases it targets. We don't recommend keyless as a replacement for all react-intl use cases — only for the product-UI majority.

What about Lingui?​

Lingui (@lingui/react) is a similar library to react-intl but with a more developer-friendly macro syntax. It shares react-intl's strengths (ICU, message extraction) and the same trade-off vs keyless (you maintain locale files).

Next steps​