Skip to content

feat(tui): TUI i18n — config, footer, /settings selector, reverse-RPC & command/help translation#8

Draft
meymchen wants to merge 10 commits into
mainfrom
feat/i18n-walking-skeleton
Draft

feat(tui): TUI i18n — config, footer, /settings selector, reverse-RPC & command/help translation#8
meymchen wants to merge 10 commits into
mainfrom
feat/i18n-walking-skeleton

Conversation

@meymchen

@meymchen meymchen commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Related Issues

Resolve #1, Resolve #2, Resolve #3, Resolve #4, Resolve #5, Resolve #6, Resolve #7

Problem

See linked issues. kimi-code had no way to render the TUI in any language other than English: there was no language preference, no translation layer, and every UI string was a hardcoded English literal. And even once a preference existed, there was no in-app way to change it — no /settings entry and no runtime switch.

What changed

Slice 1 — i18n walking skeleton (#1)

Stands up the whole path end-to-end and proves it on one surface, the footer/status bar.

  • Config. Add a language field to tui.toml as auto | en | zh-CN (default auto), wired symmetrically through parse/normalize/render so the value round-trips without loss.
  • I18n layer. A zero-dependency I18n class plus global singleton, modeled on the existing currentTheme singleton. Provides t('module.submodule.phrase', params?) with active-locale lookup, fallback to en on a missing key, {param} interpolation, and setLocale for runtime switching. Components call i18n.t(...) at render time, exactly as they call currentTheme.fg(...).
  • Locale resolution. auto resolves to a concrete locale from LC_ALL / LC_MESSAGES / LANG (POSIX precedence) against a small match table; C / POSIX / unknown values fall back to en.
  • Language packs. locales/en/ and locales/zh-CN/ organized as per-module namespace files merged into a per-locale index.ts, locking the contributor-facing key convention.
  • Startup wiring. run-shell resolves the locale and initializes the singleton right after the theme, before the TUI is constructed. AppState now carries language.
  • Live surface. The footer status labels (context, thinking, and the background task/agent "running" badges) now render via i18n.t(...), with en and zh-CN translations. Strings stay colored through currentTheme / chalk.hex — no chalk named colors introduced.

Slice 2 — language selector + runtime switch (#2)

Builds the interactive path on top of the skeleton: a user can now choose the UI language from /settings and watch it apply immediately.

  • LanguageSelectorComponent. A ChoicePickerComponent subclass (modeled on ThemeSelectorComponent) titled "Language / 界面语言", offering Auto / English / 简体中文 with the current value highlighted.
  • /settings routing. A new "Language" entry extends the SettingsSelection union and isSettingsSelection, routed to the new selector.
  • Apply on select. Persists to tui.toml via saveTuiConfig, updates AppState, flips the live locale with i18n.setLocale(resolveLocale(language)), and repaints — so the whole UI switches language immediately, no restart.
  • /reload. applyReloadedTuiConfig re-reads language and re-applies it via setAppState + i18n.setLocale, alongside the existing theme/notifications/upgrade reapplication.

Slice 3 — approval & question panel translation (#3)

Extends the translation layer to the reverse-RPC interaction surfaces — the approval and question panels the agent puts up to ask for a decision.

  • reverseRpc namespace. New per-locale locales/{en,zh-CN}/reverse-rpc.ts packs covering the approval panel, question dialog, and full-screen approval preview, merged into each locale index.ts.
  • Approval panel. Tool headers ("Run this command?" …), danger labels, cwd/scope/hidden-line metadata, the feedback prompt, and the ↑/↓ · choose · ↵ hint render via i18n.t('reverseRpc.approval.*'). Choice buttons and danger labels are localized in the approval adapter at adapt time; selected_label stays a stable English identifier so the upstream contract never shifts with the UI language.
  • Question dialog. Heading, review/submit prompts, Other/Not answered labels, Submit/Cancel actions, and the navigation hints render via i18n.t('reverseRpc.question.*').
  • Approval preview. Title and footer hints render via i18n.t('reverseRpc.preview.*').
  • Strings stay colored through currentTheme; no chalk named colors, and no printableChar() / keyboard behavior changes. Locale-render tests assert the panels show Chinese under zh-CN and English under en.

Slice 4 — slash-command descriptions & /help panel (#4)

Extends the translation layer to the command surface, so command discovery reads in the user's language.

  • commands namespace. New per-locale locales/{en,zh-CN}/commands.ts packs covering builtin command descriptions, the /help panel chrome, and /goal + /swarm argument-completion descriptions, merged into each locale index.ts.
  • Command descriptions. localizedBuiltinSlashCommands() resolves each builtin's description via i18n.t('commands.descriptions.<name>') at render time; the static registry strings remain the English source of truth. kimi-tui feeds the autocomplete provider and the /help panel from it, so both follow the active locale. Skill-provided descriptions stay owned by the skill — only framework builtins are localized.
  • /help panel. Title, dismiss hint, greeting, the "Keyboard shortcuts" / "Slash commands" section headers, every default keyboard-shortcut description, and the "showing X-Y of Z" scroll tail render via i18n.t('commands.help.*').
  • Argument completions. /goal and /swarm subcommand descriptions render via i18n.t('commands.args.*').
  • Command names, aliases, subcommand values, and keyboard-shortcut keys (Shift-Tab, Ctrl-G, …) stay untranslated — only human-readable descriptions change. Locale-render tests assert the /help panel and command descriptions show Chinese under zh-CN and English under en.

Slice 5 — usage/status panel & chrome label translation (#5)

Extends the translation layer to the /usage and /status panels, finishing the stats/chrome surface the footer began.

  • usage + status namespaces. The components pack is extended with components.usage.* (session/context/plan section labels, the input/output/total words, % used, and empty-state strings) and components.status.* (field labels and rendered values), in both en and zh-CN.
  • Usage panel. buildUsageReportLines / the shared managed-usage section render "Session usage", "Context window", "Plan usage", the per-model row words, and the empty states via i18n.t('components.usage.*').
  • Status panel. Field labels (Model, Directory, Permissions, Plan mode, Session, Title, Warning) and the values they render (on/off, not set, none, the thinking suffix) come from i18n.t('components.status.*').
  • Chrome titles. The bordered panel titles (Usage / Status) are localized. Model names and permission-mode identifiers stay untranslated — only the surrounding labels change.
  • Double-width alignment. A new padEndToWidth helper pads the aligned label columns by display width instead of code-point count, so the fixed-width panels stay aligned and never overflow under double-width Chinese characters. Locale-render tests assert both panels show Chinese under zh-CN and English under en, plus column alignment and width safety under zh-CN.

Slice 6 — welcome/onboarding & shared dialog labels (#6)

Translates the first-run welcome surface and introduces a shared common namespace for the short phrases that recur across dialogs.

  • Welcome panel. The title, the logged-out "Run /login or /provider to get started." / logged-in "Send /help for help information." hints, the "not set" model notice, and the Directory/Session/Model/Version/MCP info labels render via i18n.t('components.welcome.*') in both render paths (wide box + narrow fallback). The info-label column is now padded by display width, so it stays aligned under double-width Chinese labels.
  • common namespace. New per-locale locales/{en,zh-CN}/common.ts packs holding the generic dialog-control vocabulary (common.submit / cancel / back) plus the keyboard-hint phrases (common.hints.navigate / select / cancel / clearSearch) that recur in list-style dialog footers.
  • De-duplicated dialog hints. The model, choice, session, undo, and experiments selectors now compose their footer hints from common.hints.*, and the plugin-remove confirmation's Cancel button uses common.cancel — so each shared phrase is translated once and reused, rather than duplicated per component.
  • Strings stay colored through currentTheme. Locale-render tests assert the welcome surface and the shared dialog hints show Chinese under zh-CN and English under en.

Slice 7 — CLI config-parse notice translation (#7)

Optional phase-1 tail: routes the highest-value CLI-facing message — the tui.toml config-parse failure notice — through the i18n layer, so the CLI startup notice matches the language the TUI renders in.

  • cli namespace. New per-locale locales/{en,zh-CN}/cli.ts packs holding cli.config.invalidTuiConfig, merged into each locale index.ts. The en entry stays in sync with INVALID_TUI_CONFIG_MESSAGE.
  • Deferred init. Config load runs before the language is known, so run-shell now records only that parsing failed and translates the notice after i18n.setLocale(...) resolves the locale — falling back to the default en pack before config load.
  • English logs. TuiConfigParseError.message stays English so logs and issue triage remain English; only the user-facing startup notice is localized.
  • Tests assert the config-parse notice renders Chinese under zh-CN and English under en, and a guard test keeps the en pack in sync with INVALID_TUI_CONFIG_MESSAGE. Full translation of every CLI subcommand's help stays out of scope for phase 1.

All slices built test-first, one behavior at a time.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

Introduce a zero-dependency I18n singleton (lookup, en fallback,
param interpolation, runtime setLocale) modeled on the theme
singleton, with per-module language packs under locales/en and
locales/zh-CN. Add a language setting to tui.toml (auto | en |
zh-CN), resolve auto from the system locale at startup, and
migrate the footer status labels to translated strings.
@meymchen meymchen marked this pull request as draft June 16, 2026 23:01
Build the interactive language-switching path on top of the i18n
skeleton (#1). A LanguageSelectorComponent (ChoicePicker subclass)
offers Auto / English / 简体中文; /settings routes a new "Language"
entry to it. Selecting a language persists to tui.toml, updates app
state, flips the live i18n locale, and repaints so the UI switches
language immediately without a restart. /reload re-reads the language
and re-applies it via setAppState + i18n.setLocale alongside theme.

Closes #2

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@meymchen meymchen changed the title feat(tui): i18n walking skeleton — language config + footer translation feat(tui): TUI i18n — language config, footer translation, and /settings language selector Jun 16, 2026
meymchen and others added 2 commits June 17, 2026 07:45
Migrate the user-visible English strings in the reverse-RPC approval and
question panels to the i18n layer, with en + zh-CN translations under a new
`reverseRpc` namespace.

- Approval panel headers, choice buttons, danger labels, cwd/scope/more-lines
  metadata, and key hints now resolve via `i18n.t('reverseRpc.approval.*')`.
- Choice/danger labels are localized in the approval adapter at adapt time;
  `selected_label` stays a stable English identifier for the upstream contract.
- Question dialog heading, review/submit prompts, Other/Not-answered labels,
  submit actions, and hints resolve via `i18n.t('reverseRpc.question.*')`.
- Approval preview title and footer hints resolve via
  `i18n.t('reverseRpc.preview.*')`.
- Strings stay colored via `currentTheme`; no chalk named colors, and no
  coloring / printableChar() / keyboard behavior changes.

Locale-render tests assert the panels render in Simplified Chinese under
`zh-CN` and English under `en`.

Resolves #3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate builtin slash-command descriptions, the /help panel chrome
(title, dismiss hint, greeting, section headers, keyboard-shortcut
descriptions, scroll tail), and goal/swarm argument-completion
descriptions to the i18n layer, with en + zh-CN translations under a
new `commands` namespace.

Command names, aliases, and subcommand values stay untranslated —
only human-readable descriptions follow the active locale. Skill
command descriptions remain owned by the skill; only framework
builtins are localized via `localizedBuiltinSlashCommands()`.

Closes #4

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@meymchen meymchen changed the title feat(tui): TUI i18n — language config, footer translation, and /settings language selector feat(tui): TUI i18n — config, footer, /settings selector, reverse-RPC & command/help translation Jun 17, 2026
meymchen and others added 6 commits June 17, 2026 22:55
…keleton

# Conflicts:
#	apps/kimi-code/package.json
Migrate the `/usage` and `/status` panel labels to the i18n layer with
`en` + `zh-CN` translations, extending the `components` namespace with
`usage` and `status`.

- usage-panel: session/context/plan section labels, input/output/total
  words, "% used", and empty-state strings now come from
  `i18n.t('components.usage.*')`.
- status-panel: field labels (Model, Directory, Permissions, Plan mode,
  Session, Title, Warning) and rendered values (on/off, not set, none,
  thinking) come from `i18n.t('components.status.*')`.
- Panel chrome titles (' Usage ' / ' Status ') are localized; model names
  and permission-mode identifiers stay untranslated.
- Add a `padEndToWidth` helper so the aligned label columns pad by display
  width, keeping the panels aligned under double-width Chinese characters.

Locale-render assertions verify both panels render Chinese under `zh-CN`
and English under `en`, plus alignment/width under `zh-CN`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate the welcome panel to the i18n layer (title, login/help hints,
unset-model notice, and the Directory/Session/Model/Version/MCP info
labels), padding the label column by display width so it stays aligned
under double-width Chinese.

Introduce a `common` namespace for short phrases shared across
components: the generic Submit/Cancel/Back controls plus the
navigate/select/cancel/clear-search key hints. Migrate the list-style
dialogs (model, choice, session, undo, experiments selectors) and the
plugin-remove Cancel button to `common.*` so each shared phrase is
translated once, with en + zh-CN translations.

Resolve #6

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render the `tui.toml` parse-failure notice (INVALID_TUI_CONFIG_MESSAGE)
in the active locale. Config load runs before the language is known, so
run-shell now records only that parsing failed and translates the notice
after `i18n.setLocale(...)` resolves the locale — deferred init that
falls back to the default `en` pack before config load.

The `TuiConfigParseError.message` stays English so logs and issue triage
remain English; only the user-facing startup notice is localized.

Adds a `cli` i18n namespace (`cli.config.invalidTuiConfig`) for en and
zh-CN. Closes #7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment