rvlib-i18n
v1.0.0
Published
Type-safe i18n the simple way: the EN dict is the source of truth, every locale typechecks against it (missing keys and wrong interpolation args are compile errors), one mobx-observable singleton holds the persisted locale. No framework, no ICU, no catalo
Readme
rvlib-i18n
Type-safe i18n the simple way. No framework, no ICU syntax, no JSON catalogs, no key-string parsing: TypeScript is the engine, mobx makes the locale switch render.
The idea: your primary dict is the source of truth, and every other locale must typecheck against it. A missing key, an extra key, or a wrong interpolation argument is a compile error, not a runtime surprise.
// src/i18n/en.ts — the source of truth
export const en = {
ui: { save: "Save" },
err: { tooShort: ({ min }: { min: number }): string => `Must be at least ${min} characters` },
}
// src/i18n/fr.ts — parity enforced by the compiler
import type { en } from "./en"
export const fr: typeof en = {
ui: { save: "Enregistrer" },
err: { tooShort: ({ min }) => `Au moins ${min} caractères` },
}
// src/i18n/index.ts — the app's ONE singleton
import { createI18n } from "rvlib-i18n"
import { en } from "./en"
import { fr } from "./fr"
export const i18n = createI18n({ dicts: { en, fr }, fallback: "en" })Usage anywhere:
i18n.t.ui.save // typed, autocompleted
i18n.t.err.tooShort({ min: 8 }) // interpolation args typechecked
i18n.set("fr") // persists to localStorage, observers re-render
i18n.lang // current locale, observable
i18n.toggle() // cycle locales (a settings row, a debug key)Every mobx observer component that reads i18n.t.* re-renders on a locale switch. Non-mobx render loops (canvas, pixi) can poll a signature string that includes i18n.lang.
Pluralization, articles, casing, and per-locale formatting live INSIDE the dict as plain typed functions. The function absorbs whatever the language needs, including different plural boundaries per language:
// EN takes the plural at 0, FR keeps the singular noun at 0 and 1
tooFewItems: ({ min }: { min: number }) => `Needs at least ${min === 1 ? "1 item" : `${min} items`}`
tooFewItems: ({ min }) => `Doit contenir au moins ${min < 2 ? `${min} élément` : `${min} éléments`}`Peer dependency: mobx ^6. localStorage access is try/catch'd, so tests, SSR and private mode degrade to session-only.
