Tiny, type-safe React i18n built on Context and hooks.
~1.1 kB brotli · 0 runtime deps · dual ESM + CJS · React 19 ready.
Docs · Quickstart · API · Recipes · Migration v1 → v2
import { LocalizationProvider, useLocalize } from 'localize-react';
const translations = {
en: { hello: 'Hi {{name}}!' },
es: { hello: '¡Hola {{name}}!' },
ja: { hello: '{{name}}さん、こんにちは!' },
};
function Greeting() {
const { translate } = useLocalize();
return <h1>{translate('hello', { name: 'Alex' })}</h1>;
}
export default function App() {
return (
<LocalizationProvider locale="en" translations={translations}>
<Greeting />
</LocalizationProvider>
);
}That's the whole API. Three exports, no plugins, no extraction toolchain. Ship it.
Most React i18n libraries are 30–80 kB and bring opinions about plural rules, ICU MessageFormat, async loading, and TMS workflows. localize-react is the smallest thing that works.
It does exactly what a frontend most often needs:
- A nested translations tree, keyed by locale.
- Dot-path lookups (
'cart.summary'). {{name}}-style interpolation.- A graceful fallback when keys are missing.
For everything else (plurals, currency, dates) — reach for the platform: Intl.PluralRules, Intl.NumberFormat, Intl.DateTimeFormat. Free, fast, already in the browser. See the Intl formatters recipe.
| Property | Value |
|---|---|
| Bundle (brotli) | 1.13 kB ESM · 1.22 kB CJS |
| Runtime dependencies | 0 |
| Source | Strict TypeScript 6 |
| Module formats | ESM + CJS with proper exports/types conditions |
| Tree-shaking | sideEffects: false |
| Peer range | React >= 16.8 < 20 (tested in CI through React 19) |
| Node | >= 20.19 (CI on 20, 22, 24 × Linux/macOS/Windows) |
| Test coverage | 100 % statements · 100 % functions · 98 % branches |
| Type-checked exports | Validated by publint + @arethetypeswrong/cli in CI |
npm install localize-react
# or: pnpm add localize-react · yarn add localize-react · bun add localize-reactexport const translations = {
en: {
greeting: { hello: 'Hi {{name}}!' },
cart: { summary: '{{count}} items, {{total}} total' },
},
es: {
greeting: { hello: '¡Hola {{name}}!' },
cart: { summary: '{{count}} artículos, {{total}} total' },
},
} as const;import { LocalizationProvider } from 'localize-react';
import { translations } from './i18n/translations';
export function App() {
return (
<LocalizationProvider locale="en" translations={translations}>
<Shell />
</LocalizationProvider>
);
}import { Message, useLocalize } from 'localize-react';
// Hook
function Cart() {
const { translate } = useLocalize();
return <p>{translate('cart.summary', { count: 3, total: '$42.00' })}</p>;
}
// Component
function CartHeader() {
return (
<h1>
<Message descriptor="greeting.hello" values={{ name: 'Alex' }} />
</h1>
);
}That's the whole story. Full docs at yankouskia.github.io/localize-react.
Want the compiler to catch missing keys, typo'd descriptors, and forgotten {{tokens}}? Wrap your translations once and use the typed bindings:
import { createLocalization } from 'localize-react';
const translations = {
en: { greeting: 'Hi {{name}}!', cart: { checkout: 'Checkout' } },
es: { greeting: '¡Hola {{name}}!', cart: { checkout: 'Pagar' } },
} as const;
export const { LocalizationProvider, useLocalize, Message } =
createLocalization(translations);
// inside a component — `translate` autocompletes descriptors and requires {{name}}:
function Greeting() {
const { translate } = useLocalize();
return <h1>{translate('greeting', { name: 'Alex' })}</h1>; // ✅
// translate('greting'); ❌ Type '"greting"' is not assignable
// translate('greeting'); ❌ Property 'name' is missing
}A pure compile-time wrapper — no runtime cost, no second cache. Full guide: Type-safe API.
Need a link or component inside a translated sentence? <RichMessage /> returns a ReactNode and lets values include React nodes, so a single translation can host inline rich content without splitting the sentence across JSX:
import { RichMessage } from 'localize-react';
// en: 'By signing up, you agree to our {{link}}.'
// de: 'Mit der Anmeldung stimmen Sie unseren {{link}} zu.'
function Footer() {
return (
<p>
<RichMessage
descriptor="signup.terms"
values={{ link: <a href="/terms">Terms of Service</a> }}
/>
</p>
);
}The link lands wherever the translator put {{link}} — German word order, Japanese particles, RTL flow, all handled by the translation. Strings and numbers are still accepted (and coerced) so you can mix them freely. Available as a typed export from createLocalization() too. Full recipe: Rich content.
| Operation | API |
|---|---|
| Mount translations | <LocalizationProvider locale translations> |
| Translate (hook) | useLocalize().translate(descriptor, values?, default?) |
| Translate (component) | <Message descriptor values? defaultMessage? /> |
| Translate with JSX | <RichMessage descriptor values? defaultMessage? /> |
| Switch locale at runtime | Re-render with a new locale prop |
| Missing key | Renders defaultMessage ?? descriptor (never throws) |
| Nested lookup | translate('a.b.c') walks the tree |
| Interpolation | {{token}} — literal replacement, safe with regex chars |
| Locale normalization | En-US → en_us → en |
Real-world patterns, fully documented on the site:
- Switching locales (URL / cookie / localStorage)
- Lazy-loading translation chunks with
React.use() - Next.js (App Router) —
[locale]segments + middleware - Vite + React Router —
import.meta.glob - Testing — RTL render helper, descriptor coverage
- Intl formatters — plurals, currency, dates, lists
| localize-react | react-i18next | react-intl | lingui | |
|---|---|---|---|---|
| Bundle (brotli) | ~1.1 kB | ~17 kB | ~38 kB | ~9 kB |
| Runtime deps | 0 | several | several | one macro |
| Pluralization (CLDR) | Use Intl |
✅ | ✅ (ICU) | ✅ (ICU) |
| Number / date format | Use Intl |
Optional | ✅ | ✅ |
| ICU MessageFormat | ❌ | ✅ | ✅ | ✅ |
| Lazy locale loading | DIY | ✅ | ✅ | ✅ |
| Auto extraction | ❌ | ✅ | CLI | CLI |
| TypeScript-first | ✅ | ✅ | ✅ | ✅ |
| Learning curve | Tiny | Medium | Medium | Medium |
Use localize-react when you want a hook + a tag. Reach for the others when CLDR plurals, ICU MessageFormat, or a TMS workflow matter — they're all great at what they do.
- Types ship inside the package — no
@types/localize-reactto chase. - Provenance attestation on every published version (npm OIDC trusted publishing).
- CodeQL runs on every PR; CI matrix exercises Node 20/22/24 × Linux/macOS/Windows.
- Size budget enforced — < 2 kB ESM, < 2.5 kB CJS, checked on every PR with
size-limit. - No dynamic require, no eval, no regex from user input — interpolation is literal
replaceAll.
The runtime API is unchanged. v2 modernizes the toolchain (strict TS 6, dual ESM+CJS, React 19 peer, GitHub Actions + Changesets). One soft TypeScript regression in exactOptionalPropertyTypes mode — see the migration guide.
PRs welcome. See CONTRIBUTING.md for the setup + release flow. Security reports: please open a private security advisory rather than a public issue.
If you'd like to support the project, sponsoring helps a lot.
MIT © Aliaksandr Yankouski