Skip to content

Internationalisation

Every string Astrolabe shows comes from a message catalog: no UI text is hard-coded and issues are codes, never sentences. The UI and the content languages are Polish and English. Both catalogs of every package are always on, whatever options you pass; your own catalogs change the wording on top of them.

Setting Where Decides
UI locale locale on <astl-player> and <astl-editor>, locale in createAuthoring, the locale argument of engine.describe buttons, verdict words, counters, announcements, issue messages, the authoring kit
Content language lang on <astl-doc> (required), or on any element inside grading (decimal comma, case rules, diacritics), text direction, screen reader voice

They never mix: a Polish teacher can play an English exercise with Polish buttons. The player and the editor pick their UI locale from locale, else their own lang attribute, else the browser languages, and match it against the locales they have catalogs for, falling back to English. Matching is by language: pl-PL uses the pl catalogs and en-GB the en ones.

The content language does real work:

  • Number gaps accept a decimal comma in a Polish document: 0,5 is correct for answer="0.5", and spaces may group digits (1 234). An English document takes 0.5 and 1,234.5.
  • Text gaps lower-case with the content language, and compare diacritics strictly by default: in Polish, zolw is not żółw.
  • dir defaults from lang (ltr for Polish and English); an explicit dir wins.
  • Text in the other language goes in <span lang="...">, so screen readers switch voices.

A catalog is the messages of one namespace in one locale:

interface MessageCatalog {
readonly namespace: string; // e.g. 'astl.player'
readonly locale: string; // 'en' or 'pl'
readonly messages: Readonly<Record<string, string>>; // key -> ICU MessageFormat 1
}
Namespace Package Keys
astl.player @astrolabe/player (playerMessages) player chrome and words shared by all elements (verdict.*)
astl.engine @astrolabe/engine (engineMessages) document issue codes
astl.editor @astrolabe/editor (editorMessages) the authoring kit and the surface of <astl-editor>
astl.gap, astl.choice, ... each component (messages/ui/<locale>.json, messages/<locale>.json) ui.* for students in the first, the only strings its element loads; its issue codes, editor.* and template.* for authors in the second
astl.style.playful, astl.style.notebook @astrolabe/tokens the label and description of each built-in look

Every component page in the reference lists all its keys in both locales.

Messages use a dependency-free subset of ICU MessageFormat 1, implemented in @astrolabe/core:

Syntax Example
argument Gap {n}
number {max, number}, {share, number, percent}, {n, number, integer}
plural, with # and exact matches {count, plural, =0 {No checks left} one {# check left} other {# checks left}}
plural with an offset {count, plural, offset:1 one {...} other {...}}
ordinal {n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}
select {state, select, done {answered} partial {partly answered} other {not answered}}
quoting '' is an apostrophe; '{' is a literal brace

Dates and times are not supported on purpose: hosts format them and pass strings.

A plural message must cover every plural category of its locale. English needs one and other; Polish needs one, few, many and other:

{
"checks.left": "{count, plural, =0 {Nie masz już sprawdzeń} one {Zostało # sprawdzenie} few {Zostały # sprawdzenia} many {Zostało # sprawdzeń} other {Zostało # sprawdzenia}}"
}

pluralCategories(locale) from @astrolabe/core lists the categories a locale needs (from Intl.PluralRules).

To reword a message, pass a catalog with just the keys you change, in the namespace and locale they belong to. Your keys win over the built-in ones; every other message stays.

import type { MessageCatalog } from 'astrolabe/player';
const myWording: MessageCatalog[] = [
{ namespace: 'astl.player', locale: 'en', messages: { 'action.check': 'Check my answers' } },
{ namespace: 'astl.player', locale: 'pl', messages: { 'action.check': 'Sprawdź odpowiedzi' } },
];

Register them where the strings are shown. catalogs is an option of all three:

definePlayer({ catalogs: myWording }); // astrolabe/player: the player and its elements
createEngine({ secret, catalogs: myWording }); // astrolabe/server or astrolabe/engine: engine.describe(issue, 'pl')
defineEditor({ catalogs: myWording }); // astrolabe/editor: the kit and its TipTap surface

The player inside the editor's preview is a player like any other: it takes the catalogs given to definePlayer on that page. Reword both locales, so Polish and English readers get the same message. checkCatalog(catalog) from @astrolabe/core (also exported by the astrolabe entries) returns the problems of a catalog: messages that do not parse, and plurals that miss a category of their locale:

import { checkCatalog } from 'astrolabe/player';
for (const catalog of myWording) expect(checkCatalog(catalog)).toEqual([]);

With the @astrolabe/* packages, the same catalogs option is on definePlayer, createEngine and createAuthoring. Catalogs passed later override keys of the same namespace and locale.

  • Keys are semantic (action.check), never the English text.
  • One message per sentence the reader sees; no concatenation of translated fragments.
  • Issue messages say what is wrong and what to do ("Gap g1 has no answer."), in the reader's words.
  • No em dash characters; quotes as the language writes them (“English”, „polski”).