docs(i18n): describe per-process languages, previews and override limits

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.
This commit is contained in:
devmobasa committed 2026-10-16 18:04:08 +02:00
1 parent 89ffe56e81
commit 98b6f54c15
4 files changed
+126 -33

No files matched your search

+5 -2
View File
@@ -415,10 +415,13 @@ theme = "auto"
# currently behaves like "off" (full motion).
reduced_motion = "auto"
# UI language for overlay chrome: help, toolbars, menus, toasts, and settings.
# UI language: overlay chrome (help, toolbars, menus, toasts, settings), the
# About window, background mode's tray and notices, and the configurator.
# Options: "auto", "en", "zh-CN". "auto" follows the desktop locale
# (WAYSCRIBER_LANGUAGE, then LANGUAGE/LC_ALL/LC_MESSAGES/LANG) and falls back
# to English when that locale has no translation.
# to English when that locale has no translation. Each process reads this when
# it starts: the overlay and About window on their next open, background mode
# after a restart.
language = "auto"
# Show the status bar and its configured contents
+4 -3
View File
@@ -958,9 +958,10 @@ theme = "auto"
# Options: "auto", "on", "off"
reduced_motion = "auto"
# UI language for overlay chrome
# UI language: overlay chrome, About window, background mode, configurator
# Options: "auto", "en", "zh-CN" ("auto" follows the desktop locale and falls
# back to English when that locale has no translation)
# back to English when that locale has no translation). Each process reads it
# when it starts; background mode needs a restart.
language = "auto"
# Show the status bar and its configured contents
@@ -1136,7 +1137,7 @@ to save it.
**Theme, Motion & Language:**
- **Theme**: `theme` selects the overlay chrome theme — `"auto"` (default), `"dark"`, or `"light"`. `"auto"` currently resolves to dark chrome. The command palette supports `"light"` throughout its search, results, shortcut editing, and tooltips. Other popups retain their complete dark palette until migrated.
- **Reduced motion**: `reduced_motion = "on"` disables overlay chrome animations (toast and flash fades render instantly; coverage extends to more surfaces as they adopt the shared animation envelopes). `"off"` keeps full motion. `"auto"` (default) is reserved for a future desktop-portal query of the system reduce-motion preference and currently behaves like `"off"` (full motion).
- **Language**: `language` selects the language of the overlay chrome — `"auto"` (default) follows the desktop locale, `"en"` forces English, and `"zh-CN"` forces Simplified Chinese. A locale this build has no catalog for falls back to English, so `"auto"` is always safe; `WAYSCRIBER_LANGUAGE` overrides the setting for one run. Canvas content you type or name stays exactly as you wrote it. See `docs/I18N.md` for the catalog format and the surfaces that are not translated yet.
- **Language**: `language` selects the interface language — overlay chrome, the About window, background mode's tray and notices, and the configurator. `"auto"` (default) follows the desktop locale, `"en"` forces English, and `"zh-CN"` forces Simplified Chinese. A locale this build has no catalog for falls back to English, so `"auto"` is always safe; `WAYSCRIBER_LANGUAGE` overrides the setting for one run. Picking a language in the configurator previews it there at once and is written on Save; the overlay and the About window use it the next time they open, and background mode after it restarts. `"auto"` follows each process's own locale, and a systemd user service can see a different one than your terminal. Canvas content you type or name stays exactly as you wrote it. See `docs/I18N.md` for the catalog format, local catalog overrides, and the surfaces that are not translated yet.
**UI Styling:**
- **Font sizes**: Customize text size for status bar and help overlay
+111 -26
View File
@@ -1,8 +1,10 @@
# Internationalization
wayscriber draws its overlay chrome in the language named by `[ui] language`.
English is the source language: UI strings are written in English in the code and
looked up in a per-language catalog. A string with no entry draws in English.
wayscriber draws its interface in the language named by `[ui] language`: the
overlay and its toolbars, the About window, the background daemon's tray menu,
shortcut descriptions and update notices, and the configurator. English is the
source language: UI strings are written in English in the code and looked up in
a per-language catalog. A string with no entry draws in English.
## Choosing a language
@@ -17,17 +19,43 @@ language = "auto"
today — leaves the slot empty and the search continues; English is the final
fallback, so `auto` never fails.
The configurator's Language row lists each language by its own name
("English", "简体中文"), so a language you cannot read can always be switched
back. Picking one previews it in the configurator window at once; nothing is
written until Save. Each other process reads the saved setting when it starts:
- the overlay, the next time it opens;
- the About window, the next time it opens;
- background mode (the daemon: tray menu, portal shortcut descriptions and
update notices), after it restarts.
`auto` resolves in each process from that process's own environment. Background
mode usually runs as a systemd user service, whose environment can differ from
a terminal's: `systemctl --user show-environment` shows what it sees, and a
locale set with `systemctl --user set-environment LANG=…` or in
`~/.config/environment.d/` applies after the service restarts.
Two environment variables help while testing a translation:
- `WAYSCRIBER_LANGUAGE=zh-CN` overrides `[ui] language` entirely, so a
translation can be checked without editing `config.toml`.
- `WAYSCRIBER_LANGUAGE=zh-CN` overrides `[ui] language` entirely, in every
process that sees it, so a translation can be checked without editing
`config.toml`.
- `WAYSCRIBER_LOCALE_DIR=…` names a directory of catalog files to merge over
the embedded ones, so a translation can be corrected without a rebuild.
Without that variable, a file in `$XDG_DATA_HOME/wayscriber/locales/<code>.toml`
(such as `~/.local/share/wayscriber/locales/zh-CN.toml`) merges over the
embedded catalog of the same code. Entries in the file win over the embedded
ones; the file may hold a subset.
ones; the file may hold a subset. A process reads the file once, so an edit
applies to processes started afterwards. The file is ignored, with one log
message, and the embedded catalog used instead, when it:
- is a symbolic link, a FIFO, or anything else that is not a regular file;
- is larger than 1 MiB (the embedded Chinese catalog is about 140 KB);
- is not UTF-8, or not valid TOML.
A single entry whose `{}` placeholders do not match its key is skipped, and
the embedded entry kept.
## Adding a string
@@ -40,6 +68,22 @@ title: tr("Boards"),
let caption = trf("{} of {} selected", &[&selected, &total]);
```
`trf` fills anonymous `{}` placeholders with its arguments in order: the first
`{}` in the translated text always receives the first argument. A translation
can place its placeholders anywhere in the sentence, but it cannot swap two
values, so phrase a template so that its values read in the same order in
every language. `{name}` and `{0}` are not placeholders; they stay literal, and
the guard rejects them in a template.
Write the string literal at the call: plain or raw, on one line or several.
The guard reads only that, so a constant or an expression as the argument
fails it. Code that builds a table for one language, rather than for the active
one, binds the lookup under the same name so each literal stays readable:
```rust
let tr = |source: &'static str| crate::i18n::lookup(language, source);
```
Then run the coverage guard:
```sh
@@ -59,27 +103,34 @@ are the English strings and only values are translated.
Rules the guard enforces:
- every `tr("…")`/`trf("…")` literal in `src/` has an entry in every catalog;
- every action's `label`, `short_label`, and `description` in
`src/config/action_meta/` has an entry too (the guard iterates
`action_meta_iter()`, so derived names such as the compact display labels are
covered by translating the underlying source strings);
- every `tr(…)`/`trf(…)` call in `src/` and `configurator/src/` spells a
string literal, and that literal has an entry in every catalog. The action
accessors in `src/config/action_meta/` are the one exemption, because they
translate metadata fields centrally;
- every action's `label`, `short_label` and `description`, and the compact
display label `action_display_label` derives from them (`Whiteboard` for
"Toggle Whiteboard Mode"), has an entry too. The guard reads the metadata
table and calls the production function, so a new action or a new derivation
is covered without a literal to point at;
- a `trf` template spells only `{}`, and its argument slice holds as many
values as the template has placeholders;
- a value is not identical to its key — an entry that repeats the key is dead
weight and fails;
- a value has the same number of `{}` placeholders as its key, so `trf` never
drops or misplaces a value. Order may differ, which is the point.
- a value has the same number of `{}` placeholders as its key.
Only literals can be marked. A string built at runtime — a binding label, a user
name, a file name — is never translated. Canvas content (typed text, sticky
notes, board and page names) is user data and stays as the user wrote it.
Displayed text is never an identifier. Widget IDs, stack page names, CSS names,
config keys and action IDs are fixed English spellings derived from typed
values, never from a label, and no code compares a label with translated text
to decide what to do.
### What is not translated
- `--help` and `--version`, which print before the configuration is read;
- log messages and developer diagnostics;
- `wayscriber-configurator`, the separate GTK4 crate, whose own interface is
still English. It does expose `[ui] language` under its UI settings, so the
overlay language can be picked there instead of editing the file by hand;
- keyboard key names. `Ctrl+Shift+1`, `Enter`, and `Not bound` are printed as
they appear on the keyboard; only the prose around them is translated.
- capture and export error detail — the `Display` text of `CaptureError` — which
@@ -90,14 +141,31 @@ notes, board and page names) is user data and stays as the user wrote it.
- overlay chrome that is not wrapped yet. The guard only sees strings that are
already marked, so an unwrapped surface stays English; the config diagnostics
in the about window's debug view are one such surface.
- in the configurator, a message already on the status line, and the result
of a session or background-setup action, keep the language they were shown
in until the next message replaces them. Captions, option labels and other
persistent text switch at once.
### Searching the configurator
The configurator's search matches its English terms in every language, so a
search that works in English keeps working after a switch. It also matches tab,
sub-tab and section titles, and keybinding action names, as they are displayed.
A row's translated caption is not searchable unless it is one of those.
## Adding a language
1. `src/domain/language.rs`: add a variant, its `code()`, its `catalog_code()`,
and the spellings `language_from_code`/`language_from_locale` accept.
2. `locales/<code>.toml`: translate `locales/zh-CN.toml`, keeping the keys.
3. `src/i18n.rs`: add the new file to `EMBEDDED_CATALOGS`.
4. `cargo test --test repository_guards translations` must pass, which proves
1. `src/domain/language.rs`: add a variant, its `code()`, its
`catalog_code()`, and its serde name and aliases.
2. `src/i18n.rs`: the spellings `language_from_code` and
`language_from_locale` accept, the variant's `encode`/`decode` atom, and the
new file in `EMBEDDED_CATALOGS`.
3. `src/config/types/toolbar/items/definitions.rs`: a table for the language in
`toolbar_item_definitions_in`, which keeps one per language.
4. `configurator/src/models/fields/theme.rs`: a `LanguageOption`, labeled with
the language's own name.
5. `locales/<code>.toml`: translate `locales/zh-CN.toml`, keeping the keys.
6. `cargo test --test repository_guards translations` must pass, which proves
the new catalog covers every marked string.
## Design notes
@@ -107,11 +175,28 @@ notes, board and page names) is user data and stays as the user wrote it.
translated costs nothing and a catalog can grow one surface at a time.
- Catalogs are embedded with `include_str!`, so a release binary carries its
translations, and the optional user file merges over them.
- The selected language is process-global and set once at startup, from
`[ui] language` in `src/backend/wayland/backend/state_init/config.rs`. UI
chrome text has no owner object able to carry a language, so renderers read
the selection as ambient state; this is the one documented exception to the
explicit-parameter rule in `src/ui/AGENTS.md`. Canvas text never reads it.
- The selected language is process-global. UI chrome text has no owner object
able to carry a language, so renderers read the selection as ambient state;
this is the one documented exception to the explicit-parameter rule in
`src/ui/AGENTS.md`. Canvas text never reads it.
- Each process selects its language before it builds any chrome, with
`i18n::initialize`, which also loads the catalog so the first frame does no
file I/O: the overlay in `src/backend/wayland/backend/state_init/config.rs`,
the daemon in `src/daemon/language.rs` before its tray, shortcut and
update-notice workers start, and the About window in
`src/about_window/mod.rs`. An overlay is a child process of the daemon and
cannot change the daemon's language, so there is no live switching between
processes.
- The configurator opens in English, loads every catalog from its blocking
config-load job, and applies the draft's language whenever the draft is
replaced (a load, Defaults, a finished save) or the Language row changes it.
The shell then rebuilds its pages from the model
(`configurator/src/app/component/rebuild.rs`) and retitles the persistent
chrome in place. The model owns everything typed or selected, so nothing the
user entered is lost; focus, the caret and the visible page's scroll position
are carried across. Text the model stores for display — catalog dates, the
shortcut recorder's prompt, conflict claimants, background-setup feedback —
is stored as what it reports and rendered when drawn.
- Layout: the help overlay measures its labels, so a translation changes the
column widths and the row count; a language whose words are much longer than
English may need layout tuning in fixed-width chrome.
+6 -2
View File
@@ -10,7 +10,9 @@ language shows instead.
```
- Only values are translated. Never change a key; it is the lookup key.
- Keep the number of `{}` placeholders equal to the key's. Order may differ.
- Keep the number of `{}` placeholders equal to the key's. A placeholder may
move within the sentence, but values fill them in order: the first `{}` in
the value always receives the first value, so two cannot be swapped.
- An entry whose value equals its key is dead weight and fails the guard.
- The file is ordered by key, case-insensitively.
@@ -24,4 +26,6 @@ See `docs/I18N.md` for the whole workflow, and `src/i18n.rs` for how a catalog
is loaded. A copy of a catalog placed in
`$XDG_DATA_HOME/wayscriber/locales/<code>.toml` (or the directory
`WAYSCRIBER_LOCALE_DIR` names) merges over the embedded one, which is how a
translation is corrected without rebuilding.
translation is corrected without rebuilding. It must be a regular file of at
most 1 MiB, not a symbolic link; otherwise it is ignored and the embedded
catalog is used.