Skip to content

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 ​

ts
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') and tr('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, by Intl.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.

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

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

API ​

Translator · Catalog · formatMessage · extractStrings

Released under the MIT License.