scmJS docs

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.