Skip to content

feat(terminal): localize settings option chips via display-label catalog#8

Merged
pro-vi merged 7 commits into
feat/languagefrom
feat/settings-option-labels
Jun 10, 2026
Merged

feat(terminal): localize settings option chips via display-label catalog#8
pro-vi merged 7 commits into
feat/languagefrom
feat/settings-option-labels

Conversation

@pro-vi

@pro-vi pro-vi commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Localizes the Settings scene's option-value chips (font, cast method, cast mode, taijitu, glyph animation) for zh-Hant/zh-Hans through a token-keyed display-label catalog, while keeping stored config tokens, CLI stdout, and the config parse surface byte-identical. Implements the policy the glossary already sanctioned ("display labels for enum values are a SEPARATE catalog layer that never mutates the stored token") and removes the mixed-register zh Settings screen (CJK row labels above English chips).

Stacked on #5 (feat/language) — merge after it.

Key Changes

Features

  • Option-label catalog (packages/terminal/src/i18n/option-labels.ts): optionLabel(lang, settingKey, token) — token-keyed Map (prototype-chain reaches miss by construction), total resolution with canonical-token fallback, zh-Hans authored explicitly. Not routed through tr()MessageKey is a literal union; widening it to dynamic keys would break the exhaustive-catalog contract.
  • Localized chips: font 楷體/隸變/黑體; cast method 銅錢 (coin)/蓍草 (yarrow) — canonical token rides in the label as the bridge back to the CLI value; cast mode 自動/手動; taijitu 點陣/密實; anim 噪點/點陣/放射/沙化. en mode unchanged. Labels derive from the live language selection (mid-screen flip re-labels same frame).
  • Glossary + policy matrix (docs/language-glossary.md, TEXT_SURFACES.md): token-keyed ratification table (噪點=noise / 點陣=dots transposition hazard cross-referenced), theme labels recorded as deferred (brand-vs-literal ruling pending), input-alias policy: none.

Refactoring

  • Identity-bound getValues(): SettingRow now carries canonical token arrays (values); field↔row mapping goes through the row key via selectedValue() instead of this.rows[0]this.rows[6] positional indexing — reordering rows can no longer silently swap persisted values. Pre-existing two-way coupling (display-string→canonical by index AND field→row by position) is fully removed; no casts introduced.

Tests / Verifier

  • Behavioral enforcement (scripts/verify-language-surfaces.ts --terminal): full ratified (settingKey, token) → 繁/简 contract checked through optionLabel(); en = canonical token pinned; theme verbatim-fallback pinned; prototype-chain miss pinned; representative label sentinels wired to the inventory row. Deleting a shipped entry reds the verifier (proven by mutation during development).
  • Scene tests: per-wave chip rendering in all three languages, Simplified-residue guards, width budget under the en theme row, persistence round-trips, white-box row-reorder test locking the identity-bound contract. Re-pinned the "option-value badges remain verbatim" comment to the new policy. "[EN] 繁 简" endonym assertion preserved verbatim.

Technical Context

  • Two label mechanisms coexist deliberately: LANGUAGE_LABELS (EN/繁/简) are endonym badges, invariant across display languages, heavily pinned by fixture rows + sentinels — untouched. OPTION_LABELS is the per-display-language layer for the other rows.
  • No parser widening: translated chips do NOT get config-file aliases; config set and hand-edited files stay canonical-only (recorded in glossary + scope boundary). The TUI↔CLI vocabulary bridge is the in-label canonical hint, not an alias table.
  • Theme chips stay verbatim until the glossary records whether ink/bone/cinnabar/jade/river are literal words or palette brand names (deferred follow-up).
  • Plan: docs/plans/2026-06-10-001-feat-settings-option-label-localization-plan.md (origin: PR feat: add language-aware detail view #5 review session + cross-model second opinion).

Testing Scenarios

  • zh-Hant renders 楷體/銅錢 (coin)/自動/點陣/噪點 chips; zh-Hans renders Simplified forms; en unchanged (589 tests pass)
  • Persistence: zh-mode selections write canonical tokens; config get castMethod chain pinned by existing CLI tests
  • Live language flip re-labels chips same frame, values untouched
  • Row reorder cannot swap persisted values (white-box contract test)
  • Prototype-chain token ("constructor") falls back, never resolves inherited members
  • bun run typecheck, full suite, and verify-language-surfaces run-all: PASS — 0 issues

Generated with: Claude Code

QA checklist

All items verified fresh at ff0ffd8 on feat/settings-option-labels. TUI adaptation note: this is a terminal app — "screenshots" below are rendered CellBuffer dumps (80×24, the scene's real render path), captured by a reproducible script (bun one-shot rendering SettingsScene; commands inline). No browser/visual-proof manifests apply; provenance = git HEAD above + the capture commands.

  • R1 — zh modes render localized chips; en unchanged — full-scene dumps per language; only theme chips remain English (deferred by R5):

    ──── settings @80x24 language=zh-Hant ────
                    主題
                    > ink  [bone]  cinnabar  jade  river
                    語言
                      EN  [繁]  简
                    太極圖
                      [點陣]  密實
                    字形動畫
                      [點陣]  噪點  放射  沙化
                    字體
                      [楷體]  隸變  黑體
                    起卦法
                      [銅錢 (coin)]  蓍草 (yarrow)
                    起卦模式
                      [自動]  手動
    ──── settings @80x24 language=zh-Hans ────
                    字体
                      [楷体]  隶变  黑体
                    起卦法
                      [铜钱 (coin)]  蓍草 (yarrow)
                    起卦模式
                      [自动]  手动
    ──── settings @80x24 language=en (unchanged) ────
                    Font
                      [kaiti]  libian  heiti
                    Cast Method
                      [coin]  yarrow
    

    (zh-Hans block abridged to the wave rows; en block abridged — full dumps reproduce via the capture script.)

  • R2 — persisted tokens, CLI stdout, and parse surface unchanged; no new aliases — live CLI round-trip on a fresh --data-dir with language=zh-Hant:

    $ iching config get castMethod
    yarrow
    $ cat config.json   → "castMethod": "yarrow"  (canonical tokens only)
    $ iching config set castMethod 蓍草
    Invalid value "蓍草" for castMethod. Valid: coin, yarrow   (exit 1 — no parser widening)
    
  • R3 — labels resolve from the live language selection — start in en, focus Language row, to 繁: chips re-label in the same rendered frame, values untouched:

    ──── live flip en→zh-Hant (no save) ────
                    字體
                      [楷體]  隸變  黑體
    getValues(): { "glyphFont": "kaiti", "castMethod": "coin", ... }  (canonical)
    

    Locked by test: "flipping Language re-labels font chips immediately, before save".

  • R4 — missing label falls back to canonical token, never crashes/persists — theme chips render ink bone cinnabar jade river verbatim in zh dumps above (empty-entry fallback, live); prototype-chain tokens (constructor, __proto__, toString…) pinned to miss by option-labels.test.ts and the verifier's contract check.

  • R5 — theme chips verbatim pending glossary ruling — visible in the zh-Hant dump (主題 row shows English tokens); deferral + unratified candidates recorded in docs/language-glossary.md §Settings option-chip display labels and TEXT_SURFACES.md term-theme-names.

  • R6 — policy artifacts re-pinned to "tokens canonical; labels via catalog"scene-language.test.ts comment re-pinned; "[EN] 繁 简" assertion preserved; verifier --terminal now enforces the full ratified label contract behaviorally. Mutation red-proof during U5: deleting the noise entry → FAIL — option label settings.glyphAnimation.noise: zh-Hant != ratified "噪點".

  • Automated — fresh runs at ff0ffd8:

    bun run test       → 589 pass, 0 fail (61 files)
    bun run typecheck  → clean
    bun scripts/verify-language-surfaces.ts → run-all (AC-009 final) PASS — 0 issues
    

pro-vi added 7 commits June 10, 2026 00:31
…trix (U1)

Token-keyed renderings for font/castMethod/castMode/taijitu/anim chips
(zh-Hant + zh-Hans authored explicitly). Theme labels recorded as deferred
pending brand-vs-literal ruling. Input-alias policy: none. New surface row
term-settings-option-chips + matching policy-matrix entry.
…(U2)

OPTION_LABELS Map catalog (ships empty — behavior-neutral) with total
optionLabel() fallback to the canonical token; SettingRow now carries
canonical values, chip labels derived per render via chipLabel();
getValues() maps field→row by key, killing the positional-index drift
(reordering rows can no longer swap persisted values). Language row's
endonym badges untouched.
First ratified wave of the option-label catalog. zh-Hant/zh-Hans authored
per glossary; en keeps canonical pinyin tokens; persistence and live
language flip locked by tests.
銅錢 (coin)/蓍草 (yarrow) carry the canonical token as an in-label hint
(no transliteration bridge to the CLI value); 自動/手動, 點陣/密實, and
噪點/點陣/放射/沙化 are literal label-only renderings. Token-keyed per
glossary (噪點 = noise, 點陣 = dots, ratified once for both usages).
Width budget pinned under the en theme row.
--terminal now asserts the full ratified (settingKey, token) → 繁/简
contract behaviorally via optionLabel(), pins en = canonical token,
theme verbatim fallback (deferred), and prototype-chain misses; adds
representative label sentinels wired to the inventory row. Deleting a
shipped entry reds the verifier (proven by mutation).
Name the role (find the row owning the key, narrow its selected token),
not the category.
@pro-vi pro-vi merged commit 5e8af78 into feat/language Jun 10, 2026
@pro-vi pro-vi deleted the feat/settings-option-labels branch June 10, 2026 07:48
pro-vi added a commit that referenced this pull request Jun 10, 2026
The settings option-label localization work shipped in #8; its plan doc
(status: completed) no longer needs to live in the tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pro-vi added a commit that referenced this pull request Jun 20, 2026
…itle overlap (review #3/#8/#9)

Three cast-scene findings from the independent review:

- #3 (P2): [s] skip called skipToComplete() with animate defaulting true, so
  setFocusedHex re-seeded a fresh glyph animator (glyphAnimDone=false) and the
  central glyph noisily re-played its whole reveal instead of settling. Pass
  animate:false — [s] is the one caller that wants the static end-state.
- #8 (P3): update() derived its delta from the loop's UNCLAMPED `elapsed`. While
  the terminal is below the size floor the loop skips update() but `elapsed`
  keeps growing, so the first restored frame credited the entire hidden gap —
  fast-forwarding past the reveal, ×2/×4 at pace. Advance by the clamped per-frame
  dt instead (as the yarrow scene does).
- #9 (P3): in centered layout (narrow, < MIN_SPLIT_WIDTH, no glyph) the standalone
  "→ becoming" title renders a few rows below the primary — where the settled
  reading panel stacks down — garbling on tall-narrow terminals. Suppress it while
  the reading is showing (the reading carries the becoming).

Tests: clamped-clock advance (#8, mutation-verified), [s] settles the glyph
(#3, mutation-verified), becoming title suppressed-when-shown / present-when-
hidden (#9). The #8 change means a single big update() no longer fast-forwards,
so the test settle-helpers now use skipToComplete(false) or clamped-dt loops
(the buggy shortcut they relied on is exactly what #8 removes).

Gates: 1336 tests pass, typecheck clean, language-surface PASS, smoke 5/5.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant