localize-react
v2.2.0
Published
Tiny, type-safe React i18n library built on Context and hooks — dual ESM + CJS, zero runtime deps.
Downloads
1,045
Maintainers
Readme
localize-react
React i18n, without the weight.
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.
✨ Why?
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.
📦 Specs
| 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 |
🚀 Install
npm install localize-react
# or: pnpm add localize-react · yarn add localize-react · bun add localize-react⚡️ Quickstart
1. Define translations
export 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;2. Mount the provider
import { LocalizationProvider } from 'localize-react';
import { translations } from './i18n/translations';
export function App() {
return (
<LocalizationProvider locale="en" translations={translations}>
<Shell />
</LocalizationProvider>
);
}3. Translate, two ways
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.
🛡 Fully type-safe with createLocalization()
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.
🎨 Rich content with <RichMessage />
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.
🧠 Concept in one screen
| 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 |
📚 Recipes
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
🥊 How it compares
| | 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.
🛡 Production-ready
- 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.
📖 v1 → v2
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.
🤝 Contributing
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.
📄 License
MIT © Aliaksandr Yankouski
