feat(cli): choose and list the interface language from the command line

`--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.
This commit is contained in:
devmobasa committed 2026-10-16 18:42:33 +02:00
1 parent 98b6f54c15
commit fcbc437816
24 files changed
+678 -54

No files matched your search

+28
View File
@@ -1236,6 +1236,33 @@ Pressure controls the width of pressure-sensitive freehand Pen strokes. Marker/T
Tablet input works out of the box in default builds. Set `[tablet].enabled = false` in `config.toml` to opt out. To build without tablet support, drop only that feature (bare `--no-default-features` would also strip portal capture, tray, and the GTK toolbars): `cargo build --release --no-default-features --features portal,tray,toolbar-gtk`. Tablet input works out of the box in default builds. Set `[tablet].enabled = false` in `config.toml` to opt out. To build without tablet support, drop only that feature (bare `--no-default-features` would also strip portal capture, tray, and the GTK toolbars): `cargo build --release --no-default-features --features portal,tray,toolbar-gtk`.
### Interface language
Wayscriber's interface is available in English and Simplified Chinese. By
default it follows your desktop locale and falls back to English. Pick a
language in the configurator (**UI → Language**, which previews it right away
and keeps it when you save) or in `config.toml`:
```toml
[ui]
language = "zh-CN" # "auto" (default), "en", or "zh-CN"
```
The overlay and the About window use the saved language the next time they
open, and background mode after it restarts. To use a language for one run
without changing `config.toml`, or to see which one is in effect:
```bash
wayscriber --list-languages # the choices, and the language in effect and why
wayscriber --active --language zh-CN # this overlay only
wayscriber --daemon --language en # background mode and every overlay it opens
```
`--language` also applies to `--freeze` and `--about`. It beats both
`[ui] language` and `WAYSCRIBER_LANGUAGE`. Typed text, board names and other
content you create are never translated. See [docs/I18N.md](docs/I18N.md) for
translations, local catalog overrides and adding a language.
--- ---
## Troubleshooting ## Troubleshooting
@@ -1329,6 +1356,7 @@ Common toggles:
- `WAYSCRIBER_TOOLBAR_BACKEND=auto|gtk|builtin` overrides the toolbar frontend (default: auto) - `WAYSCRIBER_TOOLBAR_BACKEND=auto|gtk|builtin` overrides the toolbar frontend (default: auto)
- `WAYSCRIBER_NO_TRAY=1` disables the tray icon (default: tray enabled) - `WAYSCRIBER_NO_TRAY=1` disables the tray icon (default: tray enabled)
- `WAYSCRIBER_TRAY_FORCE_PIXMAP=1` forces colored tray pixmaps and overrides `[tray].icon_style` - `WAYSCRIBER_TRAY_FORCE_PIXMAP=1` forces colored tray pixmaps and overrides `[tray].icon_style`
- `WAYSCRIBER_LANGUAGE=zh-CN` uses an interface language for this run (overrides `[ui] language`; `--language` overrides it)
See `docs/CONFIG.md` for the full list. See `docs/CONFIG.md` for the full list.
+2 -1
View File
@@ -421,7 +421,8 @@ reduced_motion = "auto"
# (WAYSCRIBER_LANGUAGE, then LANGUAGE/LC_ALL/LC_MESSAGES/LANG) and falls back # (WAYSCRIBER_LANGUAGE, then LANGUAGE/LC_ALL/LC_MESSAGES/LANG) and falls back
# to English when that locale has no translation. Each process reads this when # 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 # it starts: the overlay and About window on their next open, background mode
# after a restart. # after a restart. `wayscriber --list-languages` shows the choices and the one
# in effect; `--language CODE` overrides this for one run.
language = "auto" language = "auto"
# Show the status bar and its configured contents # Show the status bar and its configured contents
+3 -4
View File
@@ -117,10 +117,9 @@ impl LanguageOption {
/// must still recognize their own language to get back. Only the /// must still recognize their own language to get back. Only the
/// automatic choice, which is not a language, follows the UI. /// automatic choice, which is not a language, follows the UI.
pub fn label(&self) -> &'static str { pub fn label(&self) -> &'static str {
match self { match self.to_language().native_name() {
LanguageOption::Auto => tr("Automatic (system locale)"), Some(name) => name,
LanguageOption::English => "English", None => tr("Automatic (system locale)"),
LanguageOption::SimplifiedChinese => "简体中文",
} }
} }
+3 -1
View File
@@ -1137,7 +1137,7 @@ to save it.
**Theme, Motion & Language:** **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. - **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). - **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 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. - **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. For one run, `wayscriber --active|--freeze|--daemon|--about --language CODE` overrides the setting without changing the file, and so does `WAYSCRIBER_LANGUAGE`; the flag wins over the variable. `wayscriber --list-languages` lists the codes and says which language is in effect and why. 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:** **UI Styling:**
- **Font sizes**: Customize text size for status bar and help overlay - **Font sizes**: Customize text size for status bar and help overlay
@@ -2693,6 +2693,8 @@ These override behavior at runtime. Bool-ish values treat anything except `0`, `
- `WAYSCRIBER_NO_TRAY=1` disables the tray icon (default: tray enabled) - `WAYSCRIBER_NO_TRAY=1` disables the tray icon (default: tray enabled)
- `WAYSCRIBER_RESUME_SESSION=1/0` forces session persistence on/off for the current run (default: unset; follows config) - `WAYSCRIBER_RESUME_SESSION=1/0` forces session persistence on/off for the current run (default: unset; follows config)
- `WAYSCRIBER_LANGUAGE=en|zh-CN|auto` sets the interface language for the current run (overrides `[ui] language`; `--language` overrides it)
- `WAYSCRIBER_LOCALE_DIR=/path` names a directory of translation catalogs to merge over the built-in ones (default: `$XDG_DATA_HOME/wayscriber/locales`; see `docs/I18N.md`)
- `WAYSCRIBER_CONFIGURATOR=/path/to/wayscriber-configurator` overrides the configurator executable path - `WAYSCRIBER_CONFIGURATOR=/path/to/wayscriber-configurator` overrides the configurator executable path
- `WAYSCRIBER_DISABLE_UPDATE_CHECK=1` disables the background update check for this run (overrides `[updates] check`; `--check-update` still works) - `WAYSCRIBER_DISABLE_UPDATE_CHECK=1` disables the background update check for this run (overrides `[updates] check`; `--check-update` still works)
- `WAYSCRIBER_FORCE_INLINE_TOOLBARS=1` forces inline toolbars on Wayland (default: off) - `WAYSCRIBER_FORCE_INLINE_TOOLBARS=1` forces inline toolbars on Wayland (default: off)
+27 -7
View File
@@ -35,11 +35,26 @@ a terminal's: `systemctl --user show-environment` shows what it sees, and a
locale set with `systemctl --user set-environment LANG=…` or in locale set with `systemctl --user set-environment LANG=…` or in
`~/.config/environment.d/` applies after the service restarts. `~/.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: Two environment variables help while testing a translation:
- `WAYSCRIBER_LANGUAGE=zh-CN` overrides `[ui] language` entirely, in every - `WAYSCRIBER_LANGUAGE=zh-CN` overrides `[ui] language` in every process
process that sees it, so a translation can be checked without editing that sees it, the configurator included, so a translation can be checked
`config.toml`. without editing `config.toml`. `--language` beats it.
- `WAYSCRIBER_LOCALE_DIR=…` names a directory of catalog files to merge over - `WAYSCRIBER_LOCALE_DIR=…` names a directory of catalog files to merge over
the embedded ones, so a translation can be corrected without a rebuild. the embedded ones, so a translation can be corrected without a rebuild.
@@ -129,7 +144,9 @@ to decide what to do.
### What is not translated ### What is not translated
- `--help` and `--version`, which print before the configuration is read; - 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; - log messages and developer diagnostics;
- keyboard key names. `Ctrl+Shift+1`, `Enter`, and `Not bound` are printed as - 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. they appear on the keyboard; only the prose around them is translated.
@@ -156,14 +173,17 @@ A row's translated caption is not searchable unless it is one of those.
## Adding a language ## Adding a language
1. `src/domain/language.rs`: add a variant, its `code()`, its 1. `src/domain/language.rs`: add a variant, its `code()`, its
`catalog_code()`, and its serde name and aliases. `catalog_code()`, its serde name and aliases, its entry in `ALL`, and its
`native_name()` and `english_name()`. `--list-languages` reads all of
these; the configurator's Language row takes its labels from
`native_name()`.
2. `src/i18n.rs`: the spellings `language_from_code` and 2. `src/i18n.rs`: the spellings `language_from_code` and
`language_from_locale` accept, the variant's `encode`/`decode` atom, and the `language_from_locale` accept, the variant's `encode`/`decode` atom, and the
new file in `EMBEDDED_CATALOGS`. new file in `EMBEDDED_CATALOGS`.
3. `src/config/types/toolbar/items/definitions.rs`: a table for the language in 3. `src/config/types/toolbar/items/definitions.rs`: a table for the language in
`toolbar_item_definitions_in`, which keeps one per language. `toolbar_item_definitions_in`, which keeps one per language.
4. `configurator/src/models/fields/theme.rs`: a `LanguageOption`, labeled with 4. `configurator/src/models/fields/theme.rs`: a `LanguageOption` for the
the language's own name. variant.
5. `locales/<code>.toml`: translate `locales/zh-CN.toml`, keeping the keys. 5. `locales/<code>.toml`: translate `locales/zh-CN.toml`, keeping the keys.
6. `cargo test --test repository_guards translations` must pass, which proves 6. `cargo test --test repository_guards translations` must pass, which proves
the new catalog covers every marked string. the new catalog covers every marked string.
+38
View File
@@ -115,6 +115,22 @@ fn run_about_event_loop(
Ok(()) Ok(())
} }
/// The arguments that start the About window: `--about`, and the `--language`
/// this process runs with, if any, so the window matches the one that opened
/// it.
pub(crate) fn launch_arguments() -> Vec<std::ffi::OsString> {
launch_arguments_for(crate::i18n::command_line_language())
}
fn launch_arguments_for(language: Option<crate::domain::Language>) -> Vec<std::ffi::OsString> {
let mut arguments = vec!["--about".into()];
if let Some(language) = language {
arguments.push("--language".into());
arguments.push(language.code().into());
}
arguments
}
/// What the dialog reads from the config before it draws: the theme, from /// What the dialog reads from the config before it draws: the theme, from
/// the same `[ui] theme` key as the overlay so the dialog matches the toolbars /// the same `[ui] theme` key as the overlay so the dialog matches the toolbars
/// the user already sees, and the UI language. A config that cannot be read /// the user already sees, and the UI language. A config that cannot be read
@@ -198,6 +214,28 @@ mod tests {
fs::write(path, contents).expect("write config"); fs::write(path, contents).expect("write config");
} }
#[test]
fn the_window_is_opened_with_the_openers_language() {
assert_eq!(launch_arguments_for(None), ["--about"]);
assert_eq!(
launch_arguments_for(Some(Language::SimplifiedChinese)),
["--about", "--language", "zh-CN"]
);
// The arguments it is given parse back to the same language.
let parsed = crate::cli::Cli::try_parse_from(
std::iter::once("wayscriber".into())
.chain(launch_arguments_for(Some(Language::SimplifiedChinese))),
);
assert!(
matches!(
parsed,
Ok(crate::cli::CliOutcome::Run(ref cli))
if cli.about && cli.language == Some(Language::SimplifiedChinese)
),
"{parsed:?}"
);
}
#[test] #[test]
fn the_dialog_reads_its_language_with_its_theme() { fn the_dialog_reads_its_language_with_its_theme() {
with_temp_config_home(|_| { with_temp_config_home(|_| {
+174
View File
@@ -0,0 +1,174 @@
//! `--list-languages`: the interface languages, and the one this run uses.
//!
//! Command-line output stays English, like the rest of the CLI.
use crate::cli::Cli;
use crate::config::Config;
use crate::domain::Language;
use crate::env_vars::OVERLAY_LANGUAGE_ENV;
use crate::i18n::{self, LanguageSource};
/// The language this run was given: its own `--language`, or, for an overlay
/// a daemon started, that daemon's.
pub(super) fn given_language(cli: &Cli) -> Option<Language> {
given_language_with(cli, std::env::var(OVERLAY_LANGUAGE_ENV).ok().as_deref())
}
fn given_language_with(cli: &Cli, from_daemon: Option<&str>) -> Option<Language> {
cli.language.or_else(|| {
from_daemon
.filter(|_| cli.active || cli.freeze)
.and_then(i18n::language_from_code)
})
}
pub(super) fn print_languages() {
let configured = Config::load()
.map(|loaded| loaded.config.ui.language)
.map_err(|err| format!("{err:#}"));
let requested = i18n::requested_language(configured.clone().unwrap_or_default());
let in_effect = i18n::resolve(requested.0);
print!("{}", render(configured, requested, in_effect));
}
/// The listing, from what was read: the configured language (or why the
/// config could not be read), the setting that decides, and what it resolves
/// to.
fn render(
configured: Result<Language, String>,
(requested, source): (Language, LanguageSource),
in_effect: Language,
) -> String {
let mut out = String::from("Interface languages:\n");
for language in Language::ALL {
out.push_str(&format!(
" {:<7} {}\n",
language.code(),
describe(language)
));
}
out.push('\n');
match &configured {
Ok(language) => out.push_str(&format!(
"Configured: {} ([ui] language)\n",
language.code()
)),
// The first line names the file and the position; the rest of a
// parse error is a source excerpt that would break up the listing.
Err(err) => out.push_str(&format!(
"Configured: auto, the default, because the config could not be read: {}\n",
err.lines().next().unwrap_or_default()
)),
}
let decided_by = match source {
LanguageSource::CommandLine => "--language",
LanguageSource::Environment => "WAYSCRIBER_LANGUAGE",
LanguageSource::Configured => "[ui] language",
};
let through_locale = if requested.is_auto() {
" through the desktop locale"
} else {
""
};
out.push_str(&format!(
"In effect: {} {}, from {decided_by}{through_locale}\n",
in_effect.code(),
describe(in_effect)
));
out.push_str(
"\nUse --language CODE for one run, or set [ui] language in config.toml or the configurator.\n",
);
out
}
fn describe(language: Language) -> String {
match language.native_name() {
None => "follow the desktop locale; English when it has no translation".to_string(),
Some(native) if native == language.english_name() => native.to_string(),
Some(native) => format!("{native} ({})", language.english_name()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(arguments: &[&str]) -> Cli {
match Cli::try_parse_from(std::iter::once("wayscriber").chain(arguments.iter().copied())) {
Ok(crate::cli::CliOutcome::Run(cli)) => cli,
other => panic!("{arguments:?} did not parse to a run: {other:?}"),
}
}
#[test]
fn an_overlay_takes_its_daemons_language_unless_given_its_own() {
let overlay = parse(&["--active"]);
assert_eq!(
given_language_with(&overlay, Some("zh-CN")),
Some(Language::SimplifiedChinese)
);
assert_eq!(given_language_with(&overlay, None), None);
assert_eq!(given_language_with(&overlay, Some("fr")), None);
let own = parse(&["--freeze", "--language", "en"]);
assert_eq!(
given_language_with(&own, Some("zh-CN")),
Some(Language::English)
);
// Only an overlay reads the daemon's input; a daemon or the About
// window that inherited it does not.
for arguments in [&["--daemon"][..], &["--about"]] {
assert_eq!(given_language_with(&parse(arguments), Some("zh-CN")), None);
}
}
#[test]
fn the_listing_names_every_language_and_why_one_is_in_effect() {
let listing = render(
Ok(Language::Auto),
(Language::Auto, LanguageSource::Configured),
Language::SimplifiedChinese,
);
assert_eq!(
listing,
"Interface languages:\n\
\x20 auto follow the desktop locale; English when it has no translation\n\
\x20 en English\n\
\x20 zh-CN 简体中文 (Simplified Chinese)\n\
\n\
Configured: auto ([ui] language)\n\
In effect: zh-CN 简体中文 (Simplified Chinese), from [ui] language through the desktop locale\n\
\n\
Use --language CODE for one run, or set [ui] language in config.toml or the configurator.\n"
);
}
#[test]
fn the_listing_says_which_override_won() {
let from_flag = render(
Ok(Language::English),
(Language::SimplifiedChinese, LanguageSource::CommandLine),
Language::SimplifiedChinese,
);
let from_environment = render(
Err("TOML parse error at line 1, column 4\n |\n1 | [ui\n".to_string()),
(Language::English, LanguageSource::Environment),
Language::English,
);
assert!(
from_flag
.contains("In effect: zh-CN 简体中文 (Simplified Chinese), from --language\n")
);
assert!(from_environment.contains(
"Configured: auto, the default, because the config could not be read: \
TOML parse error at line 1, column 4\nIn effect:"
));
assert!(from_environment.contains("In effect: en English, from WAYSCRIBER_LANGUAGE\n"));
}
}
+11
View File
@@ -1,4 +1,5 @@
mod env; mod env;
mod languages;
mod session; mod session;
mod usage; mod usage;
@@ -203,6 +204,16 @@ pub fn run(cli: Cli) -> anyhow::Result<()> {
return Ok(()); return Ok(());
} }
// Before anything selects a language, so every selection in this run,
// and the overlays and About windows it starts, use it.
if let Some(language) = languages::given_language(&cli) {
crate::i18n::set_command_line_language(language);
}
if cli.list_languages {
languages::print_languages();
return Ok(());
}
// Runtime and update-checking modes create their process broker before // Runtime and update-checking modes create their process broker before
// acquiring locks or starting threads. The guard spans the complete run. // acquiring locks or starting threads. The guard spans the complete run.
let _process_broker = needs_process_broker(&cli) let _process_broker = needs_process_broker(&cli)
+4
View File
@@ -217,6 +217,10 @@ pub(crate) fn print_usage() {
println!(" wayscriber --no-tray Skip system tray (headless daemon)"); println!(" wayscriber --no-tray Skip system tray (headless daemon)");
println!(" wayscriber --about Show the About window"); println!(" wayscriber --about Show the About window");
println!(" wayscriber --check-update Check wayscriber.com for a newer release"); println!(" wayscriber --check-update Check wayscriber.com for a newer release");
println!(" wayscriber --list-languages List interface languages and the one in effect");
println!(
" wayscriber --active --language CODE Use an interface language for this run (also --freeze, --daemon, --about)"
);
println!( println!(
" wayscriber --resume-session Force session resume on (all boards/history/tool state)" " wayscriber --resume-session Force session resume on (all boards/history/tool state)"
); );
+1 -1
View File
@@ -67,7 +67,7 @@ impl WaylandState {
match spawn_detached( match spawn_detached(
crate::process_broker::HelperKind::About, crate::process_broker::HelperKind::About,
executable.as_os_str(), executable.as_os_str(),
&["--about".into()], &crate::about_window::launch_arguments(),
) { ) {
Ok(child) => { Ok(child) => {
log::info!("Launched About window (pid {})", child.id()); log::info!("Launched About window (pid {})", child.id());
+62 -2
View File
@@ -1,6 +1,7 @@
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use crate::domain::Language;
use crate::tray_action::TrayAction; use crate::tray_action::TrayAction;
#[derive(Debug, Default)] #[derive(Debug, Default)]
@@ -76,6 +77,13 @@ pub struct Cli {
/// Print compiled runtime capabilities for companion tools /// Print compiled runtime capabilities for companion tools
pub runtime_capabilities: bool, pub runtime_capabilities: bool,
/// Interface language for this run, beating `[ui] language` and
/// `WAYSCRIBER_LANGUAGE` (the config file is not changed)
pub language: Option<Language>,
/// List the interface languages and the one in effect, then exit
pub list_languages: bool,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -151,6 +159,11 @@ impl Cli {
"--no-resume-session" => cli.no_resume_session = true, "--no-resume-session" => cli.no_resume_session = true,
"--about" => cli.about = true, "--about" => cli.about = true,
"--check-update" => cli.check_update = true, "--check-update" => cli.check_update = true,
"--language" => {
index += 1;
cli.language = Some(parse_language(&value_after(&args, index, "--language")?)?);
}
"--list-languages" => cli.list_languages = true,
crate::runtime_capabilities::RUNTIME_CAPABILITIES_FLAG => { crate::runtime_capabilities::RUNTIME_CAPABILITIES_FLAG => {
cli.runtime_capabilities = true; cli.runtime_capabilities = true;
} }
@@ -167,6 +180,9 @@ impl Cli {
_ if arg.starts_with("--rename-session=") => { _ if arg.starts_with("--rename-session=") => {
cli.rename_session = Some(value_from_equals(arg, "--rename-session")?); cli.rename_session = Some(value_from_equals(arg, "--rename-session")?);
} }
_ if arg.starts_with("--language=") => {
cli.language = Some(parse_language(&value_from_equals(arg, "--language")?)?);
}
_ if is_short_option_cluster(arg) => { _ if is_short_option_cluster(arg) => {
match parse_short_option_cluster(&args, index, &mut cli)? { match parse_short_option_cluster(&args, index, &mut cli)? {
ShortOptionOutcome::Continue(next_index) => index = next_index, ShortOptionOutcome::Continue(next_index) => index = next_index,
@@ -261,12 +277,17 @@ impl Cli {
self.validate_session_flag_pairs()?; self.validate_session_flag_pairs()?;
self.validate_overlay_commands()?; self.validate_overlay_commands()?;
self.validate_session_commands()?; self.validate_session_commands()?;
self.validate_catalog_commands() self.validate_catalog_commands()?;
self.validate_language_options()
} }
fn validate_runtime_capabilities_command(&self) -> Result<(), String> { fn validate_runtime_capabilities_command(&self) -> Result<(), String> {
if self.runtime_capabilities if self.runtime_capabilities
&& (self.selects_a_launch_command() || self.about || self.check_update) && (self.selects_a_launch_command()
|| self.about
|| self.check_update
|| self.list_languages
|| self.language.is_some())
{ {
return Err("--runtime-capabilities conflicts with launch flags".to_string()); return Err("--runtime-capabilities conflicts with launch flags".to_string());
} }
@@ -462,8 +483,44 @@ impl Cli {
return Err("--check-update conflicts with the selected command".to_string()); return Err("--check-update conflicts with the selected command".to_string());
} }
if self.list_languages
&& (self.selects_a_launch_command() || self.about || self.check_update)
{
return Err("--list-languages conflicts with the selected command".to_string());
}
Ok(()) Ok(())
} }
/// `--language` sets the language of the process it starts, and of the
/// overlays and About windows that process starts in turn. A command that
/// hands its work to a running daemon, or only prints English text, has no
/// window for it to change.
fn validate_language_options(&self) -> Result<(), String> {
if self.language.is_none() {
return Ok(());
}
if self.daemon_toggle || self.overlay_action_count() > 0 {
return Err(
"--language cannot change a running daemon's overlays; start the daemon with --language, or set [ui] language"
.to_string(),
);
}
if !(self.active || self.freeze || self.daemon || self.about || self.list_languages) {
return Err(
"--language applies to --active, --freeze, --daemon, --about and --list-languages"
.to_string(),
);
}
Ok(())
}
}
/// Parses a `--language` value: any spelling `[ui] language` accepts.
fn parse_language(value: &str) -> Result<Language, String> {
crate::i18n::language_from_code(value).ok_or_else(|| {
format!("unknown language '{value}'; run 'wayscriber --list-languages' to see the choices")
})
} }
fn is_short_option_cluster(arg: &str) -> bool { fn is_short_option_cluster(arg: &str) -> bool {
@@ -563,6 +620,7 @@ pub(crate) fn print_help() {
println!(" wayscriber --clear-tool-state [--session-file PATH]"); println!(" wayscriber --clear-tool-state [--session-file PATH]");
println!(" wayscriber --about"); println!(" wayscriber --about");
println!(" wayscriber --check-update"); println!(" wayscriber --check-update");
println!(" wayscriber --list-languages");
println!(); println!();
println!("Options:"); println!("Options:");
println!(" -d, --daemon Run as background daemon"); println!(" -d, --daemon Run as background daemon");
@@ -588,6 +646,8 @@ pub(crate) fn print_help() {
println!(" --session-file PATH Use a named session file"); println!(" --session-file PATH Use a named session file");
println!(" --about Show the About window"); println!(" --about Show the About window");
println!(" --check-update Check wayscriber.com for a newer release"); println!(" --check-update Check wayscriber.com for a newer release");
println!(" --language CODE Interface language for this run (auto, en, zh-CN)");
println!(" --list-languages List interface languages and the one in effect");
println!(" -h, --help Show help"); println!(" -h, --help Show help");
println!(" -V, --version Show version"); println!(" -V, --version Show version");
} }
+95
View File
@@ -506,3 +506,98 @@ fn check_update_rejects_valued_launch_flags() {
"--check-update conflicts with the selected command" "--check-update conflicts with the selected command"
); );
} }
#[test]
fn language_selects_this_runs_interface_language() {
use crate::domain::Language;
for (args, expected) in [
(
["wayscriber", "--active", "--language", "zh-CN"],
Language::SimplifiedChinese,
),
(
["wayscriber", "--daemon", "--language", "en"],
Language::English,
),
(
["wayscriber", "--about", "--language", "auto"],
Language::Auto,
),
// Every spelling [ui] language accepts.
(
["wayscriber", "--freeze", "--language", "zh"],
Language::SimplifiedChinese,
),
] {
assert_eq!(parse_cli(args).language, Some(expected), "{args:?}");
}
let cli = parse_cli(["wayscriber", "--active", "--language=zh-CN"]);
assert_eq!(cli.language, Some(Language::SimplifiedChinese));
}
#[test]
fn language_rejects_unknown_codes_and_commands_it_cannot_change() {
let error = |args: &[&str]| match Cli::try_parse_from(args.iter().copied()) {
Err(error) => error,
Ok(outcome) => panic!("{args:?} parsed as {outcome:?}"),
};
assert_eq!(
error(&["wayscriber", "--active", "--language", "fr"]),
"unknown language 'fr'; run 'wayscriber --list-languages' to see the choices"
);
assert_eq!(
error(&["wayscriber", "--active", "--language"]),
"--language requires a value"
);
for args in [
&["wayscriber", "--daemon-toggle", "--language", "zh-CN"][..],
&["wayscriber", "--light-toggle", "--language", "zh-CN"],
] {
assert!(
error(args).starts_with("--language cannot change a running daemon's overlays"),
"{args:?}"
);
}
for args in [
&["wayscriber", "--language", "zh-CN"][..],
&["wayscriber", "--session-info", "--language", "zh-CN"],
&["wayscriber", "--check-update", "--language", "zh-CN"],
] {
assert_eq!(
error(args),
"--language applies to --active, --freeze, --daemon, --about and --list-languages",
"{args:?}"
);
}
assert_eq!(
error(&[
"wayscriber",
"--runtime-capabilities",
"--language",
"zh-CN"
]),
"--runtime-capabilities conflicts with launch flags"
);
}
#[test]
fn list_languages_is_a_print_and_exit_command() {
assert!(parse_cli(["wayscriber", "--list-languages"]).list_languages);
// It shows what --language would select.
let cli = parse_cli(["wayscriber", "--list-languages", "--language", "zh-CN"]);
assert!(cli.list_languages && cli.language.is_some());
for args in [
["wayscriber", "--list-languages", "--active"],
["wayscriber", "--list-languages", "--about"],
] {
assert_eq!(
Cli::try_parse_from(args).unwrap_err(),
"--list-languages conflicts with the selected command",
"{args:?}"
);
}
}
+47 -4
View File
@@ -2,9 +2,10 @@ use std::ffi::OsString;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::daemon::control::DaemonToggleRequest; use crate::daemon::control::DaemonToggleRequest;
use crate::domain::Language;
use crate::env_vars::{ use crate::env_vars::{
DESKTOP_STARTUP_ID_ENV, NO_DETACH_ENV, OVERLAY_HOME_SESSION_ENV, OVERLAY_PREFERRED_SESSION_ENV, DESKTOP_STARTUP_ID_ENV, NO_DETACH_ENV, OVERLAY_HOME_SESSION_ENV, OVERLAY_LANGUAGE_ENV,
OVERLAY_SESSION_REPORTS_ENV, XDG_ACTIVATION_TOKEN_ENV, OVERLAY_PREFERRED_SESSION_ENV, OVERLAY_SESSION_REPORTS_ENV, XDG_ACTIVATION_TOKEN_ENV,
}; };
pub(in crate::daemon) struct OverlayLaunchRequest { pub(in crate::daemon) struct OverlayLaunchRequest {
@@ -81,11 +82,17 @@ pub(super) struct OverlayLaunch {
/// The launch for `request`. A child `generation` reports its session to the /// The launch for `request`. A child `generation` reports its session to the
/// daemon, which passes the session it remembers from the last report as the /// daemon, which passes the session it remembers from the last report as the
/// one to continue, unless the request asked for a session file. /// one to continue, unless the request asked for a session file.
///
/// `language` is the daemon's own `--language`, which its overlays use too.
/// It travels as an optional input, so an overlay without the flag still
/// starts, and not as `WAYSCRIBER_LANGUAGE`, which a configurator the overlay
/// opens would inherit and obey over its own Language row.
pub(super) fn build_overlay_launch( pub(super) fn build_overlay_launch(
request: &OverlayLaunchRequest, request: &OverlayLaunchRequest,
resume_default: Option<bool>, resume_default: Option<bool>,
generation: Option<&str>, generation: Option<&str>,
remembered_session_file: Option<&Path>, remembered_session_file: Option<&Path>,
language: Option<Language>,
) -> OverlayLaunch { ) -> OverlayLaunch {
let mut arguments = vec![OsString::from("--active")]; let mut arguments = vec![OsString::from("--active")];
if request.freeze { if request.freeze {
@@ -134,6 +141,10 @@ pub(super) fn build_overlay_launch(
.filter(|_| request.explicit_session_file.is_none()) .filter(|_| request.explicit_session_file.is_none())
.map(Into::into), .map(Into::into),
), ),
(
OVERLAY_LANGUAGE_ENV.into(),
language.map(|language| language.code().into()),
),
]); ]);
if let Some(mode) = request.mode() { if let Some(mode) = request.mode() {
@@ -180,7 +191,7 @@ mod tests {
false, false,
); );
let launch = build_overlay_launch(&request, default, generation, None); let launch = build_overlay_launch(&request, default, generation, None, None);
let mut expected = vec![(NO_DETACH_ENV.into(), Some("1".into()))]; let mut expected = vec![(NO_DETACH_ENV.into(), Some("1".into()))];
if let Some(generation) = generation { if let Some(generation) = generation {
@@ -202,6 +213,7 @@ mod tests {
), ),
(OVERLAY_HOME_SESSION_ENV.into(), None), (OVERLAY_HOME_SESSION_ENV.into(), None),
(OVERLAY_PREFERRED_SESSION_ENV.into(), None), (OVERLAY_PREFERRED_SESSION_ENV.into(), None),
(OVERLAY_LANGUAGE_ENV.into(), None),
]); ]);
assert_eq!(launch.environment, expected); assert_eq!(launch.environment, expected);
} }
@@ -226,7 +238,8 @@ mod tests {
false, false,
); );
let launch = build_overlay_launch(&request, None, Some("generation"), Some(remembered)); let launch =
build_overlay_launch(&request, None, Some("generation"), Some(remembered), None);
let value = |name: &str| { let value = |name: &str| {
launch launch
@@ -248,4 +261,34 @@ mod tests {
); );
} }
} }
#[test]
fn the_daemons_language_reaches_its_overlays_without_a_new_argument() {
let request = OverlayLaunchRequest::new(None, None, None, None, false);
let language_entries = |language| {
build_overlay_launch(&request, None, None, None, language)
.environment
.into_iter()
.filter(|(key, _)| {
key == OVERLAY_LANGUAGE_ENV || key == crate::env_vars::LANGUAGE_OVERRIDE_ENV
})
.collect::<Vec<_>>()
};
assert_eq!(
language_entries(Some(Language::SimplifiedChinese)),
[(OsString::from(OVERLAY_LANGUAGE_ENV), Some("zh-CN".into()))]
);
// Without --language a stale value is not inherited, so the overlay
// reads its config; a WAYSCRIBER_LANGUAGE the user set for the daemon
// is inherited untouched either way.
assert_eq!(
language_entries(None),
[(OsString::from(OVERLAY_LANGUAGE_ENV), None)]
);
assert_eq!(
build_overlay_launch(&request, None, None, None, Some(Language::Auto)).arguments,
[OsString::from("--active")]
);
}
} }
+1
View File
@@ -94,6 +94,7 @@ impl OverlayLifecycle {
crate::decode_session_override(resume_override.load(Ordering::Acquire)), crate::decode_session_override(resume_override.load(Ordering::Acquire)),
self.child.generation(), self.child.generation(),
remembered_session_file, remembered_session_file,
crate::i18n::command_line_language(),
); );
let attempt = (|| -> Result<u32> { let attempt = (|| -> Result<u32> {
+1 -1
View File
@@ -176,7 +176,7 @@ mod tests {
fn build_test_launch(daemon: &mut Daemon) -> OverlayLaunch { fn build_test_launch(daemon: &mut Daemon) -> OverlayLaunch {
let request = daemon.take_pending_launch(); let request = daemon.take_pending_launch();
build_overlay_launch(&request, daemon.session_resume_override(), None, None) build_overlay_launch(&request, daemon.session_resume_override(), None, None, None)
} }
fn launch_args(launch: &OverlayLaunch) -> Vec<String> { fn launch_args(launch: &OverlayLaunch) -> Vec<String> {
+30 -1
View File
@@ -166,6 +166,34 @@ fn wait_until_retired(daemon: &mut Daemon) {
const HOME: &str = "/tmp/home.wayscriber-session"; const HOME: &str = "/tmp/home.wayscriber-session";
const REMEMBERED: &str = "/tmp/remembered.wayscriber-session"; const REMEMBERED: &str = "/tmp/remembered.wayscriber-session";
#[test]
fn a_daemon_started_with_a_language_gives_it_to_its_overlays() {
// The command-line language is set once per process, so this runs alone.
crate::test_env::in_language_isolated_child(
concat!(
module_path!(),
"::a_daemon_started_with_a_language_gives_it_to_its_overlays"
),
|| {
crate::i18n::set_command_line_language(crate::domain::Language::SimplifiedChinese);
with_fixture(false, |daemon, _, root| {
let receipt = show(daemon, root);
assert_eq!(receipt["language"], "zh-CN");
// Not as an argument, so an older overlay still starts.
assert!(
!receipt["args"]
.as_array()
.unwrap()
.iter()
.any(|argument| argument == "--language"),
"{receipt}"
);
daemon.hide_overlay().unwrap();
});
},
);
}
#[test] #[test]
fn the_next_show_continues_the_session_the_overlay_reported() { fn the_next_show_continues_the_session_the_overlay_reported() {
with_fixture(false, |daemon, _, root| { with_fixture(false, |daemon, _, root| {
@@ -173,6 +201,7 @@ fn the_next_show_continues_the_session_the_overlay_reported() {
let first = show(daemon, root); let first = show(daemon, root);
assert_eq!(first["reports"], "1"); assert_eq!(first["reports"], "1");
assert_eq!(first["home"], HOME); assert_eq!(first["home"], HOME);
assert!(first["language"].is_null());
assert!(first["preferred"].is_null()); assert!(first["preferred"].is_null());
let generation = first["generation"].as_str().unwrap(); let generation = first["generation"].as_str().unwrap();
assert!(session_report(generation).exists()); assert!(session_report(generation).exists());
@@ -468,7 +497,7 @@ fn preparation_error_keeps_only_the_token_and_does_not_replay_options() {
assert!(daemon.start_launch(request, &[candidate]).is_err()); assert!(daemon.start_launch(request, &[candidate]).is_err());
let retained = daemon.take_pending_launch(); let retained = daemon.take_pending_launch();
let actual = launch::build_overlay_launch(&retained, Some(true), None, None); let actual = launch::build_overlay_launch(&retained, Some(true), None, None, None);
assert_eq!( assert_eq!(
actual.arguments, actual.arguments,
+1
View File
@@ -142,6 +142,7 @@ fn write_receipt() -> Result<()> {
"reports": launched_with(crate::env_vars::OVERLAY_SESSION_REPORTS_ENV), "reports": launched_with(crate::env_vars::OVERLAY_SESSION_REPORTS_ENV),
"home": launched_with(crate::env_vars::OVERLAY_HOME_SESSION_ENV), "home": launched_with(crate::env_vars::OVERLAY_HOME_SESSION_ENV),
"preferred": launched_with(crate::env_vars::OVERLAY_PREFERRED_SESSION_ENV), "preferred": launched_with(crate::env_vars::OVERLAY_PREFERRED_SESSION_ENV),
"language": launched_with(crate::env_vars::OVERLAY_LANGUAGE_ENV),
"pid": std::process::id(), "pid": std::process::id(),
"generation": std::env::var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV)?, "generation": std::env::var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV)?,
}); });
+1 -1
View File
@@ -117,7 +117,7 @@ impl WayscriberTray {
match spawn_detached( match spawn_detached(
crate::process_broker::HelperKind::About, crate::process_broker::HelperKind::About,
exe.as_os_str(), exe.as_os_str(),
&["--about".into()], &crate::about_window::launch_arguments(),
) { ) {
Ok(child) => { Ok(child) => {
info!("Launched About window (pid {})", child.id()); info!("Launched About window (pid {})", child.id());
+33 -5
View File
@@ -24,6 +24,10 @@ pub enum Language {
} }
impl Language { impl Language {
/// Every choice `[ui] language` and `--language` accept, `auto` first:
/// the order `--list-languages` and the configurator list them in.
pub const ALL: [Self; 3] = [Self::Auto, Self::English, Self::SimplifiedChinese];
/// The spelling used in configuration and logs. /// The spelling used in configuration and logs.
pub fn code(self) -> &'static str { pub fn code(self) -> &'static str {
match self { match self {
@@ -46,6 +50,26 @@ impl Language {
pub fn is_auto(self) -> bool { pub fn is_auto(self) -> bool {
matches!(self, Self::Auto) matches!(self, Self::Auto)
} }
/// The language's name in itself, never translated, so a user can find
/// their own language in a list drawn in one they cannot read. `None` for
/// `Auto`, which is not a language.
pub fn native_name(self) -> Option<&'static str> {
match self {
Self::Auto => None,
Self::English => Some("English"),
Self::SimplifiedChinese => Some("简体中文"),
}
}
/// The language's English name, for command-line output and logs.
pub fn english_name(self) -> &'static str {
match self {
Self::Auto => "Automatic",
Self::English => "English",
Self::SimplifiedChinese => "Simplified Chinese",
}
}
} }
#[cfg(test)] #[cfg(test)]
@@ -54,11 +78,7 @@ mod tests {
#[test] #[test]
fn language_codes_round_trip_through_serde() { fn language_codes_round_trip_through_serde() {
for language in [ for language in Language::ALL {
Language::Auto,
Language::English,
Language::SimplifiedChinese,
] {
let encoded = serde_json::to_string(&language).expect("serialize language"); let encoded = serde_json::to_string(&language).expect("serialize language");
assert_eq!(encoded, format!("\"{}\"", language.code())); assert_eq!(encoded, format!("\"{}\"", language.code()));
let decoded: Language = serde_json::from_str(&encoded).expect("deserialize language"); let decoded: Language = serde_json::from_str(&encoded).expect("deserialize language");
@@ -81,4 +101,12 @@ mod tests {
fn default_language_follows_the_locale() { fn default_language_follows_the_locale() {
assert_eq!(Language::default(), Language::Auto); assert_eq!(Language::default(), Language::Auto);
} }
#[test]
fn every_language_but_auto_is_named_in_itself() {
for language in Language::ALL {
assert_eq!(language.native_name().is_none(), language.is_auto());
}
assert_eq!(Language::SimplifiedChinese.native_name(), Some("简体中文"));
}
} }
+3
View File
@@ -18,6 +18,9 @@ pub(crate) const OVERLAY_HOME_SESSION_ENV: &str = "WAYSCRIBER_OVERLAY_HOME_SESSI
/// The session file a daemon overlay continues in place of home, while it is /// The session file a daemon overlay continues in place of home, while it is
/// still a usable session file. /// still a usable session file.
pub(crate) const OVERLAY_PREFERRED_SESSION_ENV: &str = "WAYSCRIBER_OVERLAY_PREFERRED_SESSION"; pub(crate) const OVERLAY_PREFERRED_SESSION_ENV: &str = "WAYSCRIBER_OVERLAY_PREFERRED_SESSION";
/// The daemon's `--language`, which its overlays take as their own. Absent,
/// an overlay selects its language like any other run.
pub(crate) const OVERLAY_LANGUAGE_ENV: &str = "WAYSCRIBER_OVERLAY_LANGUAGE";
pub const CATALOG_HOOKS_TEST_ENV: &str = "WAYSCRIBER_ENABLE_CATALOG_HOOKS_IN_TESTS"; pub const CATALOG_HOOKS_TEST_ENV: &str = "WAYSCRIBER_ENABLE_CATALOG_HOOKS_IN_TESTS";
/// Disables the periodic update check regardless of `[updates] check`. /// Disables the periodic update check regardless of `[updates] check`.
pub const DISABLE_UPDATE_CHECK_ENV: &str = "WAYSCRIBER_DISABLE_UPDATE_CHECK"; pub const DISABLE_UPDATE_CHECK_ENV: &str = "WAYSCRIBER_DISABLE_UPDATE_CHECK";
+56 -11
View File
@@ -35,20 +35,57 @@ type Catalog = BTreeMap<String, String>;
/// Atoms: English is 0 so a never-configured process needs no catalog. /// Atoms: English is 0 so a never-configured process needs no catalog.
static ACTIVE: AtomicU8 = AtomicU8::new(0); static ACTIVE: AtomicU8 = AtomicU8::new(0);
/// The `--language` this process runs with: its own, or, for an overlay, its
/// daemon's.
static COMMAND_LINE: OnceLock<Language> = OnceLock::new();
/// Selects the language the UI will draw in. /// Selects the language the UI will draw in.
/// ///
/// `Auto` resolves through the locale chain, and `WAYSCRIBER_LANGUAGE` /// `Auto` resolves through the locale chain. `--language` and then
/// overrides the argument entirely — a testing aid for checking a translation /// `WAYSCRIBER_LANGUAGE` override the argument entirely, for one run, without
/// without editing `config.toml`. Each process calls this before it builds /// editing `config.toml`. Each process calls this before it builds translated
/// translated chrome; the configurator calls it again whenever the language /// chrome; the configurator calls it again whenever the language of its draft
/// of its draft changes, to preview that language. /// changes, to preview that language.
pub fn set_language(language: Language) { pub fn set_language(language: Language) {
let active = select(language, env_var(LANGUAGE_OVERRIDE_ENV).as_deref()); let active = resolve(requested_language(language).0);
ACTIVE.store(encode(active), Ordering::Relaxed); ACTIVE.store(encode(active), Ordering::Relaxed);
log::debug!("UI language: {}", active.code()); log::debug!("UI language: {}", active.code());
} }
/// Records `--language` for the rest of this process. Set once, at startup,
/// before any language is selected.
pub fn set_command_line_language(language: Language) {
let _ = COMMAND_LINE.set(language);
}
/// The `--language` this process was started with, so the overlays and About
/// windows it starts can be given the same one.
pub fn command_line_language() -> Option<Language> {
COMMAND_LINE.get().copied()
}
/// Which setting decided the language in effect.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LanguageSource {
/// `--language`.
CommandLine,
/// The `WAYSCRIBER_LANGUAGE` environment variable.
Environment,
/// `[ui] language` in the config.
Configured,
}
/// The language the deciding setting asks for, possibly [`Language::Auto`],
/// and which setting that is, given the configured language.
pub fn requested_language(configured: Language) -> (Language, LanguageSource) {
select(
configured,
command_line_language(),
env_var(LANGUAGE_OVERRIDE_ENV).as_deref(),
)
}
/// Start-up for a process that draws translated chrome: selects the /// Start-up for a process that draws translated chrome: selects the
/// configured language and, when it draws from a catalog, loads that catalog /// configured language and, when it draws from a catalog, loads that catalog
/// before the first surface is built. The overlay, the daemon and the About /// before the first surface is built. The overlay, the daemon and the About
@@ -61,21 +98,29 @@ pub fn initialize(language: Language) {
} }
} }
/// The selection rule: `WAYSCRIBER_LANGUAGE` beats the configured language, /// The selection rule: `--language` beats `WAYSCRIBER_LANGUAGE`, which beats
/// which beats nothing — an explicit language is already final. /// the configured language. The winner may still be `Auto`; [`resolve`]
/// decides what that draws.
/// ///
/// Pure so the precedence is testable without touching the environment. /// Pure so the precedence is testable without touching the environment.
fn select(configured: Language, override_code: Option<&str>) -> Language { fn select(
configured: Language,
command_line: Option<Language>,
override_code: Option<&str>,
) -> (Language, LanguageSource) {
if let Some(language) = command_line {
return (language, LanguageSource::CommandLine);
}
if let Some(code) = override_code { if let Some(code) = override_code {
match language_from_code(code) { match language_from_code(code) {
Some(language) => return resolve(language), Some(language) => return (language, LanguageSource::Environment),
None => { None => {
log::warn!("{LANGUAGE_OVERRIDE_ENV}={code} is not a known language; ignoring it"); log::warn!("{LANGUAGE_OVERRIDE_ENV}={code} is not a known language; ignoring it");
} }
} }
} }
resolve(configured) (configured, LanguageSource::Configured)
} }
/// The language currently in effect; never [`Language::Auto`]. /// The language currently in effect; never [`Language::Auto`].
+48 -10
View File
@@ -61,24 +61,38 @@ fn explicit_languages_never_resolve_to_auto() {
} }
#[test] #[test]
fn environment_override_beats_the_configured_language() { fn the_command_line_beats_the_environment_which_beats_the_config() {
use LanguageSource::{CommandLine, Configured, Environment};
assert_eq!( assert_eq!(
select(Language::English, Some("zh-CN")), select(Language::English, None, Some("zh-CN")),
Language::SimplifiedChinese (Language::SimplifiedChinese, Environment)
); );
assert_eq!( assert_eq!(
select(Language::SimplifiedChinese, Some("en")), select(Language::SimplifiedChinese, None, Some("en")),
Language::English (Language::English, Environment)
); );
assert_eq!(select(Language::English, None), Language::English);
assert_eq!( assert_eq!(
select(Language::SimplifiedChinese, None), select(Language::SimplifiedChinese, None, None),
Language::SimplifiedChinese (Language::SimplifiedChinese, Configured)
);
assert_eq!(
select(
Language::English,
Some(Language::SimplifiedChinese),
Some("en")
),
(Language::SimplifiedChinese, CommandLine)
);
// `--language auto` follows the desktop locale whatever the config says.
assert_eq!(
select(Language::SimplifiedChinese, Some(Language::Auto), None),
(Language::Auto, CommandLine)
); );
// An unknown override is ignored rather than silencing the UI. // An unknown override is ignored rather than silencing the UI.
assert_eq!( assert_eq!(
select(Language::English, Some("klingon")), select(Language::English, None, Some("klingon")),
Language::English (Language::English, Configured)
); );
} }
@@ -335,3 +349,27 @@ fn the_first_lookup_merges_the_override_directory() {
}, },
); );
} }
#[test]
fn the_command_line_language_holds_for_the_whole_process() {
crate::test_env::in_language_isolated_child(
concat!(
module_path!(),
"::the_command_line_language_holds_for_the_whole_process"
),
|| {
// SAFETY: this child process runs only this test.
unsafe { std::env::set_var(LANGUAGE_OVERRIDE_ENV, "en") };
set_command_line_language(Language::SimplifiedChinese);
set_language(Language::English);
assert_eq!(command_line_language(), Some(Language::SimplifiedChinese));
assert_eq!(language(), Language::SimplifiedChinese);
assert_eq!(
requested_language(Language::English),
(Language::SimplifiedChinese, LanguageSource::CommandLine)
);
},
);
}
+6 -3
View File
@@ -101,6 +101,7 @@ pub(super) fn validate(
| "WAYSCRIBER_OVERLAY_SESSION_REPORTS" | "WAYSCRIBER_OVERLAY_SESSION_REPORTS"
| "WAYSCRIBER_OVERLAY_HOME_SESSION" | "WAYSCRIBER_OVERLAY_HOME_SESSION"
| "WAYSCRIBER_OVERLAY_PREFERRED_SESSION" | "WAYSCRIBER_OVERLAY_PREFERRED_SESSION"
| "WAYSCRIBER_OVERLAY_LANGUAGE"
) { ) {
bail!("environment key {name:?} is not broker-allowed"); bail!("environment key {name:?} is not broker-allowed");
} }
@@ -272,14 +273,15 @@ fn looks_like_uri_bytes(value: &[u8]) -> bool {
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.')) && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
} }
/// Markers through which one wayscriber process tells another what it is. /// Markers through which one wayscriber process tells another what it is,
/// and how a daemon's overlay should run.
/// ///
/// The broker runs with the environment of the process that started it, so /// The broker runs with the environment of the process that started it, so
/// without this a daemon-launched overlay's markers reached every helper it /// without this a daemon-launched overlay's markers reached every helper it
/// spawned, and from there xdg-open's browser, the configurator, tesseract /// spawned, and from there xdg-open's browser, the configurator, tesseract
/// and curl. A wayscriber started anywhere in those trees then took itself /// and curl. A wayscriber started anywhere in those trees then took itself
/// for a daemon child. /// for a daemon child, or took the daemon's `--language`.
const INTERNAL_PROCESS_MARKERS: [&str; 7] = [ const INTERNAL_PROCESS_MARKERS: [&str; 8] = [
crate::env_vars::OVERLAY_CHILD_GENERATION_ENV, crate::env_vars::OVERLAY_CHILD_GENERATION_ENV,
crate::env_vars::DETACHED_ENV, crate::env_vars::DETACHED_ENV,
crate::RESUME_SESSION_ENV, crate::RESUME_SESSION_ENV,
@@ -287,6 +289,7 @@ const INTERNAL_PROCESS_MARKERS: [&str; 7] = [
crate::env_vars::OVERLAY_SESSION_REPORTS_ENV, crate::env_vars::OVERLAY_SESSION_REPORTS_ENV,
crate::env_vars::OVERLAY_HOME_SESSION_ENV, crate::env_vars::OVERLAY_HOME_SESSION_ENV,
crate::env_vars::OVERLAY_PREFERRED_SESSION_ENV, crate::env_vars::OVERLAY_PREFERRED_SESSION_ENV,
crate::env_vars::OVERLAY_LANGUAGE_ENV,
]; ];
/// Whether `kind` relaunches wayscriber itself, the only helpers that keep /// Whether `kind` relaunches wayscriber itself, the only helpers that keep
+3 -2
View File
@@ -798,10 +798,11 @@ fn owned_child_inherits_daemon_pidfd_without_leaking_broker_copy() {
#[test] #[test]
fn helpers_do_not_inherit_the_internal_process_markers() { fn helpers_do_not_inherit_the_internal_process_markers() {
const MARKERS: [&str; 3] = [ const MARKERS: [&str; 4] = [
crate::env_vars::OVERLAY_CHILD_GENERATION_ENV, crate::env_vars::OVERLAY_CHILD_GENERATION_ENV,
crate::env_vars::DETACHED_ENV, crate::env_vars::DETACHED_ENV,
crate::RESUME_SESSION_ENV, crate::RESUME_SESSION_ENV,
crate::env_vars::OVERLAY_LANGUAGE_ENV,
]; ];
let _environment = crate::test_env::lock(); let _environment = crate::test_env::lock();
let previous = MARKERS.map(std::env::var_os); let previous = MARKERS.map(std::env::var_os);
@@ -838,7 +839,7 @@ fn helpers_do_not_inherit_the_internal_process_markers() {
let output = output.unwrap(); let output = output.unwrap();
assert_eq!(output.status, 0); assert_eq!(output.status, 0);
assert_eq!(output.stdout, b"unset,unset,unset"); assert_eq!(output.stdout, b"unset,unset,unset,unset");
} }
#[test] #[test]