Localisation
Translator uses the English source as the key: tr('Open the gate') shows the English until the current language's catalog has a line for it, so writing a new string never breaks the game, it only shows untranslated. Languages switch at once, with no reload.
Two languages
import { Translator } from '1681-engine';
const t = new Translator({ source: 'en', onMissing: (key, lang) => console.warn(`[i18n] no ${lang} for "${key}"`) });
t.addCatalog('fr', {
'Coins {0}/{1}': 'Pièces {0}/{1}',
Paused: 'Pause',
'Start|game': 'Jouer',
'{0} coins collected in all': { one: '{0} pièce ramassée en tout', other: '{0} pièces ramassées en tout' },
});
t.setLanguage('fr');
const { tr, trn } = t; // bound: hand them around freely
tr('Coins {0}/{1}', 3, 10); // 'Pièces 3/10'
tr('Start|game'); // 'Jouer'; in English, 'Start'
trn('{0} coin collected in all', '{0} coins collected in all', 1); // '1 pièce ramassée en tout'The rules
- Placeholders are positional:
tr('{0} joined', name). A placeholder with no argument is printed as written, so a missing argument shows where it is instead of printing "undefined".{{and}}print a literal brace. - Context after
|: the same English with two meanings is two keys,tr('Close|window')andtr('Close|near'). The context is for translators and never shown. - Plurals go through
trn(one, other, count, …args). The key is the plural form; a catalog gives every category its language has (one,few,many,other, byIntl.PluralRules), or a plain string.{0}is the count and the other arguments follow as{1},{2}…. - Missing lines are reported once per key and language to
onMissing, never for the source language.
Switching at once
setLanguage tells every subscriber, so a UI re-renders instead of reloading. Read labels when they are shown, never once at import: a table of labels computed at module load stays in the language it was loaded in.
const stop = t.subscribe(() => refresh()); // in onEnter
// in onExit: stop();
function refresh() {
title.setText(t.tr('Paused'));
resume.setText(t.tr('Resume'));
column.layout(); // a translation may be wider or narrower than what it replaced
}examples/platformer switches between English and French from the title and the pause menu, and every scene subscribes like this. Name each language in itself on the picker (Français, not French): a player looking for their own language must be able to read its name.
Finding the keys
extractStrings scans source text for tr('…') and trn('…', '…', n) calls, for a script that lists keys missing from a catalog or drops unused ones.
import { readFileSync } from 'node:fs';
import { extractStrings } from '1681-engine';
const keys = new Set<string>();
for (const file of sourceFiles) for (const s of extractStrings(readFileSync(file, 'utf8'))) keys.add(s.key);
const missing = [...keys].filter((k) => !(k in frenchCatalog));It is a scanner, not a parser: it reads a string literal straight after the call's parenthesis and skips a template literal with ${…} in it or a key that is not a literal, since neither can be a catalog key. A call inside a comment is still found. Pass { single: ['tr', 't'], plural: ['trn'] } for other function names.
The React kit's words
The engine never translates anything itself. The React kit prints a handful of words of its own (Close, Cancel, OK…); hand it the translated ones through UiProvider's labels.