Skip to content

Add multi-language UI support (zh-CN first) 「using deepseek-V4.1-flash」 - #411

Merged
devmobasa merged 11 commits into
devmobasa:mainfrom
weimona:main
Oct 17, 2026
Merged

devmobasa merged 11 commits into
devmobasa:mainfrom
weimona:main

Conversation

@weimona

@weimona weimona commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Wrap every user-visible string in the overlay and the configurator with tr()/trf(), make the UI language selectable at runtime, and ship a complete Simplified Chinese catalog.

Core:

  • src/i18n.rs: process-global language state, tr()/trf() lookup, and locale resolution (WAYSCRIBER_LANGUAGE > [ui] language > desktop locale > English)
  • src/domain/language.rs: Language enum with serde spellings
  • locales/zh-CN.toml: 2247 entries, embedded via include_str!
  • config: [ui] language = "auto" | "en" | "zh-CN"

Coverage:

  • overlay: help overlay, context menus, board picker, status bar, toasts, command palette chrome, properties panel, about window, onboarding hints, top-toolbar menus (wayland + GTK), input HUD, arrow-style labels, navigation hints, all action labels
  • wayscriber-configurator: shell, sidebar tabs, every settings page, dialogs, and status messages; it loads the language at startup and again when the Language row changes, so its chrome follows the overlay

Guards and tests:

  • tests/repository_guards/translations.rs: every tr()/trf() literal in src/ and configurator/src/ must have a catalog entry (call sites are reported); a value may not repeat its key; placeholder counts must match
  • tests/i18n.rs: catalog loading, override, and detection
  • configurator suite: 519 pass; full lib suite unchanged apart from pre-existing/environment failures

Deliberately untranslated: --help/--version, log messages, keyboard key names, TOML config keys shown in validation errors, gsettings/systemctl command arguments, CSS class names, and product names.

@devmobasa

Copy link
Copy Markdown
Owner

Thanks for the contribution.

I noticed that the commit lists me as both author and committer, although you submitted it from your fork. Could you explain how that happened and correct both fields to your own identity?

Wrap every user-visible string in the overlay and the configurator with
tr()/trf(), make the UI language selectable at runtime, and ship a
complete Simplified Chinese catalog.

Core:
- src/i18n.rs: process-global language state, tr()/trf() lookup, and
  locale resolution (WAYSCRIBER_LANGUAGE > [ui] language > desktop
  locale > English)
- src/domain/language.rs: Language enum with serde spellings
- locales/zh-CN.toml: 2247 entries, embedded via include_str!
- config: [ui] language = "auto" | "en" | "zh-CN"

Coverage:
- overlay: help overlay, context menus, board picker, status bar,
  toasts, command palette chrome, properties panel, about window,
  onboarding hints, top-toolbar menus (wayland + GTK), input HUD,
  arrow-style labels, navigation hints, all action labels
- wayscriber-configurator: shell, sidebar tabs, every settings page,
  dialogs, and status messages; it loads the language at startup and
  again when the Language row changes, so its chrome follows the
  overlay

Guards and tests:
- tests/repository_guards/translations.rs: every tr()/trf() literal in
  src/ and configurator/src/ must have a catalog entry (call sites are
  reported); a value may not repeat its key; placeholder counts must
  match
- tests/i18n.rs: catalog loading, override, and detection
- configurator suite: 519 pass; full lib suite unchanged apart from
  pre-existing/environment failures

Deliberately untranslated: --help/--version, log messages, keyboard key
names, TOML config keys shown in validation errors, gsettings/systemctl
command arguments, CSS class names, and product names.
@weimona

weimona commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

I am very sorry for the inconvenience caused. I have corrected and resubmitted the pull request.

What happened: the machine I committed from had no git identity configured at all (git could only auto-detect root@archlinux.(none)). The only remote this clone had at that point was origin = devmobasa/wayscriber — the upstream I had forked from — so AI filled the identity in from that remote's name while editing. That was a bad inference on my part. The author and committer fields both come from the same user.name/user.email pair, so both got stamped with it. Because devmobasa@users.noreply.github.com is a valid noreply address for your account, GitHub linked the commit to you instead of surfacing it as an unassociated string, which is why it read as you being the submitter.

@devmobasa

Copy link
Copy Markdown
Owner

Thanks! I was just very confused.

I am looking into the changes btw.

The language-support commit left 108 files unformatted, so the format
check failed, and strict Clippy stopped on a collapsible if in the toolbar
definitions, needless lifetimes on the catalog lookup, and two needless
borrows in the shortcut recorder prompts. This is rustfmt's output plus
those four mechanical fixes; behavior is unchanged.
The catalogs load once per process, with whatever override file the
runner has installed, and WAYSCRIBER_LANGUAGE beats every configured
language, so a test that asserts Chinese text, or one that selects
English, depended on the machine running it.

Core tests that assert embedded Chinese now run in a fresh copy of the
test binary with the override, the locale variables and the override
directory cleared. The i18n integration test points the override
directory at an empty one. with_temp_config_home clears the override for
the config it loads and restores the process language afterwards, since
loading a config at startup selects its language.

The configurator, which links the core without its test-only locale
neutrality, clears those variables once per test process, before any
test takes the environment lock, and gains the same child runner for
tests that select a language other than English.
Several places used displayed text as an identifier or read it to decide
what to do, so they broke once that text was translated:

- The configurator named its GtkStack pages after their translated tab
  titles. The names were registered in the startup language and looked up
  in the current one, so after a language change the sidebar and the UI
  sub-tabs stopped switching pages. Tabs now have fixed stack names.
- The arrow-style menu built its widget IDs from the translated style
  label, and the layout menu from the translated preset label, in both
  toolbars ("top.arrow-style.pointy" became "top.arrow-style.尖头"). Both
  now derive the ID from the typed value.
- The session catalog cleared its loading notice only when the status
  contained the English "Loading sessions", which never matched in
  Chinese. The notice is now a typed status that completion recognizes in
  any language, and that re-renders when the language changes.
- The background-setup panel chose its failure styling by searching the
  feedback for "failed" or "error", so translated failures looked like
  successes. The feedback is now typed and carries its outcome, including
  a light-control setup whose Hyprland reload failed after the controls
  were written.
- The eraser tooltip compared the slider label with a translation; it now
  reads a typed flag.
…er language

A local catalog override was read with read_to_string on the first
translated lookup: a FIFO named zh-CN.toml blocked that lookup until a
writer appeared, a large file was read whole, and a symlink was followed.
Overrides are now opened without following symlinks and without
blocking, checked on the opened descriptor to be a regular file, and read
up to 1 MiB. A file that fails those checks, or is not UTF-8 or TOML, is
ignored with one log message and the embedded catalog is kept; an entry
whose {} placeholders differ from its key is skipped. prepare_catalogs()
loads the catalogs ahead of the first lookup for processes that want that
file work done early.

The toolbar definitions cached only the last language and leaked a new
table with Box::leak on every switch. Each language now has one table,
built once from that language through i18n::lookup, so a table never
mixes two languages and switching back reuses it.
… window

The About window is its own process, started with --about, and read the
config only for its theme, so its text stayed English whatever
[ui] language said. The daemon never selected a language either, so its
tray menu, portal shortcut descriptions and update notices stayed
English; an overlay it starts is a separate process and cannot set the
daemon's language.

i18n::initialize() selects a language and loads its catalog before the
first surface is built. The overlay calls it where it already selected
its language, the About window calls it with the language from the config
read it makes for its theme, and the daemon reads the saved language once,
before its tray, shortcut and update-notice workers start, whichever are
enabled. A config the daemon cannot read keeps the automatic default.
Background mode picks up a change when it restarts.
The catalog guard read only a string literal that followed tr( or trf( on
the same line, so the calls rustfmt wraps onto the next line were never
checked, and a call whose argument was a constant or an expression was
skipped without a word. Three strings had no catalog entry as a result:
"Light Mode drawing", "Light Mode passthrough" and "Path copied". The
guard compared only the {} counts in the catalog, so a template spelling
{min} and {max} passed while trf left those words in the message, as the
whole-number range error did.

The guard now reads plain and raw literals wherever they open the call,
across lines, and trf(tr("…"), &[…]). Any other argument fails, except in
the action accessors, whose metadata the guard audits directly, now
including the compact labels action_display_label derives. A trf template
must spell only {} and pass as many values as it has placeholders.

The three calls now spell their literals and have Chinese entries, the
range message uses two {} slots, and the trf documentation states that
values fill placeholders in order, so a translation can move them but not
swap two.
Picking a language set the process language, but every label already on
screen kept its old text, so only dialogs opened afterwards changed. A
reload, Defaults or a finished save replaced the draft without applying
its language, which left the window in the language of the previous
draft. Startup read the config a second time, synchronously, just to pick
the language before the window was built.

The draft's language is now applied on every transition that replaces
the draft, and by the Language row, before the status text for that
transition is built. When the language changes, the shell rebuilds its
pages from the model and retitles the persistent chrome in place. The
model owns everything typed or selected, so nothing entered is lost; the
rebuild carries focus, the caret and selection, and the visible page's
scroll position across, and editor popovers closed by the rebuild no
longer cancel their edits. The window opens in English and switches when
the first load arrives, and that load job prepares every catalog off the
GTK thread.

Text the configurator stored for display was translated once and kept:
the "Never" of a catalog entry, the shortcut recorder's prompt, the
claimants of a pending shortcut conflict. Those now store what they
report and are rendered when drawn. The language list names each
language in itself, so a preview the user cannot read can still be undone,
and search matches the English terms in every language as well as tab,
sub-tab and section titles as displayed.

A GTK test runs the real window on the private headless compositor and
checks a language switch with open editors, focus, caret, scroll and
navigation; both GTK gates require its marker.
Much of the configurator still drew English in Chinese: row titles and
hints, buttons, accessibility labels, placeholders, validation errors,
status messages such as the shortcut reset and conflict-review notices,
the session card's Created, Opened and Saved prefixes, and the
background-setup status lines. They are now marked where they are drawn,
with format! text turned into trf templates and displayed constants
written as literals so the catalog guard can read them, and the Chinese
catalog gains an entry for each new key.

Values written into config or other programs stay as they were: default
board and profile names, the GNOME shortcut name and default quick-color
labels.
docs/I18N.md still said the configurator was untranslated, that the
language was set once at startup, and that a translation could reorder
the {} placeholders; the guard rules and the steps for adding a language
were incomplete. It now covers which processes draw translated text and
when each picks up a change, the configurator preview, how auto resolves
in a systemd user service, the fixed placeholder order, the guard's call
forms and its derived-label audit, the limits on local catalog overrides,
search under translation, and every place a new language has to be added.

CONFIG.md, config.example.toml and the catalogs README state the same
timing, override and placeholder rules.
`--language CODE` sets the interface language for one run of --active,
--freeze, --daemon or --about without changing config.toml. It accepts
the spellings `[ui] language` does and takes precedence over both
WAYSCRIBER_LANGUAGE and the config. A daemon started with it gives it to
the About windows it opens, and to its overlays as an optional
environment input that an older overlay ignores. The process broker
strips that input from every other helper, so a configurator opened from
an overlay keeps previewing its own Language row. With --daemon-toggle or
an overlay action the flag is an error, since a running daemon's
overlays cannot be changed from outside.

`--list-languages` prints each language's code and name, the configured
language, and the language in effect with the setting that decided it.

Languages now carry their native and English names, and the
configurator's Language row takes its labels from them. The help text,
README, CONFIG.md, I18N.md and config.example.toml describe both options.
@devmobasa

Copy link
Copy Markdown
Owner

Thank you again, @weimona, for this PR. Multi-language support is a big feature, and your work gave a strong base for it.
I am very grateful. 🙏

I added my commits on top of your branch. I did not change your commit. I plan to merge with a merge commit, so that your commit stays in the history with your name.

In my commits:

  • formatted the code and corrected the lint warnings.
  • made the language tests independent of the locale of the computer that runs them.
  • made identifiers (widget IDs, page names, action IDs) independent of the displayed text. The code no longer compares translated text to decide what to do.
  • added limits for local catalog files. Wayscriber ignores a file that is not a regular file, is larger than 1 MiB, is not UTF-8, or is not valid TOML.
  • kept a separate table of toolbar items for each language.
  • made background mode (the tray menu and notices) and the About window use the saved language.
  • changed the translation guard so that it reads every marked call, also a call on more than one line.
  • made the configurator show a language in the whole window when you select it. config.toml does not change until you click Save.
  • translated the configurator text that was still in English.
  • added two command-line options:
    • --language CODE uses a language for one run. It does not change config.toml.
    • --list-languages shows the languages and the language in use.
  • updated README.md, docs/CONFIG.md, docs/I18N.md and config.example.toml.

I also added 128 Chinese strings to locales/zh-CN.toml. Most of them are in 89ffe56. The other strings are in f6225ca, 996d4f1 and b0cd56b. You can see all of them in one view here: new Chinese strings. These strings did not get a review from a native speaker. If you have time, please read them and tell me what to correct.

Thank you again for your contribution!

@devmobasa
devmobasa merged commit c907eef into devmobasa:main Oct 17, 2026
3 checks passed
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.

2 participants