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)
ICU MessageFormatLimited interpolationFull support (the gold standard)
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 needing strict ICU

Pick i18n-keyless for SaaS UI, MVPs, and products where you'd rather not maintain JSON files. Pick react-intl if your app does heavy plural/select formatting, ordinal numbers, or relative time that absolutely must follow CLDR rules.

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. If your product genuinely needs this, no other approach is as expressive.

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. <I18nKeyless>Confirm payment</I18nKeyless> — that's the entire translation system from the engineer's perspective.

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

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

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 ICU MessageFormat.

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. ICU MessageFormat lets you encode plural rules, gender, ordinals, and select-statements in a single message string, and react-intl evaluates them locale-aware.

i18n-keyless supports value interpolation (see replace-values) — you can do Welcome, {name}! and substitute. But ICU plural rules are a known limitation. For pluralized strings, you typically have two patterns:

// Option 1: separate strings per case (preferred for keyless)
{count === 0 && <I18nKeyless>No items</I18nKeyless>}
{count === 1 && <I18nKeyless>1 item</I18nKeyless>}
{count > 1 && <I18nKeylessText replace={{ '{count}': count }}>{`{count} items`}</I18nKeylessText>}

// Option 2: locale-specific plural rules at the application level
const message = new Intl.PluralRules(locale).select(count); // 'one' | 'other' | ...

Both work. Neither is as elegant as react-intl's ICU. If your app is plural-heavy in non-English locales, this matters.

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

✅ Your app has heavy ICU plural/select needs across diverse locales (Polish, Arabic, Russian, etc.). ✅ 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 the small set of ICU-critical strings (e.g., the cart summary with plurals), and i18n-keyless for the 95% of UI that's just labels and buttons. 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