Open-source project
alibaba/react-intl-universal avatar
alibaba/react-intl-universal

react-intl-universal: intl.get in vanilla JS, where injectIntl cannot reach

Internationalize React apps. Not only for Component but also for Vanilla JS.

1,354 stars159 forksJavaScriptLicense varies

At a glance

What is it?
Alibaba's react-intl-universal argues that wrapping components, as react-intl does with injectIntl, breaks two things: it cannot internationalise plain modules that are not components, and it breaks refs and property inheritance. Its answer is one intl.get call that works in both layers, ICU message formatting, 150 plus languages, and since 2.14 rich React components inside messages.
Who is it for?
react-intl-universal earns its place in a codebase where validation logic, error messages and formatting live in plain modules rather than in components, since that is precisely the case its API was designed around and the case injectIntl cannot serve. Skip it for a new project with one team on one stack and no legacy vanilla layer, since the HOC problems it solves do not arise when everything is a component.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One API, two layers

The package is published to npm as react-intl-universal and the README links both its npm page and a LICENSE.md file, so start by adding it to your project in the ordinary way:

bash
npm install react-intl-universal

The feature list then leads with the distinction that defines the library: it can be used not only in a React component but also in Vanilla JS. The API surface is centred on intl.get, and the README's framing is that there is no layer where you cannot produce a localized string. Around that sit pluralized labels in strings, variables in messages, React rich text component interpolation inside a message, support for more than 150 languages, and running in both the browser and Node.js. Message format follows ICU standards strictly, and locale data in nested JSON format is supported, which matters when a catalogue is split by feature area rather than held in one file. So the design is one lookup that behaves the same whether the caller is a view, a validator or a Node script.

The argument against injectIntl is about refs, not taste

The README explains why another i18n solution exists by showing react-intl code and what it costs. injectIntl wraps your component and injects the message object as props, which works, but the library's objections are concrete. First, it only applies in the view layer: a plain validation module exporting rules that return a warning string has no way to be internationalized, and the answer we all give, splitting that module into components just for the sake of translation, is worse than duplicating strings. Second, wrapping changes behaviour: you cannot reach the component instance through a normal ref, and have to ask for getWrappedInstance() instead, with the component opted in using injectIntl(MyComponent, {withRef: true}). Properties are also not inherited in subclasses, because the component is injected. Those are the two problems the design removes.

init is where the whole configuration lives

The documented init options are worth reading as a checklist for your own setup. escapeHtml controls html escaping and defaults to true. currentLocale takes a locale string such as en-US. locales is the application data itself, shaped as an object keyed by locale with message maps inside. warningHandler is described as a way to accumulate missing messages using a third party service, which is the hook for shipping missing keys to a service instead of reading the console. fallbackLocale names a locale such as zh-CN to use when a key is not found in the current one. And two options exist purely for debugging: debug wraps the message in a span carrying a data key, and dataKey sets what that attribute is called, defaulting to data-i18n-key. init returns a Promise, so locale loading is awaited before the first render.

Debug mode is the feature teams discover late

Those last two options are the ones that pay for the library in a large application. With debug on, every rendered message is wrapped in a span with a data key attribute, so a missing translation is not something you find by clicking through locales; it is something you can select in the browser and see which key produced it. Turning that on during development and off for production is the intended workflow, and it pairs naturally with warningHandler, which accumulates missing messages on the way. Neither feature is exotic, and together they answer the classic i18n failure: a string that exists in en-US and in three locales out of forty.

Rich components inside a message, since 2.14

The newest feature changes the recommended API. In 2.14 and later, intl.get is the recommended unified entry point for plain text, ICU variables and rich React components together. The point is that the sentence stays in the locale file as one complete string while React code controls the actual component, its props and its event handlers. That is the correct division: translators reorder a sentence when the grammar requires it, and the code decides what a link or a button is. The alternative, concatenating translated fragments around markup, breaks in every language whose word order does not match English. The README links a live demo with runnable examples rather than describing the syntax here, so read that before designing your message format.

Two tools around the API: an extractor and an agent skill

react-intl-universal-extract generates a locale file, which is the boring half of any i18n adoption: you have thousands of string literals to collect into one catalogue. The newer use-react-intl-universal skill is aimed at coding agents, and its framing is that agents need a workflow rather than API hints. The listed goals are specific: writing copy that preserves product meaning instead of translating word for word, keeping product terms and UI wording consistent across modules, avoiding broken layouts such as truncation, overflow, overlap and misalignment, keeping ICU variables, rich tags, default messages and locale files aligned, and reviewing translation quality against Multidimensional Quality Metrics, which the README links as a W3C community group framework. It also describes inspecting running localized pages by URL, clicking through interactions and capturing screenshots into a findings report, and the stated aim of smaller, more reviewable locale changes.

Against react-intl, and against doing it yourself

Two comparisons. Against react-intl, the difference is the shape of the integration: a wrapper that touches your component tree and a lookup that touches nothing. If your application is entirely components and you are happy with HOC semantics, react-intl is the more standard choice and you gain nothing here. If any meaningful user facing string is produced below the view layer, this library wins that argument outright. The second comparison is against rolling your own: a lookup function over a nested JSON catalogue with a fallback locale is perhaps two hundred lines, and ICU formatting is the part that is genuinely hard to do properly. That is what you are buying, plus the extractor and the debug tooling. What you give up is the formatjs ecosystem around react-intl and the certainty that its behaviour matches the official specification, since the strictness here is a claim the README makes rather than something you can check from the package.

Editorial conclusion

react-intl-universal earns its place in a codebase where validation logic, error messages and formatting live in plain modules rather than in components, since that is precisely the case its API was designed around and the case injectIntl cannot serve. Skip it for a new project with one team on one stack and no legacy vanilla layer, since the HOC problems it solves do not arise when everything is a component. Before adopting, read the init options and set warningHandler so missing keys are collected during development rather than discovered by a user, and turn on debug mode to have messages wrapped in a span carrying data-i18n-key, which makes missing strings findable in the DOM. Version 2.14.0 added rich component interpolation and an agent skill, and the last push was on 2026-10-13.

Frequently asked questions

What is the difference between react-intl-universal and react-intl?

The README argues that injectIntl from react-intl can only be applied in the view layer, so plain JavaScript modules cannot be internationalized, and that wrapping components breaks normal ref access, requiring getWrappedInstance(), and stops properties being inherited in subclasses. react-intl-universal's answer is a single intl.get API usable in both layers.

How do I find missing translation keys in react-intl-universal?

Pass a warningHandler to init, which the README describes as a way to accumulate missing messages using a third party service. For inspection in the browser, set debug mode on, which wraps each message in a span with a data key attribute, configurable through the dataKey option and defaulting to data-i18n-key.

What message format does react-intl-universal use?

The README states that the message format is strictly implemented by ICU standards, with pluralized labels and variables in messages supported, locale data in nested JSON format, support for 150 plus languages, and rich React component interpolation since version 2.14.

Official sources

  1. alibaba/react-intl-universal on GitHub
  2. Issues
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/alibaba-react-intl-universal.svg)](https://hysenlabs.com/projects/alibaba-react-intl-universal)

Was this useful?