Skip to content

yankouskia/localize-react

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

68 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

localize-react

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

npm downloads CI coverage bundle types license


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-USen_usen

📚 Recipes

Real-world patterns, fully documented on the site:

🥊 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-react to 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

About

✈️ Lightweight React Localization Library 🇺🇸

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors