mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
Put the language picker first in General UI, with a globe and a title that includes every supported language. Derive its title and search terms from shared language metadata. Add regression coverage for the title, icon and row order, and format the additions with the pinned Rust toolchain.
244 lines
12 KiB
Markdown
244 lines
12 KiB
Markdown
# Internationalization
|
|
|
|
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
|
|
|
|
```toml
|
|
[ui]
|
|
# "auto" follows the desktop locale; "en" and "zh-CN" force one.
|
|
language = "auto"
|
|
```
|
|
|
|
`auto` reads the desktop locale, most specific first: `LANGUAGE`, `LC_ALL`,
|
|
`LC_MESSAGES`, then `LANG`. A locale whose language has no catalog — German,
|
|
today — leaves the slot empty and the search continues; English is the final
|
|
fallback, so `auto` never fails.
|
|
|
|
The configurator's Language row is the first row of the UI tab and has a globe
|
|
icon. Its title names the setting in every supported language, the interface
|
|
language first: `Language / 语言` in English, `语言 / Language` in Chinese.
|
|
The 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.
|
|
|
|
To use a language for one run without editing `config.toml`, pass
|
|
`--language`; `wayscriber --list-languages` shows the codes, the configured
|
|
language, and which language is in effect and why:
|
|
|
|
```sh
|
|
wayscriber --list-languages
|
|
wayscriber --active --language zh-CN
|
|
wayscriber --daemon --language en # background mode and every overlay it opens
|
|
```
|
|
|
|
`--language` applies to `--active`, `--freeze`, `--daemon` and `--about`.
|
|
A daemon passes it to the overlays it opens and to the About window, but not
|
|
to the configurator, whose Language row is the way to preview a language
|
|
there. It cannot change a daemon that is already running.
|
|
|
|
Two environment variables help while testing a translation:
|
|
|
|
- `WAYSCRIBER_LANGUAGE=zh-CN` overrides `[ui] language` in every process
|
|
that sees it, the configurator included, so a translation can be checked
|
|
without editing `config.toml`. `--language` beats it.
|
|
- `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. 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
|
|
|
|
Wrap the literal where it is drawn:
|
|
|
|
```rust
|
|
use crate::i18n::{tr, trf};
|
|
|
|
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
|
|
cargo test --test repository_guards translations
|
|
```
|
|
|
|
It names every marked string that a catalog lacks, with the call site, for
|
|
example:
|
|
|
|
```
|
|
zh-CN: "Switch board slot" has no entry (src/ui/help_overlay/.../sections.rs:120)
|
|
```
|
|
|
|
Add the reported keys to `locales/zh-CN.toml` and run the guard again. The
|
|
catalog is ordered by key, case-insensitively, so reviews stay readable; keys
|
|
are the English strings and only values are translated.
|
|
|
|
Rules the guard enforces:
|
|
|
|
- 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.
|
|
|
|
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, config values such as `bold` or `blue`, 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
|
|
|
|
- command-line output: `--help` and `--version`, which print before the
|
|
configuration is read, and `--list-languages`, which names each language
|
|
in itself;
|
|
- log messages and developer diagnostics;
|
|
- 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
|
|
names the failing portal, tool, or buffer bound. It is engineering detail, not
|
|
interface copy; the toast that carries it is translated.
|
|
- the GTK toolbar's health and fallback reasons, which explain a diagnostic
|
|
handoff to the built-in toolbar rather than address the user.
|
|
- 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.
|
|
The Language row is also found by every supported language's native name
|
|
(`English`, `简体中文`) and its word for "language" (`语言`), regardless of
|
|
the active interface language. For example, `language`, `locale`, `中文`
|
|
and `语言` all reveal General UI. The command palette's **Interface Language…**
|
|
action opens that setting directly and accepts the same search terms.
|
|
|
|
### Searching the command palette and help
|
|
|
|
The command palette searches English action labels, descriptions, aliases and
|
|
shortcut names in every language. It also searches translated labels,
|
|
descriptions and category headings in the active language, so `撤销` finds Undo
|
|
and a mixed query such as `clear 画布` finds Clear Canvas. English result scores
|
|
and ordering remain unchanged. Help shares the metadata search and keeps its
|
|
section order.
|
|
|
|
## Adding a language
|
|
|
|
1. `src/domain/language.rs`: add a variant, its `code()`, its
|
|
`catalog_code()`, its serde name and aliases, its entry in `ALL`, its
|
|
`native_name()` and `english_name()`, and its `word_for_language()`.
|
|
`--list-languages` reads all of these but the last. The configurator's
|
|
Language row takes its option labels from `native_name()` and its title
|
|
from `word_for_language()`, and its search matches both.
|
|
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` for the
|
|
variant. Add its native name and its word for "language" to the aliases of
|
|
`OpenConfiguratorLanguage` in `src/config/action_meta/entries/ui.rs`. The
|
|
configurator reads both from `Language::ALL`, but the palette's aliases are
|
|
static. This lets users find the setting from another interface language.
|
|
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
|
|
|
|
- Keys are the English source strings, the way gettext uses `msgid`. Lookup is
|
|
an exact match with fallback, so marking a string that will never be
|
|
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. 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.
|