use-intl
use-intl is the framework-neutral React core of next-intl. An `IntlProvider` supplies a locale, ICU message catalog, timezone, named formats, current time, and error policy; hooks then translate messages and format numbers, dates, relative times, lists, and display names. Messages support variables, plural and select branches, and rich React elements. The package uses JavaScript Intl and FormatJS internals, ships type declarations, and supports React 17 through 19. It does not provide Next.js routing, locale detection, catalog fetching, or a translation-management service.
A strong React-only i18n core if you like ICU messages and want explicit, typeable formatting without the rest of next-intl. Do not mistake it for a localization platform or routing solution, and budget for catalog loading, ICU training, fallbacks, timezone consistency, and runtime Intl support.
Use it if
- You want next-intl's hook and ICU-message model in React without adopting Next.js or locale-aware routing
- Your product needs plural rules, select branches, rich text, number and date formatting, and shared named formats from one provider
- You want message-key autocompletion and value checking through TypeScript module augmentation
- You need explicit locale, timezone, current-time, fallback, and error inputs so server and client formatting can agree
- You need locale-prefixed URLs, middleware, navigation helpers, or React Server Component integration: those are next-intl features, while use-intl is the generic React provider and hook layer
- Your translators expect plain key-value text with no ICU syntax: plural, select, date, and rich-text messages require ICU rules whose braces, apostrophe escaping, and branch requirements add authoring and QA work
- You need a translation backend, editor, machine translation, or catalog synchronization: the README says messages can come from anywhere and leaves loading and translation operations to the application
- You must support React 16 or synchronous CommonJS require(): version 4.13.5 peers with React 17 through 19, declares `type: module`, and exposes ESM defaults only
- Your runtime lacks the Intl capabilities used by your chosen formatters: the package is standards-based, but features such as Intl.ListFormat and Intl.DisplayNames still depend on runtime support or application-supplied polyfills
Setup reality
Install `use-intl` and provide compatible React 17, 18, or 19. Version 4.13.5 is ESM-only, exports separate `use-intl/core`, `use-intl/react`, and message-format subpaths, and includes TypeScript declarations. The minimum working tree wraps consumers in `IntlProvider` with a valid Unicode locale tag and a messages object. There is no required config file, native build, credential, loader, or translation service, but there is also no automatic locale detection or message fetching. Your application must decide the locale, load the right catalog, handle a failed catalog request, and remount or update the provider when the user switches languages. Components outside the provider throw when they use hooks. Message keys are nested by namespace and strings use ICU MessageFormat, so literal braces and apostrophes require ICU-aware escaping; plural messages should include `other`, and values must match the message variables. Provider values matter for rendering consistency. If `timeZone` is absent, formatting uses the user's zone; server-rendered HTML can then disagree with the browser. Supply an IANA timezone and a stable `now` when deterministic server and client output matters. `useNow()` is static when the provider supplies `now` unless an `updateInterval` is requested; an interval creates recurring renders and must be chosen according to the visible precision. The provider memoizes its context, but newly created `messages`, `formats`, callback, or Date objects can still re-render every consumer, so load catalogs once and memoize derived configuration. Missing or invalid messages default to console errors while rendering a key-like fallback, which is convenient in development but too easy to overlook in production. Set `onError` for reporting and `getMessageFallback` for deliberate user-facing output. Rich messages execute component functions supplied by your code, not HTML from the catalog, which avoids treating translator text as markup but requires each expected tag to be mapped. Type-safe keys are opt-in through module augmentation against the catalog shape; very large inferred catalogs can increase TypeScript work. Because use-intl does not bundle locale data beyond the platform Intl implementation, older or constrained runtimes may need FormatJS polyfills for the exact Intl APIs used. Catalog extraction and the Next.js plugin workflow live elsewhere in the monorepo, so do not assume installing this core package creates or updates translation files.
Patterns
Provide a locale and message catalogconfigure-provider
import { IntlProvider } from 'use-intl';
const messages = { App: { greeting: 'Hello {name}!' } };
root.render(
<IntlProvider locale="en" messages={messages} timeZone="UTC">
<App />
</IntlProvider>
);The application must load the correct catalog and choose the locale. A fixed timezone avoids server and browser formatting differences.
Translate within a message namespacetranslate-namespaced-message
import { useTranslations } from 'use-intl';
function Header() {
const t = useTranslations('Header');
return <h1>{t('title')}</h1>;
}The component must render below IntlProvider, and the catalog must contain `Header.title` for the active locale.
Insert values into a translated messageinterpolate-values
// messages: { Profile: { greeting: 'Hello, {firstName}!' } }
function Greeting({ firstName }: { firstName: string }) {
const t = useTranslations('Profile');
return <p>{t('greeting', { firstName })}</p>;
}Value names must match the ICU placeholders exactly; module augmentation can make missing values a compile-time error.
Handle plural branches with ICU syntaxformat-plurals
// messages: { Cart: { items: '{count, plural, =0 {Empty} =1 {One item} other {# items}}' } }
const t = useTranslations('Cart');
return <span>{t('items', { count })}</span>;Always include an `other` branch. The `#` token inserts the locale-formatted plural number.
Choose text with an ICU select messageselect-enum-label
// messages: { Order: { state: '{status, select, paid {Paid} pending {Pending} other {Unknown}}' } }
const t = useTranslations('Order');
return <span>{t('state', { status: order.status })}</span>;Select keys are exact string matches and still need `other` for unexpected or newly introduced values.
Map trusted rich-message tags to React elementsrender-rich-text
// messages: { Legal: { terms: 'Read the <link>terms</link> before continuing.' } }
const t = useTranslations('Legal');
return t.rich('terms', {
link: (chunks) => <a href="/terms">{chunks}</a>,
});The catalog names tags, but application code supplies the actual React elements and destinations; do not inject translator strings as HTML.
Format a currency valueformat-currency
import { useFormatter } from 'use-intl';
const format = useFormatter();
const price = format.number(1299.5, {
style: 'currency',
currency: 'EUR',
});Formatting uses the provider locale. Currency does not convert exchange rates; the number must already represent the requested currency.
Format a date in the provider timezoneformat-date-time
const format = useFormatter();
const label = format.dateTime(order.createdAt, {
dateStyle: 'medium',
timeStyle: 'short',
});Set IntlProvider `timeZone` for consistent server and client output, or pass an explicit timezone in the formatting options.
Keep relative time labels currentformat-relative-time
import { useFormatter, useNow } from 'use-intl';
const format = useFormatter();
const now = useNow({ updateInterval: 60_000 });
return <span>{format.relativeTime(publishedAt, now)}</span>;An update interval triggers recurring renders. Match it to the displayed precision instead of refreshing every second by habit.
Join a list according to locale rulesformat-list
const format = useFormatter();
const label = format.list(['React', 'Vue', 'Svelte'], {
type: 'conjunction',
});This relies on Intl.ListFormat in the runtime; add an appropriate polyfill where the target environment does not supply it.
Report errors and render an intentional fallbackhandle-missing-messages
<IntlProvider
locale={locale}
messages={messages}
onError={(error) => reportI18nError(error)}
getMessageFallback={({ namespace, key }) =>
namespace ? `[${namespace}.${key}]` : `[${key}]`
}
>
<App />
</IntlProvider>The default logs formatting errors and returns a key-like value. A custom policy makes production failures visible and predictable.
Enable typed message keys with module augmentationtype-message-catalog
// global.d.ts
import messages from './messages/en.json';
declare module 'use-intl' {
interface AppConfig {
Messages: typeof messages;
}
}Use one canonical catalog shape. Translation files with missing or structurally different keys still need a separate validation step.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-intl | npm | Choose it when the team prefers FormatJS's established component and imperative API ecosystem directly |
| react-i18next | npm | Choose it when an existing i18next backend, language detector, plugin, or translation workflow is the deciding factor |
| i18next | npm | Choose the framework-neutral core when translations must be shared across React, server jobs, and non-React clients |