clikae's board and prompts speak nine languages today — en-US, ja-JP, zh-TW,
zh-Hans, ko-KR, es-ES, de-DE, fr-FR and pt-BR. Bare clikae lang
always prints the live list; treat that, not this sentence, as authoritative
(_i18n_locales is where both come from). Every locale ships inside clikae
itself — a language switch is instant, offline, and can never fail on a
download. Adding one is a self-contained PR touching two files.
TL;DR — the touch points
lib/i18n/<code>.sh— copylib/i18n/en-US.shand translate. Use the full code (ko-KR,fr-FR, …).en-US.shis the canonical key list; your file must define every key it defines (the test below will list what's missing).- One line in
_i18n_localesinlib/core/i18n.sh— the supported-locale list. This function is the single source of truth:clikae lang's choices, the board'slpicker, and the CI completeness test all read it, so your locale appears everywhere (and is enforced) with no further edits. - Run
bash scripts/test.sh—tests/bats/i18n.batsextracts the key list fromen-US.shand the locale list from_i18n_localesmechanically, and fails on any missing/empty key or placeholder mismatch. A partial translation cannot merge silently. No test edits needed.
That's it for almost every language: regional variants resolve through the
generic language-subtag rule in _i18n_normalize (ko_KR.UTF-8 → ko →
ko-KR, fr_CA → fr-FR) with no extra line. Two honest exceptions:
- A script-split language needs its own resolver case, because the writing
system — not the region — decides which file to load. Chinese is the shipped
example:
zh_TW/zh_HK/*Hant*→zh-TW,zh_CN/zh_SG/*Hans*→zh-Hans, and a barezhkeeps Traditional as the incumbent default. Both cases are in_i18n_normalize; copy their shape if your language splits the same way. - Extra human spellings (
日本語,english) are optional case lines in the same function.
The file contract
lib/i18n/<code>.sh is plain bash, loaded with source over the en-US base
(so it must be dependency-free and bash-3.2-safe — no associative arrays):
- one
T_KEY="value"per line, at column 0, double quotes — the test andclikae langextract by this pattern; T_LANG_NAMEis your language's own name for itself (the endonym) — it labels the language inclikae lang's output;%s/%dare printf placeholders: keep exactlyen-US's placeholders inen-US's order, and write%%for a literal percent sign — a stray%corrupts the printf at runtime (CI catches this too);i18n_summary— the board's "N tanks across M engines" line — is a function, because grammars count differently. Override it if English pluralisation reads wrong in your language; if you don't, the English fallback is used.
What to translate (and what not to)
Translation is graded, not wall-to-wall:
- Localize the sentences a human reads to decide or understand: prompts, confirmations, menu items, status lines, warnings. These are the point — a consent question you can only read in English isn't informed consent.
- Keep technical the tokens that ARE the interface: command lines
(
clikae to,clikae demo), flags, paths (~/.gemini), engine/tank names, sizes and unit suffixes (MB). Users copy-paste, run, and search for these verbatim — translating them breaks that.
en-US.sh shows the split in practice; when unsure, match what ja-JP.sh and
zh-TW.sh did for the same key.
Quality bar: an LLM-grade baseline translation is acceptable to land a language (that's how ja-JP started) — completeness is enforced by machines, tone is improved by people. Native-speaker polish PRs are very welcome and can be tiny (even one string).
Checking your work
bash scripts/test.sh # completeness + the whole suite
CLIKAE_LANG=<code> clikae # eyeball the board in your language
clikae lang <code> # the switcher already knows it