i18n without catalogs

Ship in every language.
Manage zero keys.

app/hello.tsx
// one call site — every language const msg = m`Welcome back, ${{ name }}. You have ${{ count }} new replies.` .note('Dashboard greeting, casual register')
Rendered en
Welcome back, Steve. You have 3 new replies.
auto-hashed from source · translated at runtime

@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.

terminal
npm install @aurorah/i18n

@aurorah/i18n is an ES Module. Set "type": "module" in your package.json:

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.

src/lib/i18n.ts
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.

app/greeting.ts
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.

examples/easy-start
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야!!!
Full Example Code

The ten runnable example files, with their outputs, are on the Full Example Code page.

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.

01 · keys

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.

02 · production

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.

03 · resilience

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.

distinctive strength / supported partial missing
Capability@aurorah/i18ni18nextreact-intl (FormatJS)Linguinext-intlParaglide JS
Message keysAuto hashId of source text — no key managementManual key trees (JSON)Manual or extracted IDsSource text + generated IDs (macros)Manual key treesManual keys, compiled to functions
String extractionRuntime, at first render of each call siteNone built-in (3rd-party parsers)CLI extractor (@formatjs/cli)CLI + Babel/SWC macrosNone built-inCompile step (inlang project)
Translation productionSelf-improving i18n engine: fast draft at runtime, refined version replaces it automaticallyPre-translated catalogs + TMS integrationsPre-translated catalogs + TMSPO/JSON catalogs + TMSPre-translated JSONInlang files; machine translate at build
InterpolationFull ICU MessageFormat — {0} positional, {name} named, nested arguments{{name}} + format functionsFull ICU MessageFormatFull ICU via macrosICU MessageFormatNamed params, fully typed
PluralizationICU plural / selectordinal via Intl.PluralRules — offset:N, decimal/negative =N matchesCLDR plural key suffixesICU plural / selectordinalICU plural macroICU pluralMatch variants (CLDR)
Gender / selectICU select — full grammarContext suffixesICU selectICU selectICU selectMatch variants
Date / number formattingNumber/date skeletons + DecimalFormat patterns via Intl, zero depsFormatter hooks / i18next-icuIntl wrappers (FormattedNumber, …)i18n.date() / i18n.number()useFormatter (Intl)None (use Intl manually)
Rich text / components in messagest.rich / m.rich — <tag> chunks + handlers<Trans> (react-i18next)Rich-text tag chunks<Trans> macrot.rich() tagsNot supported
Translator context.note() chained at the call siteNone built-indescription on descriptorExtracted code commentsNone built-inInlang metadata
Missing-string behaviorRender source now, draft-translate in background, refine upgrades drafts, notify subscribersStatic fallback-language chaindefaultMessage fallbackSource-text fallbackFallback + onError hookSource-locale fallback
Review / pin workflowexportStrings() out, importStrings() back — pinned human-reviewed text always winsCatalog files are the review surface (TMS)Catalog files (TMS)PO catalogs (TMS)Catalog filesInlang editor / files
Catalog loading / cachingSingle-language client MemCache; other tables evicted on switchNamespaces, lazy backends (http, fs, …)Load per provider / pageCatalog splitting, dynamic importPer-request messages (RSC-aware)Per-message tree shaking, near-zero runtime
Language switchingsetLanguage() + subscribe() re-render hookchangeLanguage() + eventsSwap provider localei18n.activate()Locale routing (middleware)setLocale() / reactive adapters
Framework bindingsReact, Lit, Svelte, vanilla — patterns in the demos; packages in progressreact-, vue-, next-i18next, huge plugin ecosystemReact-firstReact, JS core usable anywhereNext.js only (App Router native)Adapters: Next, SvelteKit, Vite, Astro
Type safety of messagesUntyped tagged templates at runtime — typed codegen on the roadmapVia codegen add-onsTyped descriptors via extractionTyped macrosTyped keys via augmentationFully typed message functions
Runtime footprintZero 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 kBNear-zero (compiled output only)
Maturity / ecosystemNewest entrant — test suite + four framework demos, in production at AurorahMost mature, largest ecosystemMature, standards-drivenMatureMature (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

{n, plural, offset:N =0 =1.5 one other}

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 CLDR

Gender / select / ordinal

{g, select, …} · {n, selectordinal, …}

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

{n, number, ::currency/USD} · {n, number, #,##0.00}

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

{d, date, ::yMMMd} · {d, time, short}

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 PM

Rich text in messages

t.rich`…<link>…</link>…`

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

exportStrings() → review → importStrings()

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 wins

Migrating 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 }) with t`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
  • defaultMessage becomes the source string itself; description maps to .note()
  • Rich-text tag chunks map one-to-one to t.rich / m.rich handlers