diff --git a/crates/ui-egui/src/dialogs/package.rs b/crates/ui-egui/src/dialogs/package.rs index 9c2b3deb9..5b49be69e 100644 --- a/crates/ui-egui/src/dialogs/package.rs +++ b/crates/ui-egui/src/dialogs/package.rs @@ -160,24 +160,11 @@ fn packaged(app: &mut VectorcraftApp, r: Result, web: bool, later let (name, data) = (r["name"].as_str().unwrap_or("package.zip"), r["dataBase64"].as_str().unwrap_or_default()); let bytes = vectorcraft_format::base64_decode(data).ok_or("the package came back damaged")?; io::write_named(app, None, name, &bytes)?; - let status = if shown_note.is_empty() { - crate::i18n::tn(n, "Packaged {n} file", "Packaged {n} files") - } else { - crate::i18n::fmt(&crate::i18n::tn(n, "Packaged {n} file {note}", "Packaged {n} files {note}"), &[("note", &shown_note)]) - }; - app.status(status); + app.status(package_status(n, None, &shown_note)); return Ok(r); } let folder = r["folder"].as_str().unwrap_or_default().to_string(); - let status = if shown_note.is_empty() { - crate::i18n::fmt(&crate::i18n::tn(n, "Packaged {n} file in {folder}", "Packaged {n} files in {folder}"), &[("folder", &folder)]) - } else { - crate::i18n::fmt( - &crate::i18n::tn(n, "Packaged {n} file in {folder} {note}", "Packaged {n} files in {folder} {note}"), - &[("folder", &folder), ("note", &shown_note)], - ) - }; - app.status(status); + app.status(package_status(n, Some(&folder), &shown_note)); let detail = if shown_note.is_empty() { crate::i18n::fmt( &crate::i18n::tn(n, "{n} file in {folder}. Show the package folder?", "{n} files in {folder}. Show the package folder?"), @@ -195,6 +182,20 @@ fn packaged(app: &mut VectorcraftApp, r: Result, web: bool, later Ok(r) } +/// The status bar's line for a package of `n` files written to `folder` (`None`: the web's +/// download), followed by `note` from [`package_status_note`] when it isn't empty. Each catalog +/// also has `@msgplural` rows for the English line, shown after a language switch; they find the note +/// after the folder by its opening `(`, so a folder name with spaces stays whole. +pub(super) fn package_status(n: u64, folder: Option<&str>, note: &str) -> String { + let (one, other) = match (folder.is_some(), note.is_empty()) { + (false, true) => ("Packaged {n} file", "Packaged {n} files"), + (false, false) => ("Packaged {n} file {note}", "Packaged {n} files {note}"), + (true, true) => ("Packaged {n} file in {folder}", "Packaged {n} files in {folder}"), + (true, false) => ("Packaged {n} file in {folder} {note}", "Packaged {n} files in {folder} {note}"), + }; + crate::i18n::fmt(&crate::i18n::tn(n, one, other), &[("folder", folder.unwrap_or_default()), ("note", note)]) +} + /// Parenthetical notes after the folder path in a package status (`(2 linked files not found)` …), /// joined with spaces and without a leading space (#3494). pub(super) fn package_status_note(missing: usize, fonts: &str) -> String { diff --git a/crates/ui-egui/src/dialogs/tests_package.rs b/crates/ui-egui/src/dialogs/tests_package.rs index 22c5e2a35..91d95d922 100644 --- a/crates/ui-egui/src/dialogs/tests_package.rs +++ b/crates/ui-egui/src/dialogs/tests_package.rs @@ -302,6 +302,40 @@ fn the_missing_links_note_uses_each_count_form() { assert_eq!(note("cs", 5), "(Nenalezeno 5 propojených souborů)"); } +/// A package status written in English, for one file or several, with or without a folder and a +/// note, is translated by every catalog when the status bar shows it after a language switch, in +/// the form the language's plural rule picks for the count, and a folder whose name has a space +/// keeps its whole name. +#[test] +fn an_english_package_status_is_translated_after_a_language_switch() { + use crate::i18n::{Lang, message}; + let mut statuses = vec![]; + for n in [1, 3] { + for folder in [None, Some("/out/card Folder")] { + for note in ["", "(fonts not copied: Mono)"] { + statuses.push(package::package_status(n, folder, note)); + } + } + } + assert!(statuses.contains(&"Packaged 1 file in /out/card Folder".to_string()), "{statuses:?}"); + assert!(statuses.contains(&"Packaged 3 files".to_string()), "{statuses:?}"); + for lang in Lang::all().filter(|l| l.code() != "en") { + for s in &statuses { + let shown = message(lang, s); + assert!(!shown.starts_with("Packaged"), "{}: {shown}", lang.code()); + // A space in the folder's name stays in the path, with or without a note. + assert_eq!(s.contains("/out/card Folder"), shown.contains("/out/card Folder"), "{}: {shown}", lang.code()); + } + } + let cs = Lang::from_code("cs").unwrap(); + assert_eq!(message(cs, "Packaged 1 file in /out"), "Zabalen 1 soubor do /out"); + assert_eq!(message(cs, "Packaged 3 files in /out"), "Zabaleny 3 soubory do /out"); + assert_eq!(message(cs, "Packaged 5 files in /out"), "Zabaleno 5 souborů do /out"); + let de = Lang::from_code("de").unwrap(); + assert_eq!(message(de, "Packaged 1 file"), "1 Datei verpackt"); + assert_eq!(message(de, "Packaged 3 files in /out"), "3 Dateien in /out verpackt"); +} + #[test] fn package_status_without_notes_has_no_extra_spaces() { let dir = Folder::new("spaces"); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index af85867d8..dee009b76 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -23,6 +23,8 @@ //! message itself stays English (the control channel, MCP and tests read it); `@msg` catalog //! rows hold whole messages or templates such as `Couldn't open {name}: {e}`, and the values in //! the placeholders are translated in turn (the reason after `: {e}` is often a message too). +//! A `@msgplural` row is a template with a count: its source holds the English `one|other` +//! forms, its translation the language's plural forms, and the `{n}` value picks the form. mod catalog; @@ -651,7 +653,10 @@ pub fn message(lang: Lang, s: &str) -> String { } fn message_at(lang: Lang, s: &str, depth: usize) -> String { - let Some((template, caps)) = lang.catalog().message(s) else { return s.to_string() }; + let Some((forms, caps)) = lang.catalog().message_forms(s) else { return s.to_string() }; + let count = caps.iter().find(|(k, _)| *k == "n").and_then(|(_, v)| v.parse::().ok()); + let form = count.map_or(forms.len().saturating_sub(1), |n| (lang.0.plural)(n).min(forms.len().saturating_sub(1))); + let Some(template) = forms.get(form) else { return s.to_string() }; let values: Vec<(&str, String)> = caps.into_iter().map(|(k, v)| (k, if depth > 1 { message_at(lang, v, depth - 1) } else { v.to_string() })).collect(); let args: Vec<(&str, &str)> = values.iter().map(|(k, v)| (*k, v.as_str())).collect(); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index fd7a17751..0afca3ee6 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3833,7 +3833,7 @@ axis Both И двете @msg PDF settings are not an object Настройките за PDF не са обект @msg PDF security settings are not an object Настройките за сигурност на PDF не са обект @msg PDF writer error: {0} Грешка на PDF записа: {0} -@msg Packaged {n} file(s) in {folder} {note} Пакетирани са {n} файла в {folder}{note} +@msgplural Packaged {n} file in {folder} ({note}|Packaged {n} files in {folder} ({note} Пакетиран е {n} файл в {folder} ({note}|Пакетирани са {n} файла в {folder} ({note} @msg Packaged {n} file(s){note} Пакетирани са {n} файла{note} @msg Preview in Browser needs the desktop app (the web downloads files instead) Прегледът в браузър изисква настолното приложение (уеб версията изтегля файловете вместо това) @msg Release Guides: no guides Освобождаване на водачи: няма водачи @@ -5504,9 +5504,9 @@ theme Appearance Външен вид Slope: Наклон: axis X Axis X ос axis Y Axis Y ос -@msg Packaged {n} file(s) in {folder} Пакетирани са {n} файла в {folder} -@msg Packaged {n} file(s) {note} Пакетирани са {n} файла {note} -@msg Packaged {n} file(s) Пакетирани са {n} файла +@msgplural Packaged {n} file in {folder}|Packaged {n} files in {folder} Пакетиран е {n} файл в {folder}|Пакетирани са {n} файла в {folder} +@msgplural Packaged {n} file {note}|Packaged {n} files {note} Пакетиран е {n} файл {note}|Пакетирани са {n} файла {note} +@msgplural Packaged {n} file|Packaged {n} files Пакетиран е {n} файл|Пакетирани са {n} файла @plural Packaged {n} file|Packaged {n} files Пакетиран е {n} файл|Пакетирани са {n} файла @plural Packaged {n} file in {folder}|Packaged {n} files in {folder} Пакетиран е {n} файл в {folder}|Пакетирани са {n} файла в {folder} @plural Packaged {n} file {note}|Packaged {n} files {note} Пакетиран е {n} файл {note}|Пакетирани са {n} файла {note} diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 0ff4eeeb6..9ce6565b9 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -4199,7 +4199,7 @@ imageTrace Outline Obris @msg PDF settings are not an object PDF postavke nisu objekat @msg PDF security settings are not an object PDF sigurnosne postavke nisu objekat @msg PDF writer error: {0} greška PDF zapisivača: {0} -@msg Packaged {n} file(s) in {folder} {note} Upakovano datoteka u {folder}: {n} {note} +@msgplural Packaged {n} file in {folder} ({note}|Packaged {n} files in {folder} ({note} Upakovano datoteka u {folder}: {n} ({note} @msg Packaged {n} file(s){note} Upakovano datoteka: {n}{note} @msg Preview in Browser needs the desktop app (the web downloads files instead) Pregledaj u pregledniku zahtijeva desktop aplikaciju (web umjesto toga preuzima datoteke) @msg Release Guides: no guides Oslobodi vodilice: nema vodilica @@ -5524,9 +5524,9 @@ mark-style Japanese Japanski Slope: Nagib: axis X Axis X osa axis Y Axis Y osa -@msg Packaged {n} file(s) in {folder} Upakovano datoteka u {folder}: {n} -@msg Packaged {n} file(s) {note} Upakovano datoteka: {n} {note} -@msg Packaged {n} file(s) Upakovano datoteka: {n} +@msgplural Packaged {n} file in {folder}|Packaged {n} files in {folder} Upakovano datoteka u {folder}: {n} +@msgplural Packaged {n} file {note}|Packaged {n} files {note} Upakovano datoteka: {n} {note} +@msgplural Packaged {n} file|Packaged {n} files Upakovano datoteka: {n} @plural Packaged {n} file|Packaged {n} files Upakovano datoteka: {n}|Upakovano datoteka: {n}|Upakovano datoteka: {n} @plural Packaged {n} file in {folder}|Packaged {n} files in {folder} Upakovano datoteka u {folder}: {n}|Upakovano datoteka u {folder}: {n}|Upakovano datoteka u {folder}: {n} @plural Packaged {n} file {note}|Packaged {n} files {note} Upakovano datoteka: {n} {note}|Upakovano datoteka: {n} {note}|Upakovano datoteka: {n} {note} diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 04608e692..f6e37996d 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5258,10 +5258,10 @@ @msg Opened with {_1} note(s): {_2} Obert amb {_1} nota(es): {_2} @msg Opened {url} Obert {url} @msg Outline Stroke: no stroked paths selected Contorneja el traç: no hi ha cap traçat amb traç seleccionat -@msg Packaged {n} file(s) Empaquetat {n} fitxer(s) -@msg Packaged {n} file(s) in {folder} Empaquetats {n} fitxer(s) a {folder} -@msg Packaged {n} file(s) in {folder} {note} Empaquetats {n} fitxer(s) a {folder} {note} -@msg Packaged {n} file(s) {note} Empaquetats {n} fitxer(s) {note} +@msgplural Packaged {n} file|Packaged {n} files S’ha empaquetat {n} fitxer|S’han empaquetat {n} fitxers +@msgplural Packaged {n} file in {folder}|Packaged {n} files in {folder} S’ha empaquetat {n} fitxer a {folder}|S’han empaquetat {n} fitxers a {folder} +@msgplural Packaged {n} file in {folder} ({note}|Packaged {n} files in {folder} ({note} S’ha empaquetat {n} fitxer a {folder} ({note}|S’han empaquetat {n} fitxers a {folder} ({note} +@msgplural Packaged {n} file {note}|Packaged {n} files {note} S’ha empaquetat {n} fitxer {note}|S’han empaquetat {n} fitxers {note} @msg Packaged {n} file(s){note} Empaquetats {n} fitxer(s){note} @msg Packaging {_1} Empaquetant {_1} @msg page size {w} × {h} mida de pàgina {w} × {h} diff --git a/crates/ui-egui/src/i18n/catalog.rs b/crates/ui-egui/src/i18n/catalog.rs index a79ce351a..1be460212 100644 --- a/crates/ui-egui/src/i18n/catalog.rs +++ b/crates/ui-egui/src/i18n/catalog.rs @@ -15,7 +15,8 @@ pub struct Catalog { plurals: HashMap>, /// status and error messages without placeholders (`@msg`): English → translation messages: HashMap, - /// status and error messages with `{name}` placeholders (`@msg`), most specific first + /// status and error messages with `{name}` placeholders (`@msg`, and each English form of a + /// `@msgplural` row), most specific first templates: Vec