mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
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:
1 parent
98b6f54c15
commit
fcbc437816
24 files changed
+678
-54
No files matched your search
@@ -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`.
|
||||
|
||||
### 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
|
||||
@@ -1329,6 +1356,7 @@ Common toggles:
|
||||
- `WAYSCRIBER_TOOLBAR_BACKEND=auto|gtk|builtin` overrides the toolbar frontend (default: auto)
|
||||
- `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_LANGUAGE=zh-CN` uses an interface language for this run (overrides `[ui] language`; `--language` overrides it)
|
||||
|
||||
See `docs/CONFIG.md` for the full list.
|
||||
|
||||
|
||||
+2
-1
@@ -421,7 +421,8 @@ reduced_motion = "auto"
|
||||
# (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
|
||||
# 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"
|
||||
|
||||
# Show the status bar and its configured contents
|
||||
|
||||
@@ -117,10 +117,9 @@ impl LanguageOption {
|
||||
/// must still recognize their own language to get back. Only the
|
||||
/// automatic choice, which is not a language, follows the UI.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
LanguageOption::Auto => tr("Automatic (system locale)"),
|
||||
LanguageOption::English => "English",
|
||||
LanguageOption::SimplifiedChinese => "简体中文",
|
||||
match self.to_language().native_name() {
|
||||
Some(name) => name,
|
||||
None => tr("Automatic (system locale)"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -1137,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 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:**
|
||||
- **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_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_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)
|
||||
|
||||
+27
-7
@@ -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
|
||||
`~/.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` entirely, in every
|
||||
process that sees it, so a translation can be checked without editing
|
||||
`config.toml`.
|
||||
- `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.
|
||||
|
||||
@@ -129,7 +144,9 @@ to decide what to do.
|
||||
|
||||
### 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;
|
||||
- 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.
|
||||
@@ -156,14 +173,17 @@ 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 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
|
||||
`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.
|
||||
4. `configurator/src/models/fields/theme.rs`: a `LanguageOption` for the
|
||||
variant.
|
||||
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.
|
||||
|
||||
@@ -115,6 +115,22 @@ fn run_about_event_loop(
|
||||
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
|
||||
/// 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
|
||||
@@ -198,6 +214,28 @@ mod tests {
|
||||
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]
|
||||
fn the_dialog_reads_its_language_with_its_theme() {
|
||||
with_temp_config_home(|_| {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod env;
|
||||
mod languages;
|
||||
mod session;
|
||||
mod usage;
|
||||
|
||||
@@ -203,6 +204,16 @@ pub fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
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
|
||||
// acquiring locks or starting threads. The guard spans the complete run.
|
||||
let _process_broker = needs_process_broker(&cli)
|
||||
|
||||
@@ -217,6 +217,10 @@ pub(crate) fn print_usage() {
|
||||
println!(" wayscriber --no-tray Skip system tray (headless daemon)");
|
||||
println!(" wayscriber --about Show the About window");
|
||||
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!(
|
||||
" wayscriber --resume-session Force session resume on (all boards/history/tool state)"
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ impl WaylandState {
|
||||
match spawn_detached(
|
||||
crate::process_broker::HelperKind::About,
|
||||
executable.as_os_str(),
|
||||
&["--about".into()],
|
||||
&crate::about_window::launch_arguments(),
|
||||
) {
|
||||
Ok(child) => {
|
||||
log::info!("Launched About window (pid {})", child.id());
|
||||
|
||||
+62
-2
@@ -1,6 +1,7 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::domain::Language;
|
||||
use crate::tray_action::TrayAction;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -76,6 +77,13 @@ pub struct Cli {
|
||||
|
||||
/// Print compiled runtime capabilities for companion tools
|
||||
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)]
|
||||
@@ -151,6 +159,11 @@ impl Cli {
|
||||
"--no-resume-session" => cli.no_resume_session = true,
|
||||
"--about" => cli.about = 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 => {
|
||||
cli.runtime_capabilities = true;
|
||||
}
|
||||
@@ -167,6 +180,9 @@ impl Cli {
|
||||
_ if arg.starts_with("--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) => {
|
||||
match parse_short_option_cluster(&args, index, &mut cli)? {
|
||||
ShortOptionOutcome::Continue(next_index) => index = next_index,
|
||||
@@ -261,12 +277,17 @@ impl Cli {
|
||||
self.validate_session_flag_pairs()?;
|
||||
self.validate_overlay_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> {
|
||||
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());
|
||||
}
|
||||
@@ -462,8 +483,44 @@ impl Cli {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// `--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 {
|
||||
@@ -563,6 +620,7 @@ pub(crate) fn print_help() {
|
||||
println!(" wayscriber --clear-tool-state [--session-file PATH]");
|
||||
println!(" wayscriber --about");
|
||||
println!(" wayscriber --check-update");
|
||||
println!(" wayscriber --list-languages");
|
||||
println!();
|
||||
println!("Options:");
|
||||
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!(" --about Show the About window");
|
||||
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!(" -V, --version Show version");
|
||||
}
|
||||
|
||||
@@ -506,3 +506,98 @@ fn check_update_rejects_valued_launch_flags() {
|
||||
"--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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@ use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::daemon::control::DaemonToggleRequest;
|
||||
use crate::domain::Language;
|
||||
use crate::env_vars::{
|
||||
DESKTOP_STARTUP_ID_ENV, NO_DETACH_ENV, OVERLAY_HOME_SESSION_ENV, OVERLAY_PREFERRED_SESSION_ENV,
|
||||
OVERLAY_SESSION_REPORTS_ENV, XDG_ACTIVATION_TOKEN_ENV,
|
||||
DESKTOP_STARTUP_ID_ENV, NO_DETACH_ENV, OVERLAY_HOME_SESSION_ENV, OVERLAY_LANGUAGE_ENV,
|
||||
OVERLAY_PREFERRED_SESSION_ENV, OVERLAY_SESSION_REPORTS_ENV, XDG_ACTIVATION_TOKEN_ENV,
|
||||
};
|
||||
|
||||
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
|
||||
/// daemon, which passes the session it remembers from the last report as the
|
||||
/// 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(
|
||||
request: &OverlayLaunchRequest,
|
||||
resume_default: Option<bool>,
|
||||
generation: Option<&str>,
|
||||
remembered_session_file: Option<&Path>,
|
||||
language: Option<Language>,
|
||||
) -> OverlayLaunch {
|
||||
let mut arguments = vec![OsString::from("--active")];
|
||||
if request.freeze {
|
||||
@@ -134,6 +141,10 @@ pub(super) fn build_overlay_launch(
|
||||
.filter(|_| request.explicit_session_file.is_none())
|
||||
.map(Into::into),
|
||||
),
|
||||
(
|
||||
OVERLAY_LANGUAGE_ENV.into(),
|
||||
language.map(|language| language.code().into()),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(mode) = request.mode() {
|
||||
@@ -180,7 +191,7 @@ mod tests {
|
||||
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()))];
|
||||
if let Some(generation) = generation {
|
||||
@@ -202,6 +213,7 @@ mod tests {
|
||||
),
|
||||
(OVERLAY_HOME_SESSION_ENV.into(), None),
|
||||
(OVERLAY_PREFERRED_SESSION_ENV.into(), None),
|
||||
(OVERLAY_LANGUAGE_ENV.into(), None),
|
||||
]);
|
||||
assert_eq!(launch.environment, expected);
|
||||
}
|
||||
@@ -226,7 +238,8 @@ mod tests {
|
||||
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| {
|
||||
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")]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ impl OverlayLifecycle {
|
||||
crate::decode_session_override(resume_override.load(Ordering::Acquire)),
|
||||
self.child.generation(),
|
||||
remembered_session_file,
|
||||
crate::i18n::command_line_language(),
|
||||
);
|
||||
|
||||
let attempt = (|| -> Result<u32> {
|
||||
|
||||
@@ -176,7 +176,7 @@ mod tests {
|
||||
|
||||
fn build_test_launch(daemon: &mut Daemon) -> OverlayLaunch {
|
||||
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> {
|
||||
|
||||
@@ -166,6 +166,34 @@ fn wait_until_retired(daemon: &mut Daemon) {
|
||||
const HOME: &str = "/tmp/home.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]
|
||||
fn the_next_show_continues_the_session_the_overlay_reported() {
|
||||
with_fixture(false, |daemon, _, root| {
|
||||
@@ -173,6 +201,7 @@ fn the_next_show_continues_the_session_the_overlay_reported() {
|
||||
let first = show(daemon, root);
|
||||
assert_eq!(first["reports"], "1");
|
||||
assert_eq!(first["home"], HOME);
|
||||
assert!(first["language"].is_null());
|
||||
assert!(first["preferred"].is_null());
|
||||
let generation = first["generation"].as_str().unwrap();
|
||||
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());
|
||||
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!(
|
||||
actual.arguments,
|
||||
|
||||
@@ -142,6 +142,7 @@ fn write_receipt() -> Result<()> {
|
||||
"reports": launched_with(crate::env_vars::OVERLAY_SESSION_REPORTS_ENV),
|
||||
"home": launched_with(crate::env_vars::OVERLAY_HOME_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(),
|
||||
"generation": std::env::var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV)?,
|
||||
});
|
||||
|
||||
@@ -117,7 +117,7 @@ impl WayscriberTray {
|
||||
match spawn_detached(
|
||||
crate::process_broker::HelperKind::About,
|
||||
exe.as_os_str(),
|
||||
&["--about".into()],
|
||||
&crate::about_window::launch_arguments(),
|
||||
) {
|
||||
Ok(child) => {
|
||||
info!("Launched About window (pid {})", child.id());
|
||||
|
||||
+33
-5
@@ -24,6 +24,10 @@ pub enum 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.
|
||||
pub fn code(self) -> &'static str {
|
||||
match self {
|
||||
@@ -46,6 +50,26 @@ impl Language {
|
||||
pub fn is_auto(self) -> bool {
|
||||
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)]
|
||||
@@ -54,11 +78,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn language_codes_round_trip_through_serde() {
|
||||
for language in [
|
||||
Language::Auto,
|
||||
Language::English,
|
||||
Language::SimplifiedChinese,
|
||||
] {
|
||||
for language in Language::ALL {
|
||||
let encoded = serde_json::to_string(&language).expect("serialize language");
|
||||
assert_eq!(encoded, format!("\"{}\"", language.code()));
|
||||
let decoded: Language = serde_json::from_str(&encoded).expect("deserialize language");
|
||||
@@ -81,4 +101,12 @@ mod tests {
|
||||
fn default_language_follows_the_locale() {
|
||||
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("简体中文"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// still a usable session file.
|
||||
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";
|
||||
/// Disables the periodic update check regardless of `[updates] check`.
|
||||
pub const DISABLE_UPDATE_CHECK_ENV: &str = "WAYSCRIBER_DISABLE_UPDATE_CHECK";
|
||||
|
||||
+56
-11
@@ -35,20 +35,57 @@ type Catalog = BTreeMap<String, String>;
|
||||
/// Atoms: English is 0 so a never-configured process needs no catalog.
|
||||
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.
|
||||
///
|
||||
/// `Auto` resolves through the locale chain, and `WAYSCRIBER_LANGUAGE`
|
||||
/// overrides the argument entirely — a testing aid for checking a translation
|
||||
/// without editing `config.toml`. Each process calls this before it builds
|
||||
/// translated chrome; the configurator calls it again whenever the language
|
||||
/// of its draft changes, to preview that language.
|
||||
/// `Auto` resolves through the locale chain. `--language` and then
|
||||
/// `WAYSCRIBER_LANGUAGE` override the argument entirely, for one run, without
|
||||
/// editing `config.toml`. Each process calls this before it builds translated
|
||||
/// chrome; the configurator calls it again whenever the language of its draft
|
||||
/// changes, to preview that 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);
|
||||
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
|
||||
/// 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
|
||||
@@ -61,21 +98,29 @@ pub fn initialize(language: Language) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The selection rule: `WAYSCRIBER_LANGUAGE` beats the configured language,
|
||||
/// which beats nothing — an explicit language is already final.
|
||||
/// The selection rule: `--language` beats `WAYSCRIBER_LANGUAGE`, which beats
|
||||
/// the configured language. The winner may still be `Auto`; [`resolve`]
|
||||
/// decides what that draws.
|
||||
///
|
||||
/// 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 {
|
||||
match language_from_code(code) {
|
||||
Some(language) => return resolve(language),
|
||||
Some(language) => return (language, LanguageSource::Environment),
|
||||
None => {
|
||||
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`].
|
||||
|
||||
+48
-10
@@ -61,24 +61,38 @@ fn explicit_languages_never_resolve_to_auto() {
|
||||
}
|
||||
|
||||
#[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!(
|
||||
select(Language::English, Some("zh-CN")),
|
||||
Language::SimplifiedChinese
|
||||
select(Language::English, None, Some("zh-CN")),
|
||||
(Language::SimplifiedChinese, Environment)
|
||||
);
|
||||
assert_eq!(
|
||||
select(Language::SimplifiedChinese, Some("en")),
|
||||
Language::English
|
||||
select(Language::SimplifiedChinese, None, Some("en")),
|
||||
(Language::English, Environment)
|
||||
);
|
||||
assert_eq!(select(Language::English, None), Language::English);
|
||||
assert_eq!(
|
||||
select(Language::SimplifiedChinese, None),
|
||||
Language::SimplifiedChinese
|
||||
select(Language::SimplifiedChinese, None, None),
|
||||
(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.
|
||||
assert_eq!(
|
||||
select(Language::English, Some("klingon")),
|
||||
Language::English
|
||||
select(Language::English, None, Some("klingon")),
|
||||
(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)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -101,6 +101,7 @@ pub(super) fn validate(
|
||||
| "WAYSCRIBER_OVERLAY_SESSION_REPORTS"
|
||||
| "WAYSCRIBER_OVERLAY_HOME_SESSION"
|
||||
| "WAYSCRIBER_OVERLAY_PREFERRED_SESSION"
|
||||
| "WAYSCRIBER_OVERLAY_LANGUAGE"
|
||||
) {
|
||||
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'.'))
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// without this a daemon-launched overlay's markers reached every helper it
|
||||
/// spawned, and from there xdg-open's browser, the configurator, tesseract
|
||||
/// and curl. A wayscriber started anywhere in those trees then took itself
|
||||
/// for a daemon child.
|
||||
const INTERNAL_PROCESS_MARKERS: [&str; 7] = [
|
||||
/// for a daemon child, or took the daemon's `--language`.
|
||||
const INTERNAL_PROCESS_MARKERS: [&str; 8] = [
|
||||
crate::env_vars::OVERLAY_CHILD_GENERATION_ENV,
|
||||
crate::env_vars::DETACHED_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_HOME_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
|
||||
|
||||
@@ -798,10 +798,11 @@ fn owned_child_inherits_daemon_pidfd_without_leaking_broker_copy() {
|
||||
|
||||
#[test]
|
||||
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::DETACHED_ENV,
|
||||
crate::RESUME_SESSION_ENV,
|
||||
crate::env_vars::OVERLAY_LANGUAGE_ENV,
|
||||
];
|
||||
let _environment = crate::test_env::lock();
|
||||
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();
|
||||
assert_eq!(output.status, 0);
|
||||
assert_eq!(output.stdout, b"unset,unset,unset");
|
||||
assert_eq!(output.stdout, b"unset,unset,unset,unset");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user