Translations
The editor's own words — menus, dialogs, hints, Check Map's findings — can be shown in
another language; a map's text is the map's and is never touched by this. English and
Korean exist today, chosen in Preferences ▸ General or following the browser. The
scheme is deliberately small and has no library behind it: the browser's own Intl
does the parts that need data (plural rules, number formatting), and the rest is a
hundred lines.
Writing a string. The English text is the key. A component calls t("Save {name}", { name }) and reads as it always did; an untranslated string falls back to itself; and
editing the English deliberately orphans the Korean, which is the right failure — a
translation of words that are no longer there should not survive them. Where one
English word means two things (Open as a menu label and as an adjective), tc("menu", "Open") gives it a context. A string kept in a table rather than said at the point of
use is marked with msg("…") where it is written and shown through translate(value),
so the extractor sees the text and the table keeps its English. The argument must be a
string literal — never a variable or a template with ${} in it — because the extractor
reads the source, not the running program.
Placeholders are a small subset of ICU MessageFormat. {name} is filled from the
parameters; {n, plural, one {# map} other {# maps}} picks a branch by the language's
plural rules, with =0-style exact matches first and # the number formatted for the
language; {x, select, a {…} other {…}} picks by value. Korean has no plural forms and
writes only other, and its particles agree with the word before them, so a Korean
translation writes {name|을} (or 이/가, 은/는, 과/와, 으로/로) and gets 맵을 or 위치를
by the value's last syllable rather than "{name}(을)를".
The catalogues. One flat JSON object per language under src/i18n/ — ko.json —
with the English text as the key and the translation as the value. npm run i18n reads
every t(), tc() and msg() in src/ with the TypeScript compiler and reports what
each catalogue is missing, no longer needs, or has wrong (a translation whose
placeholders are not the source's); npm run i18n -- --write brings the files up to
date, adding a new key with an empty value and dropping keys nothing asks for. An empty
value is allowed and shows in English, so a string can be added before its translation
exists; the test suite fails on a missing key, an unused key, a placeholder mismatch or
a t() whose argument is not a literal, so the catalogues cannot drift from the source.
Translating is filling in the empty values — the report lists them — and a native
reader's review of the wording is worth more than any of the machinery.
Adding a language is an entry in LOCALES, a src/i18n/<tag>.json, and its name in the
script's list. A language that needs its own fonts adds them after the Latin ones in
the font stack, the way the Korean faces are: Inter has no Hangul, so Latin text keeps
its look and Hangul falls through to a face that has it.
Plugins get the same translator on api.i18n — register their own catalogues,
t and tc over them, language, and the "language" event — and can run the same
extractor over their own source; see docs/plugins.md.
Tables of named things. A menu label is an item's identity as well as its text —
plugins place items by the English label — so the menu model keeps the English and
translates as it draws: msg() on the label where the table is written, translate()
where it is shown. The same holds for every table the editor keeps: the layer names,
the unit, upgrade and technology names (unitLabel is the translated unitName; the
English name stays the vocabulary of the text trigger format and the plugin API), the
tileset and terrain names, the player colours, the trigger editor's choices, the
section descriptions in the Save dialog. The Select control translates its option
labels itself, so a table handed to it needs nothing more. A string that is not a
literal at the point of use must never go through t() — the extractor cannot see it
and the catalogue would drift — and a t() at module scope runs before the language
is known, so a module-level table uses msg() and is translated where it is read.
Reviewing the Korean. npm run i18n -- --export ko ko.csv writes the catalogue as
a spreadsheet: the English, the Korean, and the source files each string appears in, so
a term can be checked in context. A reviewer edits the Translation column; npm run i18n -- --import ko ko.csv reads it back, matching rows by their English. The whole
editor is translated, but by its author with a dictionary rather than by a native
speaker: the vocabulary follows the Korean StarCraft community's transliterations
(마린, 저글링, 질럿; 장식물 for doodads, 로케이션 for locations, 세력 for forces), and
every sentence would benefit from a native reader.