diff --git a/crates/ui-egui/src/dialogs/missing_fonts.rs b/crates/ui-egui/src/dialogs/missing_fonts.rs index 16fa10bea..205e4fa98 100644 --- a/crates/ui-egui/src/dialogs/missing_fonts.rs +++ b/crates/ui-egui/src/dialogs/missing_fonts.rs @@ -337,19 +337,21 @@ pub(super) fn progress(ui: &mut egui::Ui, d: &Dialog) { widgets::dim_name(ui, &crate::i18n::message(crate::i18n::current(), e)); } if let Some(s) = d.fields.get("searched") { - let (folders, files) = (s["folders"].to_string(), s["files"].to_string()); - widgets::dim_name( - ui, - &crate::i18n::fmt(tl!("{folders} folder(s) and {files} file(s) searched"), &[("folders", &folders), ("files", &files)]), - ); + let folders = crate::i18n::tn(s["folders"].as_u64().unwrap_or(0), "{n} folder", "{n} folders"); + let files = crate::i18n::tn(s["files"].as_u64().unwrap_or(0), "{n} file", "{n} files"); + widgets::dim_name(ui, &crate::i18n::fmt(tl!("{folders} and {files} searched"), &[("folders", &folders), ("files", &files)])); } - let count = |k: &str| d.fields.get(k).and_then(Value::as_u64).filter(|n| *n > 0).map(|n| n.to_string()); + let count = |k: &str| d.fields.get(k).and_then(Value::as_u64).filter(|n| *n > 0); if let Some(n) = count("skipped") { - let text = tl!("{count} folder(s) skipped, such as other apps' and the system's folders and hidden ones"); - widgets::dim_name(ui, &crate::i18n::fmt(text, &[("count", &n)])); + let text = crate::i18n::tn( + n, + "{n} folder skipped, such as another app's or the system's folder or a hidden one", + "{n} folders skipped, such as other apps' and the system's folders and hidden ones", + ); + widgets::dim_name(ui, &text); } if let Some(n) = count("unreadable") { - widgets::dim_name(ui, &crate::i18n::fmt(tl!("{count} folder(s) or file(s) couldn't be read"), &[("count", &n)])); + widgets::dim_name(ui, &crate::i18n::tn(n, "{n} folder or file couldn't be read", "{n} folders or files couldn't be read")); } if matches!(d.fields.get("stopped").and_then(Value::as_str), Some("time" | "limit")) { widgets::dim_label(ui, tl!("The search stopped at its limit: search a smaller folder.")); diff --git a/crates/ui-egui/src/dialogs/missing_fonts/tests.rs b/crates/ui-egui/src/dialogs/missing_fonts/tests.rs index 20266d97b..c2b2a4e6c 100644 --- a/crates/ui-egui/src/dialogs/missing_fonts/tests.rs +++ b/crates/ui-egui/src/dialogs/missing_fonts/tests.rs @@ -328,6 +328,29 @@ fn files_go_with_their_fonts_by_name() { assert_eq!((d.str("state"), &d.fields["chosen"]), ("done".to_string(), &json!([]))); } +/// A folder search's counts use the forms the language's plural rule picks for each count. +#[test] +fn search_counts_use_each_count_form() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let mut progress_text = |code: &str, n: u64| { + crate::i18n::set_current(crate::i18n::Lang::from_code(code).unwrap()); + let d = Dialog::new(KIND, json!({"state": "done", "searched": {"folders": n, "files": n}, "unreadable": n})); + let text = crate::tests_labels::painted_text(&mut app, |_, ui| progress(ui, &d)); + crate::i18n::set_current(crate::i18n::Lang::EN); + text + }; + let text = progress_text("en", 1); + assert!(text.contains("1 folder and 1 file searched") && text.contains("1 folder or file couldn't be read"), "{text}"); + for (n, searched, unreadable) in [ + (1, "Prohledáno: 1 složka, 1 soubor", "Nepodařilo se přečíst 1 složku nebo soubor"), + (2, "Prohledáno: 2 složky, 2 soubory", "Nepodařilo se přečíst 2 složky nebo soubory"), + (5, "Prohledáno: 5 složek, 5 souborů", "Nepodařilo se přečíst 5 složek nebo souborů"), + ] { + let text = progress_text("cs", n); + assert!(text.contains(searched) && text.contains(unreadable), "{n}: {text}"); + } +} + #[test] fn the_web_says_how_many_fonts_are_missing() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/dialogs/missing_links.rs b/crates/ui-egui/src/dialogs/missing_links.rs index b71705769..fe6a80486 100644 --- a/crates/ui-egui/src/dialogs/missing_links.rs +++ b/crates/ui-egui/src/dialogs/missing_links.rs @@ -75,17 +75,23 @@ pub(super) fn ask_next(app: &mut VectorcraftApp, rest: Vec, modified: Vec /// With Update Links: Ask When Modified, offer to read the modified linked files `ids` again. fn ask_update(app: &mut VectorcraftApp, ids: Vec) { - ask(app, ids, tl!("{count} linked image(s) changed since this document was saved. Show the new versions?")); + ask( + app, + ids, + "{n} linked image changed since this document was saved. Show the new version?", + "{n} linked images changed since this document was saved. Show the new versions?", + ); } /// The same offer for linked files another app changed while the document is open /// ([`vectorcraft_engine::link_watch`]). #[cfg(not(target_arch = "wasm32"))] pub(crate) fn ask_update_changed(app: &mut VectorcraftApp, ids: Vec) { - ask(app, ids, tl!("{count} linked image(s) changed on disk. Show the new versions?")); + ask(app, ids, "{n} linked image changed on disk. Show the new version?", "{n} linked images changed on disk. Show the new versions?"); } -fn ask(app: &mut VectorcraftApp, ids: Vec, question: &str) { +/// Ask about the modified linked files `ids`; `one` and `other` are the English forms of the question. +fn ask(app: &mut VectorcraftApp, ids: Vec, one: &str, other: &str) { if ids.is_empty() { return; } @@ -93,7 +99,7 @@ fn ask(app: &mut VectorcraftApp, ids: Vec, question: &str) { app.status(format!("{} linked image(s) changed on disk: Update Links shows the new versions", ids.len())); return; } - let detail = crate::i18n::fmt(question, &[("count", &ids.len().to_string())]); + let detail = crate::i18n::tn(ids.len() as u64, one, other); super::confirm::ask(app, tl!("Update Modified Links"), &detail, "links.update", json!({ "ids": ids })); } @@ -133,7 +139,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { if rest > 0 { ui.add_space(10.0); let all = d.bool("applyToAll"); - let label = crate::i18n::fmt(tl!("Apply to All ({count} more missing)"), &[("count", &rest.to_string())]); + let label = crate::i18n::tn(rest as u64, "Apply to All ({n} more missing)", "Apply to All ({n} more missing)"); if widgets::check(ui, &label, all, true) { d.fields.insert("applyToAll".into(), json!(!all)); } @@ -262,6 +268,24 @@ mod tests { let _ = std::fs::remove_dir_all(dir); } + /// The question about modified linked files uses the form the language's plural rule picks for + /// the count. + #[test] + fn the_modified_links_question_uses_each_count_form() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let mut question = |code: &str, n: u64| { + crate::i18n::set_current(crate::i18n::Lang::from_code(code).unwrap()); + ask_update(&mut app, (0..n).map(|id| json!(id)).collect()); + let detail = app.ui.dialog.take().map(|d| d.str("detail")); + crate::i18n::set_current(crate::i18n::Lang::EN); + detail.unwrap_or_default() + }; + assert_eq!(question("en", 1), "1 linked image changed since this document was saved. Show the new version?"); + assert_eq!(question("cs", 1), "Od uložení dokumentu se změnil 1 propojený obrázek. Zobrazit novou verzi?"); + assert_eq!(question("cs", 2), "Od uložení dokumentu se změnily 2 propojené obrázky. Zobrazit nové verze?"); + assert_eq!(question("cs", 5), "Od uložení dokumentu se změnilo 5 propojených obrázků. Zobrazit nové verze?"); + } + #[test] fn apply_to_all_replaces_the_others_from_the_same_folder() { let (mut app, dir, _) = opened_without_links("all"); diff --git a/crates/ui-egui/src/dialogs/package.rs b/crates/ui-egui/src/dialogs/package.rs index 839f76383..2cc994be4 100644 --- a/crates/ui-egui/src/dialogs/package.rs +++ b/crates/ui-egui/src/dialogs/package.rs @@ -155,32 +155,25 @@ fn packaged(app: &mut VectorcraftApp, r: Result, web: bool, later // The fonts left out: their license doesn't allow it, or they aren't on this computer. let fonts = r["skippedFonts"].as_array().into_iter().flatten().filter_map(|f| f["font"].as_str()).collect::>().join(", "); let shown_note = package_status_note(missing, &fonts); - let n_s = n.to_string(); + let n = n as u64; if web { 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::fmt(tl!("Packaged {n} file(s)"), &[("n", &n_s)]) - } else { - crate::i18n::fmt(tl!("Packaged {n} file(s) {note}"), &[("n", &n_s), ("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(tl!("Packaged {n} file(s) in {folder}"), &[("n", &n_s), ("folder", &folder)]) - } else { - crate::i18n::fmt(tl!("Packaged {n} file(s) in {folder} {note}"), &[("n", &n_s), ("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(tl!("{count} file(s) in {folder}. Show the package folder?"), &[("count", &n_s), ("folder", &folder)]) + crate::i18n::fmt( + &crate::i18n::tn(n, "{n} file in {folder}. Show the package folder?", "{n} files in {folder}. Show the package folder?"), + &[("folder", &folder)], + ) } else { crate::i18n::fmt( - tl!("{count} file(s) in {folder} {note}. Show the package folder?"), - &[("count", &n_s), ("folder", &folder), ("note", &shown_note)], + &crate::i18n::tn(n, "{n} file in {folder} {note}. Show the package folder?", "{n} files in {folder} {note}. Show the package folder?"), + &[("folder", &folder), ("note", &shown_note)], ) }; if app.ui.dialog.is_none() { @@ -189,12 +182,27 @@ fn packaged(app: &mut VectorcraftApp, r: Result, web: bool, later Ok(r) } -/// Parenthetical notes after the folder path in a package status (`(2 linked file(s) not found)` …), +/// The status bar's line for a package of `n` files written to `folder` (`None`: the web's +/// download), with `note` from [`package_status_note`] when it isn't empty. With a note and a +/// folder, the note comes before the folder and the folder ends the line. Each catalog also has +/// `@msgplural` rows for the English line, shown after a language switch; the folder takes the rest +/// of the line after `) in `, so a folder name with spaces or parentheses 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 {note} in {folder}", "Packaged {n} files {note} in {folder}"), + }; + crate::i18n::fmt(&crate::i18n::tn(n, one, other), &[("folder", folder.unwrap_or_default()), ("note", note)]) +} + +/// Parenthetical notes in a package status (`(2 linked files not found)` …), /// joined with spaces and without a leading space (#3494). -fn package_status_note(missing: usize, fonts: &str) -> String { +pub(super) fn package_status_note(missing: usize, fonts: &str) -> String { let mut parts = Vec::new(); if missing > 0 { - parts.push(format!("({})", crate::i18n::fmt(tl!("{count} linked file(s) not found"), &[("count", &missing.to_string())]))); + parts.push(format!("({})", crate::i18n::tn(missing as u64, "{n} linked file not found", "{n} linked files not found"))); } if !fonts.is_empty() { parts.push(format!("({})", crate::i18n::fmt(tl!("fonts not copied: {fonts}"), &[("fonts", fonts)]))); diff --git a/crates/ui-egui/src/dialogs/tests_package.rs b/crates/ui-egui/src/dialogs/tests_package.rs index cd34213af..816c6641d 100644 --- a/crates/ui-egui/src/dialogs/tests_package.rs +++ b/crates/ui-egui/src/dialogs/tests_package.rs @@ -167,7 +167,7 @@ fn edits_after_ok_do_not_reach_the_package() { gate.open(); background::wait_all(&mut app); assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("package")); - assert!(app.ui.status.starts_with("Packaged 3 file(s) in "), "{}", app.ui.status); + assert!(app.ui.status.starts_with("Packaged 3 files in "), "{}", app.ui.status); let files = files.lock().unwrap(); let (_, bytes) = files.iter().find(|(p, _)| p.ends_with("card.vectorcraft")).unwrap(); assert_eq!(vectorcraft_format::load(bytes).unwrap().node_count(), nodes, "the document as it was at OK"); @@ -213,7 +213,7 @@ fn a_dirty_documents_save_is_written_before_its_package() { release.join().unwrap(); assert_eq!(written(&files), [doc]); assert!(app.background.jobs.is_empty() && !app.session.active().unwrap().is_dirty()); - assert!(app.ui.status.starts_with("Packaged 3 file(s) in "), "the package's status came last: {}", app.ui.status); + assert!(app.ui.status.starts_with("Packaged 3 files in "), "the package's status came last: {}", app.ui.status); } #[test] @@ -254,7 +254,7 @@ fn the_web_downloads_a_zip() { assert!(!frame(&mut app).contains("Location:"), "no folder to choose"); confirm(&mut app).unwrap(); assert!(app.ui.dialog.is_none()); - assert!(app.ui.status.contains("Packaged") && app.ui.status.contains("file(s)"), "{}", app.ui.status); + assert!(app.ui.status.contains("Packaged") && app.ui.status.contains("files"), "{}", app.ui.status); let got = got.borrow(); assert_eq!(got.len(), 1); assert_eq!(got[0].0, "card Folder.zip"); @@ -285,6 +285,71 @@ fn fonts_left_out_of_the_package_are_named() { assert!(app.ui.status.contains(gone), "{}", app.ui.status); } +/// The note on linked files left out of a package uses the form the language's plural rule picks +/// for the count. +#[test] +fn the_missing_links_note_uses_each_count_form() { + let note = |code: &str, missing: usize| { + crate::i18n::set_current(crate::i18n::Lang::from_code(code).unwrap()); + let note = package::package_status_note(missing, ""); + crate::i18n::set_current(crate::i18n::Lang::EN); + note + }; + assert_eq!(note("en", 1), "(1 linked file not found)"); + assert_eq!(note("en", 2), "(2 linked files not found)"); + assert_eq!(note("cs", 1), "(Nenalezen 1 propojený soubor)"); + assert_eq!(note("cs", 2), "(Nenalezeny 2 propojené soubory)"); + 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 or +/// parentheses keeps its whole name, next to one note or two. +#[test] +fn an_english_package_status_is_translated_after_a_language_switch() { + use crate::i18n::{Lang, message}; + let folders = [None, Some("/out/card Folder"), Some("/out/card (draft) v2")]; + let notes = ["", "(fonts not copied: Mono)", "(1 linked file not found) (fonts not copied: Mono)"]; + let mut statuses = vec![]; + for n in [1, 3] { + for folder in folders { + for note in notes { + statuses.push((package::package_status(n, folder, note), folder, note)); + } + } + } + let english: Vec<&str> = statuses.iter().map(|(s, ..)| s.as_str()).collect(); + assert!(english.contains(&"Packaged 1 file in /out/card Folder"), "{english:?}"); + assert!(english.contains(&"Packaged 1 file (fonts not copied: Mono) in /out/card (draft) v2"), "{english:?}"); + assert!(english.contains(&"Packaged 3 files"), "{english:?}"); + for lang in Lang::all().filter(|l| l.code() != "en") { + for (s, folder, note) in &statuses { + let shown = message(lang, s); + assert!(!shown.starts_with("Packaged"), "{}: {shown}", lang.code()); + // The folder stays whole in the translated line, and the note's font is still named. + assert!(folder.is_none_or(|f| shown.contains(f)), "{}: {shown}", lang.code()); + assert_eq!(note.contains("Mono"), shown.contains("Mono"), "{}: {shown}", lang.code()); + } + } + let de = Lang::from_code("de").unwrap(); + assert_eq!( + message(de, &package::package_status(1, Some("/out/card (draft)"), "(fonts not copied: Mono)")), + "1 Datei in /out/card (draft) verpackt (fonts not copied: Mono)" + ); + assert_eq!( + message(de, &package::package_status(3, Some("/out/card (draft)"), "(1 linked file not found) (fonts not copied: Mono)")), + "3 Dateien in /out/card (draft) verpackt (1 linked file not found) (fonts not copied: Mono)" + ); + 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 0becb7e96..83697b7f3 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -203,7 +203,7 @@ font-technology Variable Променливи Apply Paragraph Style Прилагане на стил на абзац Apply SVG Filter… Прилагане на SVG филтър… Apply to All Прилагане към всички - Apply to All ({count} more missing) Прилагане към всички (още {count} липсващи) +@plural Apply to All ({n} more missing)|Apply to All ({n} more missing) Прилагане към всички (още {n} липсващ)|Прилагане към всички (още {n} липсващи) Arc Дъга Arc Lower… Дъга надолу… Arc Tool Инструмент „Дъга“ @@ -2217,7 +2217,7 @@ font-technology Variable Променливи Package Пакет Package collects a saved document with its linked files and fonts. Пакетът събира запазен документ заедно с неговите свързани файлове и шрифтове. Package created Пакетът е създаден - Packaged {n} file(s) in {folder} {note} Пакетирани са {n} файла в {folder}{note} +@plural Packaged {n} file {note} in {folder}|Packaged {n} files {note} in {folder} Пакетиран е {n} файл в {folder} {note}|Пакетирани са {n} файла в {folder} {note} Packaged {n} file(s){note} Пакетирани са {n} файла{note} Package… Пакет… Page Страница @@ -2534,9 +2534,11 @@ font-technology Variable Променливи Stop Спиране Searching… Търсене… Not found Няма намерени резултати - {folders} folder(s) and {files} file(s) searched Претърсени са {folders} папки и {files} файла - {count} folder(s) skipped, such as other apps' and the system's folders and hidden ones Пропуснати са {count} папки (като системни папки, папки на други приложения и скрити папки) - {count} folder(s) or file(s) couldn't be read {count} папки или файла не можаха да бъдат прочетени + {folders} and {files} searched Претърсени са {folders} и {files} +@plural {n} folder|{n} folders {n} папка|{n} папки +@plural {n} file|{n} files {n} файл|{n} файла +@plural {n} folder skipped, such as another app's or the system's folder or a hidden one|{n} folders skipped, such as other apps' and the system's folders and hidden ones Пропусната е {n} папка (като системни папки, папки на други приложения и скрити папки)|Пропуснати са {n} папки (като системни папки, папки на други приложения и скрити папки) +@plural {n} folder or file couldn't be read|{n} folders or files couldn't be read {n} папка или файл не можа да бъде прочетен|{n} папки или файла не можаха да бъдат прочетени The search stopped at its limit: search a smaller folder. Търсенето спре поради достигане на лимита: потърсете в по-малка папка. Copy only fonts you own or are licensed to install. Копирайте само шрифтове, които притежавате или имате лиценз да инсталирате. VectorCraft copies the fonts to {folder}. VectorCraft копира шрифтовете в {folder}. @@ -3604,13 +3606,13 @@ font-technology Variable Променливи Zoom Tool Инструмент „Мащаб“ Zoom with Mouse Wheel Мащабиране с колелцето на мишката Scrub Numeric Fields by Dragging Плъзгане на числени полета чрез влачене - {count} file(s) in {folder} {note}. Show the package folder? {count} файл(а) в {folder}{note}. Показване на папката на пакета? - {count} linked file(s) not found {count} свързан(и) файл(а) не са намерени +@plural {n} file in {folder} {note}. Show the package folder?|{n} files in {folder} {note}. Show the package folder? {n} файл в {folder} {note}. Показване на папката на пакета?|{n} файла в {folder} {note}. Показване на папката на пакета? +@plural {n} linked file not found|{n} linked files not found {n} свързан файл не е намерен|{n} свързани файла не са намерени fonts not copied: {fonts} некопирани шрифтове: {fonts} - {count} linked image(s) changed since this document was saved. Show the new versions? {count} свързано(и) изображение(я) е/са променено(и) след запазването на този документ. Показване на новите версии? - {count} linked image(s) changed on disk. Show the new versions? {count} свързано(и) изображение(я) е/са променено(и) на диска. Показване на новите версии? - {count} more rules: Export writes every one. {count} още правила: Експортът записва всяко едно. - {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} ненаименувани обекта са пропуснати: наименувайте ги или включете „Ненаименувани обекти“ в менюто на панела. +@plural {n} linked image changed since this document was saved. Show the new version?|{n} linked images changed since this document was saved. Show the new versions? {n} свързано изображение е променено след запазването на този документ. Показване на новата версия?|{n} свързани изображения са променени след запазването на този документ. Показване на новите версии? +@plural {n} linked image changed on disk. Show the new version?|{n} linked images changed on disk. Show the new versions? {n} свързано изображение е променено на диска. Показване на новата версия?|{n} свързани изображения са променени на диска. Показване на новите версии? +@plural {n} more rule: Export writes it.|{n} more rules: Export writes every one. Още {n} правило: Експортът го записва.|Още {n} правила: Експортът записва всяко едно. +@plural {n} unnamed object left out: name it, or turn on Unnamed Objects in the panel menu.|{n} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {n} ненаименуван обект е пропуснат: наименувайте го или включете „Ненаименувани обекти“ в менюто на панела.|{n} ненаименувани обекта са пропуснати: наименувайте ги или включете „Ненаименувани обекти“ в менюто на панела. {effect} is already applied {effect} вече е приложен {format} Options Опции за {format} {kind} Gradient {kind} градиент @@ -3831,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 ({note}) in {folder}|Packaged {n} files ({note}) in {folder} Пакетиран е {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 Освобождаване на водачи: няма водачи @@ -5502,13 +5504,13 @@ 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} файла - Packaged {n} file(s) Пакетирани са {n} файла - Packaged {n} file(s) in {folder} Пакетирани са {n} файла в {folder} - Packaged {n} file(s) {note} Пакетирани са {n} файла {note} - {count} file(s) in {folder}. Show the package folder? {count} файл(а) в {folder}. Показване на папката на пакета? +@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} +@plural {n} file in {folder}. Show the package folder?|{n} files in {folder}. Show the package folder? {n} файл в {folder}. Показване на папката на пакета?|{n} файла в {folder}. Показване на папката на пакета? Clockwise По часовниковата стрелка Dither Дитъринг Fine noise hides banding in the gradient fill Фин шум скрива ивиците в градиентната запълка diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index b1635dc7b..3a737414d 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -662,7 +662,7 @@ 1 Layer 1 sloj Libraries Biblioteke Links Veze - {count} linked image(s) changed on disk. Show the new versions? Povezanih slika promijenjenih na disku: {count}. Prikazati nove verzije? +@plural {n} linked image changed on disk. Show the new version?|{n} linked images changed on disk. Show the new versions? Povezanih slika promijenjenih na disku: {n}. Prikazati nove verzije?|Povezanih slika promijenjenih na disku: {n}. Prikazati nove verzije?|Povezanih slika promijenjenih na disku: {n}. Prikazati nove verzije? Update Changed Links Ažuriraj promijenjene veze @msg Updated {_1} linked file(s) changed on disk Ažurirano povezanih datoteka promijenjenih na disku: {_1} Magic Wand Čarobni štapić @@ -1482,8 +1482,8 @@ lighting Light Svjetlo Select objects to see their CSS, or Generate CSS for the whole document. Odaberite objekte da vidite njihov CSS ili Generiši CSS za cijeli dokument. The selected objects have no CSS of their own (see below). Odabrani objekti nemaju vlastiti CSS (vidi ispod). Units: {units} Jedinice: {units} - {count} more rules: Export writes every one. Još pravila: {count}; izvoz zapisuje svako. - {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. Izostavljeno neimenovanih objekata: {count}; imenujte ih ili uključite Neimenovani objekti u meniju panela. +@plural {n} more rule: Export writes it.|{n} more rules: Export writes every one. Još pravila: {n}; izvoz zapisuje svako.|Još pravila: {n}; izvoz zapisuje svako.|Još pravila: {n}; izvoz zapisuje svako. +@plural {n} unnamed object left out: name it, or turn on Unnamed Objects in the panel menu.|{n} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. Izostavljeno neimenovanih objekata: {n}; imenujte ih ili uključite Neimenovani objekti u meniju panela.|Izostavljeno neimenovanih objekata: {n}; imenujte ih ili uključite Neimenovani objekti u meniju panela.|Izostavljeno neimenovanih objekata: {n}; imenujte ih ili uključite Neimenovani objekti u meniju panela. All Categories Sve kategorije Repeat Objects Ponovi objekte Save… Sačuvaj… @@ -2376,19 +2376,21 @@ lighting Light Svjetlo Stop Zaustavi Searching… Pretraga… Not found Nije pronađeno - {folders} folder(s) and {files} file(s) searched Pretraženo foldera: {folders}, datoteka: {files} - {count} folder(s) skipped, such as other apps' and the system's folders and hidden ones Preskočeno foldera: {count}, npr. foldera drugih aplikacija, sistemskih i skrivenih - {count} folder(s) or file(s) couldn't be read Foldera ili datoteka koje nije moguće pročitati: {count} + {folders} and {files} searched Pretraženo: {folders}, {files} +@plural {n} folder|{n} folders {n} folder|{n} foldera|{n} foldera +@plural {n} file|{n} files {n} datoteka|{n} datoteke|{n} datoteka +@plural {n} folder skipped, such as another app's or the system's folder or a hidden one|{n} folders skipped, such as other apps' and the system's folders and hidden ones Preskočeno foldera: {n}, npr. foldera drugih aplikacija, sistemskih i skrivenih|Preskočeno foldera: {n}, npr. foldera drugih aplikacija, sistemskih i skrivenih|Preskočeno foldera: {n}, npr. foldera drugih aplikacija, sistemskih i skrivenih +@plural {n} folder or file couldn't be read|{n} folders or files couldn't be read Foldera ili datoteka koje nije moguće pročitati: {n}|Foldera ili datoteka koje nije moguće pročitati: {n}|Foldera ili datoteka koje nije moguće pročitati: {n} The search stopped at its limit: search a smaller folder. Pretraga se zaustavila na svom ograničenju: pretražite manji folder. Copy only fonts you own or are licensed to install. Kopirajte samo fontove koje posjedujete ili imate licencu za instaliranje. VectorCraft copies the fonts to {folder}. VectorCraft kopira fontove u {folder}. - Apply to All ({count} more missing) Primijeni na sve (još {count} nedostaje) +@plural Apply to All ({n} more missing)|Apply to All ({n} more missing) Primijeni na sve (još {n} nedostaje)|Primijeni na sve (još {n} nedostaju)|Primijeni na sve (još {n} nedostaje) Ignore Zanemari Linked File Not Found Povezana datoteka nije pronađena Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Zamijenite je drugom datotekom ili je zanemarite: njene slike i dalje prikazuju pregled sačuvan s dokumentom. Replace… Zamijeni… Update Modified Links Ažuriraj izmijenjene veze - {count} linked image(s) changed since this document was saved. Show the new versions? Povezanih slika promijenjenih otkako je dokument sačuvan: {count}. Prikazati nove verzije? +@plural {n} linked image changed since this document was saved. Show the new version?|{n} linked images changed since this document was saved. Show the new versions? Povezanih slika promijenjenih otkako je dokument sačuvan: {n}. Prikazati nove verzije?|Povezanih slika promijenjenih otkako je dokument sačuvan: {n}. Prikazati nove verzije?|Povezanih slika promijenjenih otkako je dokument sačuvan: {n}. Prikazati nove verzije? Dialog Dijalog Convert Process to Global Pretvori procesne u globalne Create From: Kreiraj iz: @@ -2427,12 +2429,12 @@ lighting Light Svjetlo Fonts whose licence doesn't allow embedding are not copied. Fontovi čija licenca ne dozvoljava ugrađivanje ne kopiraju se. Package collects a saved document with its linked files and fonts. Paket prikuplja sačuvani dokument s njegovim povezanim datotekama i fontovima. Package created Paket je kreiran - Packaged {n} file(s) in {folder} {note} Upakovano datoteka u {folder}: {n} {note} +@plural Packaged {n} file {note} in {folder}|Packaged {n} files {note} in {folder} Upakovano datoteka u {folder}: {n} {note}|Upakovano datoteka u {folder}: {n} {note}|Upakovano datoteka u {folder}: {n} {note} Packaged {n} file(s){note} Upakovano datoteka: {n}{note} Relink Linked Files to the Document Ponovo poveži povezane datoteke s dokumentom Save the document first? Prvo sačuvati dokument? - {count} file(s) in {folder} {note}. Show the package folder? Datoteka u {folder}: {count} {note}. Prikazati folder paketa? - {count} linked file(s) not found Povezanih datoteka nije pronađeno: {count} +@plural {n} file in {folder} {note}. Show the package folder?|{n} files in {folder} {note}. Show the package folder? Datoteka u {folder}: {n} {note}. Prikazati folder paketa?|Datoteka u {folder}: {n} {note}. Prikazati folder paketa?|Datoteka u {folder}: {n} {note}. Prikazati folder paketa? +@plural {n} linked file not found|{n} linked files not found Povezanih datoteka nije pronađeno: {n}|Povezanih datoteka nije pronađeno: {n}|Povezanih datoteka nije pronađeno: {n} fonts not copied: {fonts} fontovi nisu kopirani: {fonts} Built-in presets are read-only: New… starts an editable copy. Ugrađeni predlošci su samo za čitanje: Novo… započinje kopiju koja se može uređivati. 1 {unit} on the artboard stands for {real} {unit} in the scene. 1 {unit} na radnoj ploči predstavlja {real} {unit} u sceni. @@ -4197,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 ({note}) in {folder}|Packaged {n} files ({note}) in {folder} 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 @@ -5522,13 +5524,13 @@ 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} - Packaged {n} file(s) Upakovano datoteka: {n} - Packaged {n} file(s) in {folder} Upakovano datoteka u {folder}: {n} - Packaged {n} file(s) {note} Upakovano datoteka: {n} {note} - {count} file(s) in {folder}. Show the package folder? Datoteka u {folder}: {count}. Prikazati folder paketa? +@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} +@plural {n} file in {folder}. Show the package folder?|{n} files in {folder}. Show the package folder? Datoteka u {folder}: {n}. Prikazati folder paketa?|Datoteka u {folder}: {n}. Prikazati folder paketa?|Datoteka u {folder}: {n}. Prikazati folder paketa? Clockwise U smjeru kazaljke na satu Dither Dithering Fine noise hides banding in the gradient fill Fini šum skriva trake u gradijentnoj ispuni diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index b875d1067..c04ca4698 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -320,7 +320,7 @@ Apply Paragraph Style Aplica l’estil de paràgraf Apply SVG Filter… Aplica el filtre SVG… Apply to All Aplica-ho a tot - Apply to All ({count} more missing) Aplica-ho a tot ({count} més que falten) +@plural Apply to All ({n} more missing)|Apply to All ({n} more missing) Aplica-ho a tot ({n} més que falta)|Aplica-ho a tot ({n} més que falten) Apply {name} Aplica {name} Approximate path Aproxima el traçat Arabic Àrab @@ -2812,10 +2812,10 @@ Package Empaqueta Package collects a saved document with its linked files and fonts. L’empaquetatge recull un document desat amb els fitxers enllaçats i els tipus de lletra. Package created Empaquetatge creat - Packaged {n} file(s) S’han empaquetat {n} fitxers - Packaged {n} file(s) in {folder} S’han empaquetat {n} fitxers a {folder} - Packaged {n} file(s) in {folder} {note} S’han empaquetat {n} fitxers a {folder} {note} - Packaged {n} file(s) {note} S’han empaquetat {n} fitxers {note} +@plural Packaged {n} file|Packaged {n} files S’ha empaquetat {n} fitxer|S’han empaquetat {n} fitxers +@plural Packaged {n} file in {folder}|Packaged {n} files in {folder} S’ha empaquetat {n} fitxer a {folder}|S’han empaquetat {n} fitxers a {folder} +@plural Packaged {n} file {note} in {folder}|Packaged {n} files {note} in {folder} S’ha empaquetat {n} fitxer a {folder} {note}|S’han empaquetat {n} fitxers a {folder} {note} +@plural Packaged {n} file {note}|Packaged {n} files {note} S’ha empaquetat {n} fitxer {note}|S’han empaquetat {n} fitxers {note} Packaged {n} file(s){note} S’han empaquetat {n} fitxers{note} Package… Empaqueta… Page Pàgina @@ -4653,19 +4653,21 @@ Zoom to Selection Amplia a la selecció Zoom Tool Eina de zoom Zoom with Mouse Wheel Zoom amb la roda del ratolí - {count} file(s) in {folder} {note}. Show the package folder? {count} fitxer(s) a {folder} {note}. Voleu mostrar la carpeta del paquet? - {count} file(s) in {folder}. Show the package folder? {count} fitxer(s) a {folder}. Voleu mostrar la carpeta del paquet? - {count} folder(s) or file(s) couldn't be read No s’han pogut llegir {count} carpeta(es) o fitxer(s) - {count} folder(s) skipped, such as other apps' and the system's folders and hidden ones S’han omès {count} carpeta(es), com ara les d’altres aplicacions, les del sistema i les ocultes - {count} linked file(s) not found No s’han trobat {count} fitxer(s) enllaçat(s) - {count} linked image(s) changed on disk. Show the new versions? {count} imatge(s) enllaçada(es) han canviat al disc. Voleu mostrar les versions noves? - {count} linked image(s) changed since this document was saved. Show the new versions? {count} imatge(s) enllaçada(es) han canviat des que es va desar aquest document. Voleu mostrar les versions noves? - {count} more rules: Export writes every one. {count} regles més: l’exportació les escriu totes. +@plural {n} file in {folder} {note}. Show the package folder?|{n} files in {folder} {note}. Show the package folder? {n} fitxer a {folder} {note}. Voleu mostrar la carpeta del paquet?|{n} fitxers a {folder} {note}. Voleu mostrar la carpeta del paquet? +@plural {n} file in {folder}. Show the package folder?|{n} files in {folder}. Show the package folder? {n} fitxer a {folder}. Voleu mostrar la carpeta del paquet?|{n} fitxers a {folder}. Voleu mostrar la carpeta del paquet? +@plural {n} folder or file couldn't be read|{n} folders or files couldn't be read No s’ha pogut llegir {n} carpeta o fitxer|No s’han pogut llegir {n} carpetes o fitxers +@plural {n} folder skipped, such as another app's or the system's folder or a hidden one|{n} folders skipped, such as other apps' and the system's folders and hidden ones S’ha omès {n} carpeta, com ara les d’altres aplicacions, les del sistema i les ocultes|S’han omès {n} carpetes, com ara les d’altres aplicacions, les del sistema i les ocultes +@plural {n} linked file not found|{n} linked files not found No s’ha trobat {n} fitxer enllaçat|No s’han trobat {n} fitxers enllaçats +@plural {n} linked image changed on disk. Show the new version?|{n} linked images changed on disk. Show the new versions? {n} imatge enllaçada ha canviat al disc. Voleu mostrar la versió nova?|{n} imatges enllaçades han canviat al disc. Voleu mostrar les versions noves? +@plural {n} linked image changed since this document was saved. Show the new version?|{n} linked images changed since this document was saved. Show the new versions? {n} imatge enllaçada ha canviat des que es va desar aquest document. Voleu mostrar la versió nova?|{n} imatges enllaçades han canviat des que es va desar aquest document. Voleu mostrar les versions noves? +@plural {n} more rule: Export writes it.|{n} more rules: Export writes every one. {n} regla més: l’exportació l’escriu.|{n} regles més: l’exportació les escriu totes. {count} of {total} artboards {count} de {total} taulers - {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. S’han omès {count} objectes sense nom: poseu-los un nom o activeu Objectes sense nom al menú del panell. +@plural {n} unnamed object left out: name it, or turn on Unnamed Objects in the panel menu.|{n} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. S’ha omès {n} objecte sense nom: poseu-li un nom o activeu Objectes sense nom al menú del panell.|S’han omès {n} objectes sense nom: poseu-los un nom o activeu Objectes sense nom al menú del panell. {effect} is already applied {effect} ja està aplicat {file}, line {line} {file}, línia {line} - {folders} folder(s) and {files} file(s) searched S’han cercat {folders} carpeta(es) i {files} fitxer(s) + {folders} and {files} searched S’han cercat {folders} i {files} +@plural {n} folder|{n} folders {n} carpeta|{n} carpetes +@plural {n} file|{n} files {n} fitxer|{n} fitxers {format} Options Opcions de {format} {kind} Gradient Degradat {kind} {kind} Style Options… Opcions d’estil de {kind}… @@ -5256,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 ({note}) in {folder}|Packaged {n} files ({note}) in {folder} 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