npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aurorah/i18n

v1.0.6

Published

Zero-config i18n for JavaScript/TypeScript with automatic translation. Works with Next.js, SvelteKit, and any Node or browser app. ICU MessageFormat compatible. Write strings in any language, render with i18n.t`...`. No message IDs, no catalogs, no API ke

Readme

@aurorah/i18n

Free i18n engine — a single I18n class: write strings in your source language and render them with i18n.t...``` / i18n.m....note()``; the @aurorah/i18nengine translates them automatically. Free to use, no cost: a barenew I18n()` works out of the box, no API key or signup required.

  • Free — new I18n() with no parameters works immediately with the @aurorah/i18n engine; standalone (offline tables only) mode works fully offline.
  • No ID files — the text itself is the key. If a language has no translation yet, the original text is shown, so nothing ever breaks.
  • Every language — codes are {language}_{country} (ISO 639-1 + ISO 3166-1), e.g. en_us, ko_kr, ja_jp.
  • Self-improving — a fast first answer is shown immediately and a higher-quality refined version replaces it automatically shortly after.

Home page: https://aurorah.ai/i18n

Install

$ npm install @aurorah/i18n

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

{
  "type": "module",
  "dependencies": {
    "@aurorah/i18n": "^1.0.6"
  }
}

Easy start — t, m, and .note()

For everyday use you only need three things: t for texts, m to define a message once, and .note() to leave a comment for translators. Everything else (plurals, ordinals, number/date formats) is completed automatically by the i18n engine.

1. Very easy way to use @aurorah/i18n

]

Wrap your text with t. The text itself is the key — there are no ID files to maintain. If a language has no translation yet, the original text is shown, so nothing ever breaks.

t renders synchronously from the in-memory cache. On a miss it shows the source text immediately and fetches the translation in the background — for one-shot scripts, await i18n.waitFor("translate") after the t() calls; on a later run, await i18n.load() downloads that language's whole cache in ONE request.

$ node exam-helloworld-1.js
//
// exam-helloworld-1.js
//
import { I18n } from "@aurorah/i18n";

// "www.example.com" = your project's namespace (scopes its translation tables)
// "ko_kr" = {language}_{country} (ISO 639-1 + ISO 3166-1)
const i18n = new I18n({ namespace: "www.example.com", language: "ko_kr" });
const t = i18n.t; // `t` stays bound to the instance

// display text without translation
console.log(t("Hello~ world !!!")); // Hello~ world !!!
console.log(t("menu/marketplace")); // menu/marketplace

// wait for translations to be loaded
await i18n.waitFor("translate");

// display translations
console.log(t("Hello~ world !!!")); // 안녕~ 세상아 !!!
console.log(t("menu/marketplace")); // 메뉴/마켓플레이스

On a later run, await i18n.load() loads the translation cache filled by the previous translate — no waitFor needed:

$ node exam-helloworld-2.js
//
// exam-helloworld-2.js
//
import { I18n } from "@aurorah/i18n";

// "www.example.com" = your project's namespace (scopes its translation tables)
// "ko_kr" = {language}_{country} (ISO 639-1 + ISO 3166-1)
const i18n = new I18n({ namespace: "www.example.com", language: "ko_kr" });
const t = i18n.t; // `t` stays bound to the instance

// load translation cache (filled by a previous translate run)
await i18n.load();

// display translations
console.log(t("Hello~ world !!!")); // 안녕~ 세상아 !!!
console.log(t("menu/marketplace")); // 메뉴/마켓플레이스

For a long-lived UI (or console loop), i18n.subscribe() re-renders when a translation arrives — including the refined version later:

$ node exam-subscribe.js
//
// exam-subscribe.js
//
import { I18n } from "@aurorah/i18n";

let iRenderCount = 0;

// "www.example.com" = your project's namespace (scopes its translation tables)
// "ko_kr" = {language}_{country} (ISO 639-1 + ISO 3166-1)
const i18n = new I18n({ namespace: "www.example.com", language: "ko_kr" });
const t = i18n.t; // `t` stays bound to the instance

// load translation cache (filled by a previous translate run)
await i18n.load();

// display translations
function displayTexts() {
  iRenderCount++;
  console.log("\n\nRender count:", iRenderCount);
  console.log(
    t("Hello~ everyone !!! This is Steve, developer of @aurorah/i18n."),
  );
  console.log(
    t("It's very easy to use for software localization with @aurorah/i18n."),
  );
}

// subscribe to i18n events
i18n.subscribe((e) => {
  console.log(
    `\n\ni18n-event: ${e.reason}${e.phase ? ` - ${e.phase}` : ""} [${e.language}]`,
  );
  // Render texts when translations are loaded
  if (e.reason === "translate") {
    displayTexts();
  }
});

// display texts immediately:
// - first run shows source in English (because the cache is cold)
// - next runs show Korean (because the cache is warm from the previous run)
displayTexts();

await i18n.waitFor("exit"); // wait for the application to exit

Example output for the first run — 1) source text now, 2) fast translation, 3) refined translation:

Render count: 1
Hello~ everyone !!! This is Steve, developer of @aurorah/i18n.
It's very easy to use for software localization with @aurorah/i18n.


i18n-event: translate - draft [ko_kr]
Render count: 2
안녕하세요~ 여러분!!! 저는 @aurorah/i18n 개발자 스티브입니다.
@aurorah/i18n을 사용하면 소프트웨어 현지화에 매우 쉽게 사용할 수 있습니다.


i18n-event: translate - refined [ko_kr]
Render count: 3
안녕하세요~ 여러분!!! 저는 @aurorah/i18n 개발자 스티브입니다.
@aurorah/i18n을 사용하면 소프트웨어 현지화가 매우 쉬워집니다.

Example output for the second run — the cache is warm, so the translation is shown immediately:

Render count: 1
안녕하세요~ 여러분!!! 저는 @aurorah/i18n 개발자 스티브입니다.
@aurorah/i18n을 사용하면 소프트웨어 현지화가 매우 쉬워집니다.

Without load(), a t() call whose translation is not in memory yet renders the SOURCE text immediately and fetches the translation in the background — subscribers (i18n.subscribe()) are notified when it arrives, so long-lived UIs can re-render. For one-shot scripts, await i18n.waitFor("translate") after the t() calls (or await i18n.load() on a later run once the cache is warm). Console demos that should keep running use await i18n.waitFor("exit").

2. Show a text in the user's language

The source texts can be written in ANY language — here the app is written in Japanese, and an English user sees English. The same t call, no other change.

$ node exam-source-language-1.js
//
// exam-source-language-1.js
//
import { I18n } from "@aurorah/i18n";

// "www.example.com" = your project's namespace (scopes its translation tables)
// "ja_jp" = {language}_{country} (ISO 639-1 + ISO 3166-1)
const i18n = new I18n({
  namespace: "www.example.com",
  sourceLanguage: "ja_jp",
  language: "ja_jp",
});
const t = i18n.t; // `t` stays bound to the instance

console.log(t("こんにちは〜 世界 !!!")); // こんにちは〜 世界 !!! (the source — no load() needed)

i18n.setLanguage("en_us"); // switch to English
await i18n.load(); // load the en_us cache

console.log(t("こんにちは〜 世界 !!!")); // Hello~ world !!! (same call, now English)

Output:

# Example output for the first run:

$ node exam-source-language-1.js
こんにちは〜 世界 !!!
[@aurorah/i18n] no en_us translation for "こんにちは〜 世界 !!!" (hashId 17bfirr1aam9kv) in the loaded table
こんにちは〜 世界 !!!

# Example output for the second run:

$ node exam-source-language-1.js
こんにちは〜 世界 !!!
Hello World !!!

Why twice? On the first run the en_us cache is empty for that Japanese source string — load() finds nothing, so the second t() still prints the source. The string is translated in the background and stored on the i18n engine; the second run's load() then returns English immediately. To get English on the first run without waiting for a second process, subscribe and re-render when "load" / "translate" arrive (next example).

The script above is the one-shot style: await i18n.load() after setLanguage(), then call t() again. For a long-lived UI, subscribe instead — setLanguage() already kicks a background load(), and "load" / "translate" notify you when the new language is ready to re-render.

Same example with subscribe, so English appears on the first run:

$ node exam-source-language-2.js
//
// exam-source-language-2.js
//
import { I18n } from "@aurorah/i18n";

// "www.example.com" = your project's namespace (scopes its translation tables)
// "ja_jp" = {language}_{country} (ISO 639-1 + ISO 3166-1)
const i18n = new I18n({
  namespace: "www.example.com",
  sourceLanguage: "ja_jp",
  language: "ja_jp",
});
const t = i18n.t; // `t` stays bound to the instance

function renderTexts() {
  console.log(t("こんにちは〜 世界 !!!"));
}

// subscribe to i18n events
i18n.subscribe((e) => {
  console.log(
    `\n\ni18n-event: ${e.reason}${e.phase ? ` - ${e.phase}` : ""} [${e.language}]`,
  );
  if (e.reason === "load" || e.reason === "translate") {
    // after i18n.setLanguage("en_us") : Hello~ world !!! (same call, now English)
    renderTexts();
  }
});

renderTexts(); // before i18n.setLanguage("en_us") : こんにちは〜 世界 !!! (the source — no load() needed)

i18n.setLanguage("en_us"); // switch to English

await i18n.waitFor("exit"); // wait for the application to exit

Output:

$ node exam-source-language-2.js
こんにちは〜 世界 !!!


i18n-event: language [en_us]


i18n-event: load [en_us]
Hello~ World !!!

3. Switch the language

One call switches the whole app. setLanguage() plus await load() pulls that language's translations once; after that, every t() call renders instantly. Any locale works — codes are {language}_{country}, e.g. en_us, ko_kr, ja_jp, zh_cn, es_es, fr_fr, de_de, …

$ node exam-switch-language.js
//
// exam-switch-language.js
//
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

// default language is 'en_us'
console.log(t("Hello~ world !!!")); // Hello~ world !!! (the source — no load() needed)

i18n.setLanguage("ko_kr"); // switch to Korean
await i18n.load(); // load the ko_kr cache
console.log(t("Hello~ world !!!")); // 안녕~ 세상아 !!! (same call, now Korean)

i18n.setLanguage("ja_jp"); // Japanese
await i18n.load();
console.log(t("Hello~ world !!!")); // こんにちは〜 世界 !!!

i18n.setLanguage("zh_cn"); // Chinese
await i18n.load();
console.log(t("Hello~ world !!!")); // 你好〜 世界 !!!

i18n.setLanguage("es_es"); // Spanish
await i18n.load();
console.log(t("Hello~ world !!!")); // ¡Hola~ mundo !!!

await i18n.waitFor("translate"); // wait for the translations to be loaded

// next run will be warm cache

Output:

$ node exam-switch-language.js
Hello~ world !!!
[@aurorah/i18n] no ko_kr translation for "Hello~ world !!!" (hashId 1b917x6mio0ie) in the loaded table
Hello~ world !!!
[@aurorah/i18n] no ja_jp translation for "Hello~ world !!!" (hashId 1b917x6mio0ie) in the loaded table
Hello~ world !!!
[@aurorah/i18n] no zh_cn translation for "Hello~ world !!!" (hashId 1b917x6mio0ie) in the loaded table
Hello~ world !!!
[@aurorah/i18n] no es_es translation for "Hello~ world !!!" (hashId 1b917x6mio0ie) in the loaded table
Hello~ world !!!

$ node exam-switch-language.js
Hello~ world !!!
안녕~ 세상아!!!
こんにちは、世界!!!
你好~世界!!!
Hola~ mundo !!!

4. Put values into your text — Positional Placeholder

Drop a value into the sentence with ${...}. It becomes a positional placeholder ({0}, {1}, ...) — each language's translation can move it to wherever its word order needs it.

$ node exam-positional-placeholder-1.js
//
// exam-positional-placeholder-1.js
//
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";
const age = 20;

i18n.setLanguage("en_us");
console.log(t`${name} is ${age} years old !!!`); // Steve is 20 years old !!!

i18n.setLanguage("ko_kr");
await i18n.load(); // load the ko_kr cache

console.log(t`${name} is ${age} years old !!!`); // Steve는 20살입니다 !!!

await i18n.waitFor("translate"); // wait for the translations to be loaded

// next run will be warm cache

Output:

# 1st run — no translation yet

$ node exam-positional-placeholder-1.js
Steve is 20 years old !!!
[@aurorah/i18n] no ko_kr translation for "{0} is {1} years old !!!" (hashId 4s5hfondoi0e) in the loaded table
Steve is 20 years old !!!

# 2nd run — draft (fast first answer; often still English)

$ node exam-positional-placeholder-1.js
Steve is 20 years old !!!
Steve is 20 years old !!!

# Let the process finish before the next run — the i18n engine may still be refining.

# 3rd run — refined

$ node exam-positional-placeholder-1.js
Steve is 20 years old !!!
Steve는 20살입니다 !!!

Why those three runs look different — placeholder strings use the i18n engine’s long-lived translation, so finishing takes longer:

  • 1st run (no translation) — cold cache. load() has no ko_kr entry (the warning), so t() prints the text as written. waitFor("translate") runs after that log, so this process only waits for the fast first answer and never reprints it.
  • 2nd run (draft) — that fast answer is already in the table (no warning), but the long-lived refine pass is not done yet — often still English.
  • 3rd run (refined) — the i18n engine’s refined version has landed; load() returns Korean (Steve는 20살입니다 !!!).

Same idea with subscribe, so draft and refined updates re-render in one process — no second/third run needed:

$ node exam-positional-placeholder-2.js
//
// exam-positional-placeholder-2.js
//
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";
const age = 20;

// render texts
function renderTexts() {
  console.log(t`Is ${name} really ${age} years old ???`);
}

// subscribe to i18n events
i18n.subscribe((e) => {
  console.log(
    `\n\ni18n-event: ${e.reason}${e.phase ? ` - ${e.phase}` : ""} [${e.language}]`,
  );
  if (e.reason === "load" || e.reason === "translate") {
    renderTexts();
  }
});

i18n.setLanguage("en_us"); // switch to English (default)
renderTexts(); // first render: Is Steve really 20 years old ???

i18n.setLanguage("ko_kr"); // switch to Korean

await i18n.waitFor("exit"); // wait for the application to exit

Output (first run without cache):

Is Steve really 20 years old ???


i18n-event: language [ko_kr]
[@aurorah/i18n] no ko_kr translation for "Is {0} really {1} years old ???" (hashId jfebmd1bbrvr5) in the loaded table


i18n-event: load [ko_kr]
Is Steve really 20 years old ???


i18n-event: translate - draft [ko_kr]
Is Steve really 20 years old ???


i18n-event: translate - refined [ko_kr]
Steve이(가) 정말 20살인가요???

Output (second run with cache):

Is Steve really 20 years old ???


i18n-event: language [ko_kr]


i18n-event: load [ko_kr]
Steve이(가) 정말 20살인가요???

5. Put values into your text — Named Placeholder

Write ${{ something }} to give the placeholder a NAME ({something}). With m you define a message once and reuse it with different values — and .note() leaves a comment for the i18n translator. The value at define time is only for shape (a dummy is fine); pass real values when you call the message.

$ node exam-named-placeholder.js
//
// exam-named-placeholder.js
//
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 m = i18n.m; // `m` stays bound to the instance

const something = ""; // dummy — only the key name matters at define time

// define once, reuse anywhere — .note() is for the translator:
const hello = m`Hello~ world !!! And this is ${{ something }} !!!`.note(
  "Greeting on the home page casually. {something} is the user's name, introducing himself.",
);

// render texts
function renderTexts() {
  console.log(hello({ something: "Steve" })); // 안녕~ 세상아!!! 그리고 나는 Steve야!!!
  console.log(hello({ something: "Alex" })); // 안녕~ 세상아!!! 그리고 나는 Alex야!!!
}

// subscribe to i18n events
i18n.subscribe((e) => {
  console.log(
    `\n\ni18n-event: ${e.reason}${e.phase ? ` - ${e.phase}` : ""} [${e.language}]`,
  );
  if (e.reason === "load" || e.reason === "translate") {
    renderTexts();
  }
});

i18n.setLanguage("ko_kr");

await i18n.waitFor("exit"); // wait for the application to exit

Output (first run without cache):

i18n-event: language [ko_kr]
[@aurorah/i18n] no ko_kr translation for "Hello~ world !!! And this is {something} !!!" (hashId 920g3m1g1qylg) in the loaded table


i18n-event: load [ko_kr]
Hello~ world !!! And this is Steve !!!
Hello~ world !!! And this is Alex !!!


i18n-event: translate - draft [ko_kr]
안녕하세요~ 세상아!!! 그리고 이건 Steve 야!!!
안녕하세요~ 세상아!!! 그리고 이건 Alex 야!!!


i18n-event: translate - refined [ko_kr]
안녕~ 세상아!!! 그리고 나는 Steve야!!!
안녕~ 세상아!!! 그리고 나는 Alex야!!!

Output (second run with cache):

i18n-event: language [ko_kr]


i18n-event: load [ko_kr]
안녕~ 세상아!!! 그리고 나는 Steve야!!!
안녕~ 세상아!!! 그리고 나는 Alex야!!!

Same pattern as positional placeholders: load shows the source text, then translate - draft / translate - refined upgrade it in one process via subscribe.

6. i18n engine — Auto-complete

Your code stays a plain t call — grammar such as ordinals (1st, 2nd, 3rd), plurals, and number/date formats is completed AUTOMATICALLY by the i18n engine. With upgradeSourceLanguage: true even the source language resolves through the engine.

The i18n engine is self-improving, and i18n.subscribe() is how the UI follows it: 1) the first render shows the text as written, 2) after ~1-2 seconds the fast first answer arrives (translate - draft), 3) after ~10-60 seconds the refined version replaces it (translate - refined) — each arrival fires the subscriber, so just re-render there.

$ node exam-auto-complete.js
//
// exam-auto-complete.js
//
import { I18n } from "@aurorah/i18n";

// "www.example.com" = your project's namespace (scopes its translation tables)
// "en_us" = {language}_{country} (ISO 639-1 + ISO 3166-1)
const i18n = new I18n({
  namespace: "www.example.com",
  language: "en_us", // active language
  sourceLanguage: "en_us", // language the source strings are written in
  upgradeSourceLanguage: true, // let the i18n engine improve the source language too (ordinals, etc.)
});
const t = i18n.t; // `t` stays bound to the instance

const name = "Steve";

// render texts
function renderTexts() {
  for (let n = 1; n <= 4; n++) {
    // 1) NOW: the text as written — "2st", "3st", "4st"
    // 2) ~1-2s: the fast first answer arrives
    // 3) ~10-60s: the refined version — the i18n engine completed the grammar
    console.log(t`${name} is ${n}st winner in the contest.`);
  }
}

// subscribe to i18n events
i18n.subscribe((e) => {
  console.log(
    `\n\ni18n-event: ${e.reason}${e.phase ? ` - ${e.phase}` : ""} [${e.language}]`,
  );
  if (e.reason === "load" || e.reason === "translate") {
    renderTexts();
  }
});

renderTexts(); // first render: text as written

await i18n.waitFor("exit"); // wait for the application to exit

Output (first run without cache):

Steve is 1st winner in the contest.
Steve is 2st winner in the contest.
Steve is 3st winner in the contest.
Steve is 4st winner in the contest.
[@aurorah/i18n] {1, plural}: en_us needs [one, other], missing [one] (falls back to "other") — hashId 14ykp781y8zfz4, translation "{1, plural, other {{0} is #st winner in the contest.}}"


i18n-event: translate - draft [en_us]
Steve is 1st winner in the contest.
Steve is 2st winner in the contest.
Steve is 3st winner in the contest.
Steve is 4st winner in the contest.


i18n-event: translate - refined [en_us]
Steve is 1st winner in the contest.
Steve is 2nd winner in the contest.
Steve is 3rd winner in the contest.
Steve is 4th winner in the contest.

The draft may still show "2st" / "3st" / "4st"; the refined pass completes the ordinals (2nd, 3rd, 4th). On a later run with a warm cache, the completed grammar can appear on the first render.

That's the whole everyday API — t, m, .note(), plus subscribe() when the UI or console needs to follow updates. Plurals, gender branches, and number/date formats are handled by the i18n engine, so your code never needs to worry about them.

Support for Next.js and other web frameworks (CSR/SSR)

For web apps @aurorah/i18n supports the CSR/SSR pattern out of the box. Two pieces wire it up:

  • I18n.initServer() — the SERVER half; create it once on the server side.
  • ssrServer constructor option — the CLIENT half; new I18n({ ssrServer }) connects the two.

This works with any Node framework: Next.js (works as a Server Action as-is), SvelteKit, Nuxt, Remix, Express, and Nest.js.

Exact example for Next.js:

//
// lib/i18n.server.ts — the server half (Server Action)
//
"use server";

import { I18n } from "@aurorah/i18n";

export const I18nServer = I18n.initServer({ namespace: "my-app" });
//
// lib/i18n.ts — 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(() => {
    return 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
}

Core functions

| Function | Description | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | new I18n({...}) | Configuration (namespace, language, sourceLanguage, tables, autoTranslate, debug, refinePollMs, upgradeSourceLanguage). | | i18n.load() | Pre-load one language table into memory. | | i18n.setLanguage() | Switch the active language; only the target language's table stays in memory. | | ``i18n.t...``` | Render NOW at the call site (also callable as i18n.t("...")). | | i18n.m....note() | Define ONCE, render later;.note()chains a translator note. | |i18n.t.rich... / i18n.m.rich... | Rich text:...survives translation; render returns a chunk array with tag handlers supplied at render. | |i18n.subscribe() | Listen for change events (language/load/translate/import). On "translate", e.phaseis"draft"or"refined". Returns unsubscribe. | | i18n.waitFor(reason) | Await the next matching change event (console/scripts). Optional{ timeoutMs }. Console demos: waitFor("exit")keeps the process open. | |i18n.exportStrings() | Export all strings (with translator notes) for human translators. | |i18n.importStrings() | Pin reviewed translations for one language; they are never overwritten automatically (production posture withautoTranslate: false). | | i18n.formatNumber()/formatDate()/formatCurrency()|Intl` formatters bound to the active language. |

Placeholders

| Interpolation | Canonical textId | | ------------- | ------------------------------------------------------ | | ${value} | positional — {0}, {1}, … | | ${{name}} | named — {name} (object literal with exactly one key) |

Number values are auto-detected and translations come back as ICU MessageFormat plural messages; the built-in ICU runtime picks the CLDR branch via Intl.PluralRules. Date values are auto-detected too: a bare Date renders with dateStyle=medium / timeStyle=short, and i18n.date(d, style) / i18n.time(d, style) pin date-only/time-only styles; formatting happens locally via Intl.DateTimeFormat, translations only localize the surrounding words.

ICU MessageFormat support

Writing ICU by hand is an option for advanced users — it keeps @aurorah/i18n compatible with existing ICU catalogs and i18n translators. The built-in runtime parses the full ICU MessageFormat grammar: {name}; {name, number} with keyword styles and ::skeletons and DecimalFormat patterns; {name, date|time} with short|medium|long|full, ::skeletons and custom CLDR patterns; {name, plural} with offset:N and =N exact matches; {name, selectordinal}; {name, select}; {name, ordinal}; {name, duration}; {name, spellout}; {name, choice, ...}; ICU quote rules; <tag>...</tag> rich-text tags. Malformed input never crashes a render — it falls back to flat text.

Authoring guidance: prefer plural/selectordinal/select, number skeletons (::currency/XXX, ::compact-short, ::percent scale/100) and date skeletons (::yMMMd); avoid spellout, choice, and custom date patterns — those degrade to plain numbers / approximate formats.

i18n engine

The i18n engine translates strings automatically — no setup needed. Translations improve on their own in two phases: a fast first answer arrives immediately (translate - draft) and a higher-quality refined version replaces it shortly after (translate - refined) — subscribers are notified on each, with the phase in e.phase.

License

MIT