i18n
Screen. Strings as data: ICU messages compiled per locale, tr() returns a Message, the screen gets parts, never a joined string.
What it does
i18n holds the strings of the game. tr(key, params?) builds a frozen Message
{ key, params }; no locale is read at the call site. format(message) turns it into Part[] at
run time: text parts and element parts, never one joined string.
| Set | Tier | Builds on | Emits |
|---|---|---|---|
screen, in screen | Complex | flow | i18n:locale-changed |
Strings. They live in strings/<locale>.json of any feature or sub-feature, as ICU
MessageFormat: plural, select, selectordinal, number, date, time. No skeletons and no
currency.
{n, plural, one {# order} other {# orders}}moku-game keys (or bun run assets:keys) compiles them to plain functions in
generated/strings.<locale>.ts, and writes the Strings type for defineGame. With
strings: Strings in the game types, a wrong key or a missing parameter does not compile. The
compiler is compileStrings of @moku-labs/game/assets.
Sub-features. A sub-feature's keys start with its name, ep01.title, and compile to
generated/strings.ep01.<locale>.ts. It registers them lazily. With assets composed they load
and unload with the bundle named like the sub-feature. While the bundle is not loaded, a key of it
gives ⟦key⟧ and has(key) is false.
Missing keys. The lookup order is the current locale, then fallback. A missing key gives one
text part ⟦key⟧ and one warning.
Config
pluginConfigs.i18n.
| Option | Type | Default | Description |
|---|---|---|---|
locale | string | "en" | The locale at start. |
fallback | string | "en" | The locale a missing key is read from before it is reported missing. |
locales | Record<string, module | loader> | {} | Compiled modules outside features, per locale. A loader is imported on first use. |
A feature brings its own: defineFeature("hud", { strings: { en: () => import("./generated/strings.en") } }).
API
Mounted at app.i18n, reachable with ctx.require(i18nPlugin).
| Method | Signature | Description |
|---|---|---|
locale | () => string | The current locale. |
setLocale | (locale: string) => Promise<void> | Imports the module once, then emits i18n:locale-changed. text draws every label again. |
format | (message: I18n.Message, locale?: string) => readonly I18n.Part[] | Formats in the current locale, or another registered one without switching. |
plain | (message) => string | The parts as one string, elements dropped. For logs and tests. |
has | (key: string) => boolean | Whether a key exists in the current locale or the fallback. |
isKey | (value: unknown) => value is Key | Whether a value read from a save is a key the game still has. |
duration | (ms: number, style?) => string | Milliseconds through Intl.DurationFormat, rounded up to whole seconds. |
locales | () => readonly string[] | Every locale a module was brought for, sorted. |
replace | (locale: string, messages) => void | The dev hot swap of a generated strings file. |
messageArgument and messageDuration are root exports the compiled modules call. A game does
not call them.
Events
Emits
| Event | Payload | When |
|---|---|---|
i18n:locale-changed | { locale: string } | The new locale is loaded, or replace swapped messages of the current locale or the fallback. Never at start. |
Listens to
None.
Example
import { createApp, screen, tr } from "@moku-labs/game";
const app = createApp({
plugins: [...screen],
pluginConfigs: { i18n: { locale: "en", fallback: "en" } }
});
await app.start();
const orders = tr("hud.orders", { n: 3 }); // a frozen Message: no locale is read here
app.i18n.format(orders); // Part[]: text and element parts
app.i18n.plain(orders); // one string, for logs and tests
if (app.i18n.locales().includes("ru")) await app.i18n.setLocale("ru");- Never join parts yourself. An element part carries an icon or a styled run;
plaindrops it. - Check saved keys with
isKey.defineGameApp({ strings: type<Strings>() })typesapp.i18n, so a trueisKey(value)narrows the value to a key. - The kit's
trtakes keys, not plain strings. A key from a data table is typedI18n.TextKeyor built with the kit'skey("board.hint.red").