diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 2ea5cfae0..c8016f34b 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -175,6 +175,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - } "engine.commands" => { let mut v: Vec = app.session.commands().into_iter().map(|c| serde_json::to_value(c).unwrap_or_default()).collect(); + let language_params = format!("{{language: {}}} (alias: code)", crate::i18n::Language::code_list("|")); for (id, label, params) in [ ("ui.ribbonTab", "Ribbon Tab", ""), ("view.collapseRibbon", "Collapse Ribbon", ""), @@ -195,7 +196,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - "Submit Print Job", "existing file.print sheet/pagination params → {printing,bytes,pending}; submits without opening a dialog", ), - ("app.language.set", "Interface Language", "{language: \"en\"|\"zh\"|\"ja\"|\"ko\"|\"ru\"|\"pt\"|\"de\"|\"cs\"} (alias: code)"), + ("app.language.set", "Interface Language", language_params.as_str()), ("app.language.english", "Interface Language: English", ""), ("app.language.japanese", "Interface Language: Japanese", ""), ] { @@ -530,4 +531,19 @@ mod tests { assert!(!app.printing.busy(), "discovery must not submit a job"); assert!(app.backstage.is_none(), "discovery must not open settings"); } + + #[test] + fn language_command_docs_list_every_language() { + let mut session = gridcraft_engine::Session::new(); + session.new_workbook(); + let mut app = SheetApp::new(session, crate::Services::default()); + let Outcome::Done(reply) = handle(&mut app, &egui::Context::default(), "engine.commands", &json!({"search":"language"})) else { + panic!("command discovery must return immediately"); + }; + let commands = reply["result"].as_array().unwrap(); + let docs = commands.iter().find(|c| c["id"] == "app.language.set").unwrap()["params"].as_str().unwrap(); + for l in crate::i18n::Language::ALL { + assert!(docs.contains(&format!("\"{}\"", l.code())), "app.language.set docs miss {:?}: {docs}", l.code()); + } + } } diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 44bc34d5b..be92fd779 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -78,6 +78,12 @@ impl Language { Self::from_tag(code) } + /// Every code `app.language.set` takes, quoted and joined (`"en"|"es"|…`), for command docs + /// and errors. + pub fn code_list(separator: &str) -> String { + Self::ALL.iter().map(|l| format!("\"{}\"", l.code())).collect::>().join(separator) + } + fn from_tag(tag: &str) -> Option { match tag.trim().split(['-', '_']).next()?.to_ascii_lowercase().as_str() { "en" => Some(Self::En), @@ -774,6 +780,10 @@ mod tests { app.run("app.language.set", serde_json::json!({"code": "ja"})).unwrap(); assert_eq!(app.ui.language, Language::Ja, "`code` is an alias for `language`"); assert!(app.run("app.language.set", serde_json::json!({})).is_err(), "no language is an error, not a dialog"); + let unknown = app.run("app.language.set", serde_json::json!({"language": "xx"})).unwrap_err().to_string(); + for l in Language::ALL { + assert!(unknown.contains(&format!("\"{}\"", l.code())), "the error lists {:?}: {unknown}", l.code()); + } app.run("app.language.english", serde_json::json!({})).unwrap(); assert_eq!(app.ui.language, Language::En); for l in Language::ALL { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1b08f5d7f..86e543120 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -658,7 +658,7 @@ impl SheetApp { self.ui.language = l; Ok(json!({"language": l})) } - None => Err(format!("unknown language {code:?} (use \"en\", \"es\", \"zh\", \"ja\", \"ko\", \"ru\", \"pt\" or \"cs\")")), + None => Err(format!("unknown language {code:?} (use one of {})", i18n::Language::code_list(", "))), } } "app.language.english" => { diff --git a/docs/control-protocol.md b/docs/control-protocol.md index e82ae7299..af73ca515 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -56,8 +56,9 @@ the View ▸ Animations checkbox, saved with the other UI preferences and on by `{command: "app.language.set", params: {language: "ja"}}` switches the interface language (`code` is accepted as an alias for `language`); `app.language.english` and -`app.language.japanese` are shortcuts. Only interface chrome is translated, never command ids or -document text. The choice is saved in `ui.json`; with nothing saved the desktop app follows the +`app.language.japanese` are shortcuts. `engine.commands` lists every code it takes (`en`, `es`, +`zh`, `ja`, `ko`, `ru`, `pt`, `de`, `cs`, `fr`, `sr`), as does the error for an unknown one. Only +interface chrome is translated, never command ids or document text. The choice is saved in `ui.json`; with nothing saved the desktop app follows the system language. `{command: "file.open", params: {path: "Secret.xlsx", password: "…"}}` opens a password-protected