Skip to content

feat(webui): let the settings page switch between Chinese and English - #250

Open
stevenjj33 wants to merge 2 commits into
webuifrom
feat/webui-settings-language-switch
Open

stevenjj33 wants to merge 2 commits into
webuifrom
feat/webui-settings-language-switch

Conversation

@stevenjj33

@stevenjj33 stevenjj33 commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

What this does

The settings page's copy was hardcoded Chinese, and mavis-locale was read by a useState that had no setter — the key was there, the read was there, and nothing a user could do ever wrote a different value. The settings search matched a tab's name and key only, so asking it for 「语言」 answered 「没有匹配的设置」: the one question a language switch would exist to answer.

  • A 语言 / Language row in 通用 switches the locale, writes mavis-locale (the key the page already used — no second key), and the document's lang attribute follows from it.
  • Two packs — Chinese and English — in features/settings/projection/settings-locale.ts. Chinese is the base: a key the English pack does not carry falls back to the Chinese text rather than to a raw key.
  • The search reaches a row, not only a tab: it matches a row's own copy, and it matches both packs. A search that matched a row also opens the page that owns it and marks the row.

Chinese is the base language, so a fresh install opens in Chinese and the English pack is the opt-in. One consequence worth stating plainly: the old build wrote mavis-locale as "en" on every mount of the settings dialog, so anyone who has opened it before arrives on this build with "en" already stored and sees the settings page in English. The storage key and its values are unchanged, so nothing is lost and clearing that one key returns the page to Chinese.

No new dependency.

The boundary with #33, and the scope of this round

#33 is merged, and its own Scope section says why the i18n work is not in it:

多语言 (P-3) is deliberately not here: 8604 CJK characters across 77 of 109 client files, with mavis-locale currently read by a useState that has no setter. That is a full extraction and wants its own PR.

So nothing here repeats #33: #33 never carried the i18n work, and this PR does the part of it that is small enough to be correct — an entry point, a minimal pack, and a page that actually switches. The client-wide extraction is untouched and still wants its own change.

This round covers the settings page only. The conversation, the file panel, the workspace and the rest of the client keep their own copy. The other settings pages (浏览器, 用量与模型, 账户, 工作树) keep their literals too, and the search does not promise a row it cannot name. ADR 0025 records that boundary, and the rule for the next business that needs a switch: it copies the shape, it does not import the settings pack.

Also deliberately untouched: the 语音 / 快捷键 / 个性化 tabs keep their current state (nothing is unlocked here), and the runtime's reply language is a different system (packages/local-runtime/src/review/preparation.ts, responseLanguage: 'zh-CN' | 'en') that this change does not touch.

Verification

Unit

packages/webui/test/unit/settings-locale.test.ts (new, registered in test/vitest-suites.json):

  • the two packs carry the same keys, and no value is empty or still an untranslated placeholder;
  • t() reads each language, falls back to the Chinese text when a language does not carry a key, and falls back to the key itself when neither does;
  • a stored value resolves to the language it names, and an unrecognised one to Chinese;
  • the search finds the language row by 「语言」 and by "Language", in either UI language, still finds the tabs by their names, and still finds nothing for a name that is not there.

Browser (the built client)

packages/webui/test/browser/settings-language.spec.mjs (new), run with pnpm build:webui and Playwright's pinned Chromium on macOS 27.0.1 (arm64), Playwright's default profile (locale: zh-CN, timezone: Asia/Shanghai, colorScheme: light), one fresh browser context per test, against the built client served by test/browser/server.mjs.

What it did, and what it saw:

  1. Clicked the user-menu trigger, clicked 设置, selected English in the 语言 row. The sidebar's group heading changed 偏好 → Preferences, the tab changed 通用 → General, the row's own title changed 语言 → Language, <html lang> changed zh-CN → en, and localStorage["mavis-locale"] became "en" — with no reload.
  2. Clicked the back button to close the dialog, clicked the user-menu trigger and 设置 again: the dialog came up in English, and stayed in English.
  3. Typed 「语言」 into the settings search: the sidebar kept the 通用 tab and the 语言 row was marked (data-search-hit="true"), instead of the sidebar answering 没有匹配的设置. The same row was found by typing "Language".

3 passed. The existing settings specs pass too — settings-theme.spec.mjs and settings-account-tab.spec.mjs, 12 passed — which matters because the theme spec drives the buttons by their Chinese labels.

Negative control: the same specs are red before the fix

Built the pre-fix tree webui @ 491569cd (this PR's base) in a separate worktree and ran the same two files against it:

  • the browser spec: 3 failed — the 语言 row does not exist (element(s) not found on [data-testid="mavis-settings-language-row"]), the select cannot be used (timeout), and searching 「语言」 finds no 通用 tab;
  • the unit spec: fails to import settings-locale.js, because that module is what this change adds.

And the substantive assertion, written against the pre-fix API only (filterSettingsTabs with no locale), is red on its own:

AssertionError: expected [] to include 'desktop'

A suite that was already green before the fix would have been an empty test. This one is not.

The Linux visual baselines

The 通用 page gains a row, so the settings dialog's Linux Chromium baseline changes: everything below the 应用 section shifts down. The baseline was regenerated on the gate's Linux runner with the gate's CJK fonts installed, as packages/webui/docs/verification/browser-suite.md requires, and the regeneration ran twice and produced byte-identical images.

Three plugin-surface baselines were refreshed in the same run, and that was measured rather than assumed. This branch does not touch the plugin, Skill or MCP surfaces, yet the same differences appear with and without it on the same runner — 449 pixels in the marketplace and 122 in the MCP rows, all of it icon antialiasing a few pixels wide. The committed images no longer match what the gate renders, so every PR on this branch would have been red for a reason that predates it.

Gates

pnpm verify on macOS: check:source, check:tsconfig, export source preview, test:release-tools, lint:tui, typecheck, build, check:standalone, typecheck:webui, check:webui-dependency (0 violations), build:webui, check:webui-boundary and test:webui-package all pass.

test:webui reports 215 files / 3346 tests passed, 0 failed (22 skipped), and pnpm verify --profile full passed 22 gates on darwin with nothing failing.

Two of those files are real-browser integration gates, and they need a Chrome on the machine: the isolated-browser provider looks for /Applications/Google Chrome.app/Contents/MacOS/Google Chrome (then Chromium, then Brave) and refuses to start without one. On a machine with no Chrome, browser-core-agent-actions.test.ts (5) fails with No Chrome executable was found; set browser.chromePath to the browser to use. and agent-transcript.test.ts (1) waits 30s for a browser-panel-frame that never arrives, because Chrome's network service cannot start and no screencast frame is ever produced. The same six fail identically on the clean base 491569cd, which is what identifies them as the machine's missing Chrome rather than a regression — and CI runners ship Chrome, so those files are green there.

One note on the frozen metadata: packages/webui/test/gates/fixtures/path-metadata-f00.json records every source path with its layer, role, business and home, so the new module is declared there (features/settings/projection/settings-locale.ts → view / feature-projection / settings / target).

CI on this pull request

Source verification is green on all three platforms (run 40108865569):

  • ubuntu-latest — pnpm verify --profile full → Verification passed: 22 gates on linux. The gate's test:webui-browser passed, and the refreshed Linux baselines matched on the runner that produced them — the settings dialog layout baseline and the plugin management skill and MCP rows layout baselines included.
  • macos-latest — pnpm verify --profile platform → passed.
  • windows-latest — pnpm verify --profile platform → passed.

The three new browser cases also run there, on Linux this time:

✓ 126 packages/webui/test/browser/settings-language.spec.mjs:78:1 › the language row switches the page and the document's language
✓ 127 packages/webui/test/browser/settings-language.spec.mjs:106:1 › the language choice survives the dialog being closed and opened again
✓ 128 packages/webui/test/browser/settings-language.spec.mjs:120:1 › the settings search finds the language row

The settings page's copy was hardcoded Chinese, and `mavis-locale` was read by
a `useState` that had no setter: the key was there, the read was there, and
nothing a user could do ever wrote a different value. The settings search
matched a tab's name and key only, so asking it for 「语言」 answered 「没有匹配的
设置」 — the one question a language switch would exist to answer.

The page now carries two packs, Chinese and English, in
`features/settings/projection/settings-locale.ts` — a view module, so it
reaches both the tab registry and the dialog without a new dependency edge and
without the platform layer reaching into a business. Chinese is the base: a
key the English pack does not carry falls back to the Chinese text rather than
to a raw key, and the English pack's type is the Chinese key set, so a missing
key is a compile error rather than a silent gap.

A 语言 row in the 通用 page switches the locale, writes the key the page
already used, and the document's `lang` attribute follows from it.

The search now matches a row's own copy as well as its tab's name, and it
matches both packs — a reader who looks for 「语言」 in an English page, or for
"Language" in a Chinese one, is looking for the same setting. A search that
matched a row also opens the page that owns it and marks the row.

Scope: the settings page only. The conversation, the file panel and the
workspace keep their own copy, and PR #33 deferred the client-wide extraction
for exactly that reason (ADR 0025).

Verified on macOS against the built client: the unit suite covers the pack
parity, the fallback and the search; a browser run selects English and watches
the sidebar and the 通用 page change without a reload, the `lang` attribute
follow, and the choice survive a close and reopen. The same specs are red
against `webui @ 63b9739`: the language row does not exist and the search
answers 「没有匹配的设置」.
@stevenjj33
stevenjj33 force-pushed the feat/webui-settings-language-switch branch from 1f796e8 to 6704ab1 Compare October 24, 2026 15:21
…environment

The settings dialog's baseline changes because this branch adds a 语言 row to
the 通用 page, which shifts everything below it.

The three plugin-surface baselines are refreshed alongside it because they no
longer match the gate's rendering — measured, not assumed: the base commit
`491569cd`, checked out on the same runner and rebuilt, fails the same
comparisons with the same pixel counts (449 for the marketplace, 122 for the
MCP rows, and 17455 for the settings dialog it should have matched). Two
independent runs of the regeneration produced byte-identical images, so the
gate is deterministic on that environment.

The baselines were regenerated on the gate's Linux runner, with the gate's CJK
fonts installed, as `packages/webui/docs/verification/browser-suite.md` asks.
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