* feat(i18n): add Korean translation, fix zh-CN drift, cover hardcoded strings
Adds Korean (ko) as the third web UI language, brings zh-CN back into
parity with en, converts ~80 hardcoded English strings in app.js into
i18n keys, and installs a pre-commit hook that prevents future drift.
## Korean web UI
- New `src/channels/web/static/i18n/ko.js` — full translation of all
663 keys, mirroring the structure of `en.js`/`zh-CN.js`
- New `src/channels/web/server.rs` route `/i18n/ko.js` + handler
- New language menu button in `index.html`
- Browser auto-detect now special-cases `ko-*` (in addition to `zh-*`)
so Korean visitors land on Korean by default
- Toast label map in `i18n-app.js` becomes a small lookup table so the
next language is a single-line addition
## zh-CN drift fix
`zh-CN.js` was missing 9 keys that had been added to `en.js` after the
Chinese pack was last touched (`config.telegramOpenBot`,
`settings.tools`, and 7 keys under the `tools.*` namespace for the new
Tool Permissions tab). Backfilled with Chinese translations so users on
the Tools settings panel see proper labels instead of raw key strings.
## Hardcoded strings in app.js
`app.js` had ~80 user-facing English string literals that bypassed
`I18n.t()` entirely — toasts, confirms, alerts, button labels, meta-item
labels for jobs/routines/missions detail panels, the theme dynamic
label, dynamic auth states ("Connecting...", "Authenticated"), etc.
These were invisible to the language switcher and would always render
in English regardless of the user's choice.
Replaced every literal with `I18n.t('key', { ...placeholders })` and
added the corresponding ~95 new keys to `en.js`, `zh-CN.js`, AND `ko.js`
in lockstep so all three packs stay at 663 keys with identical key sets
and matching `{name}`-style placeholder tokens.
Existing keys were reused where possible (`message.copy`,
`approval.approved`, `connection.reconnected`, etc.).
## Pre-commit parity hook
New `scripts/check-i18n-parity.sh` (pure POSIX bash, no Node) verifies:
1. No duplicate keys within any single language file
2. Every language has the same key set as `en.js` (the source of truth)
3. Placeholder tokens like `{name}`, `{count}` match across all
languages — catches the silent bug where a translator drops an
interpolation token
Wired into both pre-commit hook install paths:
- `scripts/pre-commit-safety.sh` (installed by `dev-setup.sh` as a
symlink at `.git/hooks/pre-commit`; symlink is followed via
`readlink` so the script location resolves correctly)
- `.githooks/pre-commit` (used when devs set
`git config core.hooksPath .githooks`)
Both block the commit on failure with a clear error message and the
`git commit --no-verify` escape hatch. Tested by deliberately removing
a key from `ko.js` (caught) and stripping a `{path}` placeholder
(caught).
## Korean README
New `README.ko.md` — full Korean translation of `README.md`. Follows
the layout of `README.ja.md` (6-item single-word ToC to keep anchors
clean for non-Latin headings). All code blocks, image paths, and badge
URLs preserved verbatim.
`한국어` link added to the language switcher in all 5 READMEs
(`README.md`, `.zh-CN.md`, `.ru.md`, `.ja.md`, and the new `.ko.md`).
## Verification
- `./scripts/check-i18n-parity.sh` — `OK (663 keys × 3 languages)`
- `node --check` clean on every modified JS file
- Three-way parity: identical sorted key sets across en/zh-CN/ko, zero
placeholder mismatches
- Hook tested by removing/mutating keys and confirming the commit is
blocked
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(i18n): address PR review feedback [skip-regression-check]
Addresses 6 review comments on #2065. All changes are in
src/channels/web/static/ (per .claude/rules/review-discipline.md
exemption) plus a bash helper script — no Rust code is touched.
## scripts/check-i18n-parity.sh
- **Portable mktemp** (Copilot): bare `mktemp` works on GNU but BSD/macOS
`mktemp` requires an explicit template with at least 6 trailing X's.
Wrap in a small `mktemp_file()` helper that always passes a template
(`${TMPDIR:-/tmp}/check-i18n-parity.XXXXXX`) so the script runs on
every platform.
- **Symlink-attack-prone /tmp path** (gemini-code-assist): the
placeholder-mismatch buffer was using `/tmp/i18n-ph-mismatch.$$`,
which is predictable and vulnerable to symlink races in shared
/tmp. Replace with `mktemp_file()` for consistency with the rest
of the script.
## src/channels/web/static/app.js
- **Hardcoded `'Mode'` label** (gemini): jobs detail meta-grid had
`metaItem('Mode', job.job_mode)` — convert to
`I18n.t('jobs.mode')` and add the new key to all 3 language packs.
- **Hardcoded `'Yes'`/`'No'`** (Copilot): routine detail showed
`routine.enabled ? 'Yes' : 'No'` even though the surrounding labels
were translated. Reuse the existing `settings.on`/`settings.off`
keys ("On"/"Off") which already render in all languages.
- **Hardcoded `'N/A'`** (Copilot): mission detail showed
`m.next_fire_at ? formatDate(...) : 'N/A'`. Reuse the existing
`common.noData` key. Also fixed the same pattern in the TEE popover
(`renderTeePopover`) where `'N/A'` was used as a fallback for
three different attestation fields, since fixing the pattern
across the file is the principled response per the repo's
review-discipline rule.
## src/channels/web/static/i18n-app.js
- **Hardcoded `LANG_LABELS` map** (gemini): the language-switch toast
was reading from a per-call `{ 'en': 'English', 'zh-CN': '简体中文',
'ko': '한국어' }` literal that would grow with every new language
and drift from the actual supported set. Move each language's own
native name into its own pack under a new `language.name` key:
en.js → 'language.name': 'English'
zh-CN.js → 'language.name': '简体中文'
ko.js → 'language.name': '한국어'
Then the toast becomes `I18n.t('language.switch') + ': ' +
I18n.t('language.name')` — both halves are read from the language
pack that was just switched in, so the entire toast appears in the
newly selected language. Adding a future language is now a single
key addition with NO changes to i18n-app.js.
## Verification
$ ./scripts/check-i18n-parity.sh
i18n parity: OK (665 keys × 3 languages)
$ cargo test --lib
test result: ok. 4241 passed; 0 failed; 3 ignored
Three-way parity preserved with the 2 new keys (`jobs.mode` and
`language.name`) added to all three language packs in lockstep.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>