Tiny, type-safe React i18n built on Context and hooks.
< 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) | 916 B ESM Β· 989 B 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.
| Operation | API |
|---|---|
| Mount translations | <LocalizationProvider locale translations> |
| Translate (hook) | useLocalize().translate(descriptor, values?, default?) |
| Translate (component) | <Message 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 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