Ship in every language.
Manage zero keys.
@aurorah/i18n hashes your source strings at the call site, renders them immediately, and the free i18n engine translates them automatically — no API key, no signup, and the engine is self-improving: a fast first answer is shown immediately and a higher-quality refined version replaces it shortly after. No key files. No extraction step. No pre-deploy catalog freeze. Full ICU MessageFormat — plurals, select, number/date skeletons, rich text — on a zero-dependency, Intl-backed engine.
Installation
One package, zero dependencies. Install it and you're translating.
npm install @aurorah/i18n@aurorah/i18n is an ES Module. Set "type": "module" in your package.json:
{ "type": "module", "dependencies": { "@aurorah/i18n": "^1.0.5" } }
Works in Node 18+, every bundler, and directly in the browser as ESM.
Initialization
Create the shared instance once and the whole app translates through it. Free to use, no cost: a bare new I18n() works out of the box — no API key or signup required.
import { I18n } from "@aurorah/i18n"; // "www.example.com" = your project's namespace // (scopes its translation tables) const i18n = new I18n({ namespace: "www.example.com" });
No extraction step, no build plugin, no catalog files to create first.
Very easy sample code
The whole API in one screen: render now with t, define once with m, switch languages, and let the UI follow.
import { I18n } from "@aurorah/i18n"; const i18n = new I18n({ namespace: "www.example.com" }); const t = i18n.t; const name = "Steve"; function render() { // render NOW, at the call site — the source string is the key: console.log(t`Hi ${name}, how are you?`); // positional -> "Hi {0}, how are you?" console.log(t`And this is ${{ name }} !!!`); // named -> "And this is {name} !!!" // define ONCE, render later — .note() gives the translator context: const hello = i18n.m`Hello ${{ name }}!`.note("Greeting on first login."); console.log(hello({ name: "Steve" })); } i18n.setLanguage("ko_kr"); // fires "language" -> first render happens here i18n.subscribe(() => render()); // re-render whenever anything changes await i18n.waitFor("exit"); // wait for the application to exit
The same app, in your framework
Every example below follows the patterns of the README — one shared I18n instance, one small binding per framework, and the views re-render automatically on language switch or when a background translation arrives.
import { I18n } from "@aurorah/i18n"; // "www.example.com" = your project's namespace (scopes its translation tables) const i18n = new I18n({ namespace: "www.example.com" }); const t = i18n.t; // `t` stays bound to the instance const name = "Steve"; i18n.setLanguage("ko_kr"); // switch to Korean await i18n.load(); // load the ko_kr cache // display translations console.log(t("menu/marketplace")); // 메뉴/마켓플레이스 console.log(t`Just text anything... Hi, ${name}. How are you?`); // 그냥 아무 텍스트나... 안녕, Steve. 잘 지내? console.log(t`Hello~ world !!! And this is ${{ name }} !!!`); // 안녕~ 세상아!!! 그리고 나는 Steve야!!!
The ten runnable example files, with their outputs, are on the Full Example Code page.
// the server half (Server Action) "use server"; import { I18n } from "@aurorah/i18n"; // the catalog namespace is enforced HERE, server-side export const I18nServer = I18n.initServer({ namespace: "my-app" });
// the CSR singleton + React binding "use client"; import { useEffect, useReducer } from "react"; import { I18n } from "@aurorah/i18n"; import { I18nServer } from "./i18n.server"; // the single client-side instance — connected to the server half export const i18n = new I18n({ ssrServer: I18nServer }); // re-render subscribed views on language switch / load / translate export function useI18n() { const [, force] = useReducer((x) => x + 1, 0); useEffect(() => i18n.subscribe(() => force()), []); return { t: i18n.t, m: i18n.m, language: i18n.getLanguage() }; }
// any client component "use client"; import { useI18n } from "@/lib/i18n"; export function Hello() { const { t } = useI18n(); return <p>{t("Hello~ world !!!")}</p>; // re-renders on draft/refined updates }
The CSR/SSR pattern out of the box — I18n.initServer() is the server half (works as a Server Action as-is) and new I18n({ ssrServer }) connects the client. The same two pieces work with SvelteKit, Nuxt, Remix, Express, and Nest.js.
// the shared singleton + Lit binding import type { ReactiveController, ReactiveControllerHost } from "lit"; import { I18n } from "@aurorah/i18n"; // "www.example.com" = your project's namespace export const i18n = new I18n({ namespace: "www.example.com" }); // ONE instance app-wide export const t = i18n.t; export const m = i18n.m; export const language = () => i18n.getLanguage(); // reactive controller — requestUpdate() on language switch / load / translate export class I18nController implements ReactiveController { private unsub?: () => void; constructor(private host: ReactiveControllerHost) { host.addController(this); } hostConnected() { this.unsub = i18n.subscribe(() => this.host.requestUpdate()); } hostDisconnected() { this.unsub?.(); } }
// any Lit element import { LitElement, html } from "lit"; import { I18nController, t } from "../lib/i18n"; export class HelloView extends LitElement { private i18nCtl = new I18nController(this); // subscribe this element render() { return html`<p>${t("Hello~ world !!!")}</p>`; // re-renders on draft/refined updates } }
A bare new I18n() works out of the box (free engine, no API key) — the ReactiveController re-renders elements on language switch and draft/refined updates.
// the server half (server-only module in SvelteKit) import { I18n } from "@aurorah/i18n"; // the catalog namespace is enforced HERE, server-side export const I18nServer = I18n.initServer({ namespace: "my-app" });
// the CSR singleton + Svelte store binding import { readable } from "svelte/store"; import { I18n } from "@aurorah/i18n"; import { I18nServer } from "./i18n.server"; // the single client-side instance — connected to the server half const i18n = new I18n({ ssrServer: I18nServer }); // t / m / language stores re-emit on language switch / load / translate export const t = readable(i18n.t, (set) => i18n.subscribe(() => set(i18n.t))); export const m = readable(i18n.m, (set) => i18n.subscribe(() => set(i18n.m))); export const language = readable(i18n.getLanguage(), (set) => i18n.subscribe(() => set(i18n.getLanguage())));
<!-- any Svelte component --> <script lang="ts"> import { t } from "$lib/i18n"; </script> <p>{$t("Hello~ world !!!")}</p> <!-- re-renders on draft/refined updates -->
The same two CSR/SSR pieces as Next.js — I18n.initServer() plus new I18n({ ssrServer }) — with Svelte stores as the re-render binding.
// the shared singleton import { I18n } from "@aurorah/i18n"; // "www.example.com" = your project's namespace export const i18n = new I18n({ namespace: "www.example.com" }); // ONE instance app-wide export const t = i18n.t; export const m = i18n.m; export const language = () => i18n.getLanguage();
// any view import { i18n, t } from "./lib/i18n"; const el = document.querySelector("#hello")!; function retranslate() { el.textContent = t("Hello~ world !!!"); // re-renders on draft/refined updates } retranslate(); // re-run when the language switches or a background translation arrives; // call the returned unsubscribe in the view's destroy(): const unsubscribe = i18n.subscribe(retranslate);
No framework needed — subscribe() re-runs the render function when the language switches or a draft/refined translation arrives.
Three things no catalog-based library can do
Every library in the matrix below assumes translations exist before you deploy. @aurorah/i18n inverts that assumption.
Zero key management
The source string is the key. A stable hash is derived at the call site — no JSON trees, no ID naming debates, no orphaned keys after refactors. Rename-safe by construction.
Draft → refine → pin translation
The i18n engine is self-improving: a fast first answer is shown immediately, a higher-quality refined version replaces it automatically shortly after, and importStrings() pins human-reviewed text that always wins — never overwritten automatically.
Graceful missing strings
A string the system has never seen renders in the source language instantly, translates in the background, and re-renders via subscription. Shipping a feature never waits on a translation cycle.
Capability matrix
Graded against each library's official documentation and core source.
| Capability | @aurorah/i18n | i18next | react-intl (FormatJS) | Lingui | next-intl | Paraglide JS |
|---|---|---|---|---|---|---|
| Message keys | Auto hashId of source text — no key management | Manual key trees (JSON) | Manual or extracted IDs | Source text + generated IDs (macros) | Manual key trees | Manual keys, compiled to functions |
| String extraction | Runtime, at first render of each call site | None built-in (3rd-party parsers) | CLI extractor (@formatjs/cli) | CLI + Babel/SWC macros | None built-in | Compile step (inlang project) |
| Translation production | Self-improving i18n engine: fast draft at runtime, refined version replaces it automatically | Pre-translated catalogs + TMS integrations | Pre-translated catalogs + TMS | PO/JSON catalogs + TMS | Pre-translated JSON | Inlang files; machine translate at build |
| Interpolation | Full ICU MessageFormat — {0} positional, {name} named, nested arguments | {{name}} + format functions | Full ICU MessageFormat | Full ICU via macros | ICU MessageFormat | Named params, fully typed |
| Pluralization | ICU plural / selectordinal via Intl.PluralRules — offset:N, decimal/negative =N matches | CLDR plural key suffixes | ICU plural / selectordinal | ICU plural macro | ICU plural | Match variants (CLDR) |
| Gender / select | ICU select — full grammar | Context suffixes | ICU select | ICU select | ICU select | Match variants |
| Date / number formatting | Number/date skeletons + DecimalFormat patterns via Intl, zero deps | Formatter hooks / i18next-icu | Intl wrappers (FormattedNumber, …) | i18n.date() / i18n.number() | useFormatter (Intl) | None (use Intl manually) |
| Rich text / components in messages | t.rich / m.rich — <tag> chunks + handlers | <Trans> (react-i18next) | Rich-text tag chunks | <Trans> macro | t.rich() tags | Not supported |
| Translator context | .note() chained at the call site | None built-in | description on descriptor | Extracted code comments | None built-in | Inlang metadata |
| Missing-string behavior | Render source now, draft-translate in background, refine upgrades drafts, notify subscribers | Static fallback-language chain | defaultMessage fallback | Source-text fallback | Fallback + onError hook | Source-locale fallback |
| Review / pin workflow | exportStrings() out, importStrings() back — pinned human-reviewed text always wins | Catalog files are the review surface (TMS) | Catalog files (TMS) | PO catalogs (TMS) | Catalog files | Inlang editor / files |
| Catalog loading / caching | Single-language client MemCache; other tables evicted on switch | Namespaces, lazy backends (http, fs, …) | Load per provider / page | Catalog splitting, dynamic import | Per-request messages (RSC-aware) | Per-message tree shaking, near-zero runtime |
| Language switching | setLanguage() + subscribe() re-render hook | changeLanguage() + events | Swap provider locale | i18n.activate() | Locale routing (middleware) | setLocale() / reactive adapters |
| Framework bindings | React, Lit, Svelte, vanilla — patterns in the demos; packages in progress | react-, vue-, next-i18next, huge plugin ecosystem | React-first | React, JS core usable anywhere | Next.js only (App Router native) | Adapters: Next, SvelteKit, Vite, Astro |
| Type safety of messages | Untyped tagged templates at runtime — typed codegen on the roadmap | Via codegen add-ons | Typed descriptors via extraction | Typed macros | Typed keys via augmentation | Fully typed message functions |
| Runtime footprint | Zero deps incl. the full ICU engine — Intl-backed, never-crash fallback | ~14 kB core + plugins | ~17 kB + Intl polyfills where needed | ~8 kB runtime | ~14 kB | Near-zero (compiled output only) |
| Maturity / ecosystem | Newest entrant — test suite + four framework demos, in production at Aurorah | Most mature, largest ecosystem | Mature, standards-driven | Mature | Mature (Next.js standard) | Newer, fast-growing (inlang) |
Sources: @aurorah/i18n README, CHANGELOG, and core source; official documentation of each library. Found an error? Open an issue — we correct this table.
Full ICU MessageFormat, zero dependencies
The built-in engine parses the full ICU MessageFormat grammar and renders through the platform Intl APIs — no polyfills, no locale data bundles. Malformed input never crashes a render; it falls back to the flat source text.
Pluralization
CLDR plural categories resolved per active locale via Intl.PluralRules. Korean and Japanese correctly collapse to the "other" category; Slavic and Arabic many-form rules resolve without developer effort. Exact matches accept decimals and negatives.
{count, plural, =0 {no replies} one {# reply} other {# replies}}
// ko → "새 답글이 3개 있습니다."
// ru → few/many forms resolved via CLDRGender / select / ordinal
Select variants and ordinal categories with standard ICU syntax, so descriptors migrated from react-intl or Lingui keep working. Ordinals render "3rd" in English and locale digits elsewhere.
{gender, select, male {He} female {She} other {They}} replied.
{rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}Number skeletons + patterns
ICU number skeletons (currency, compact, scientific, percent scale, units, precision, sign and grouping controls) and classic DecimalFormat patterns, compiled once per style to Intl.NumberFormat options.
{price, number, ::currency/USD} // $1,234.56
{views, number, ::compact-short} // 1.2M
{share, number, ::percent scale/100 .0} // 42.0%Date / time skeletons
Named styles (short / medium / long / full) and CLDR skeletons mapped to Intl.DateTimeFormat components — the active locale picks order and punctuation, so "Jul 23, 2026" becomes "2026년 7월 23일" for free.
{when, date, ::yMMMdEHm} // Wed, Jul 23, 2026, 16:30
{when, time, short} // 4:30 PMRich text in messages
Component and tag chunks inside translated strings, so links, emphasis, and inline components survive translation with word order intact — critical for KO/JA, where sentence structure reorders around the markup.
t.rich`Read the <link>full changelog</link> before upgrading.`
// → ["Read the ", link("full changelog"), " before upgrading."]Review / pin workflow
Dump the catalog for translators, review the i18n engine's output, and pin the approved text back in. Pinned translations always win over engine output and load()ed tables — production strings stay under human control.
const catalog = i18n.exportStrings(); // { hashId: { text, note? } }
i18n.importStrings("ko_kr", reviewed); // pinned — always winsMigrating from a catalog-based library
Because the source string is the key, migration is mostly deletion: your existing catalogs seed the translation cache, then stop being your problem.
From i18next / next-intl
- Import existing JSON catalogs — each entry seeds the cache under its source-string hash
- Replace
t('dashboard.greeting', { name })witht`Welcome back, ${name}` - Delete the key files; extraction and namespaces are no longer needed
From react-intl / Lingui
- ICU plural, select, number, and date syntax is compatible — existing descriptors carry over
defaultMessagebecomes the source string itself;descriptionmaps to.note()- Rich-text tag chunks map one-to-one to
t.rich/m.richhandlers