M14.i18n: translate Czech labels at UI render sites

Localize native/web file filters, diagnostic labels, numbered presets
and contributor tooltips with English fallback and Czech plural forms.
Fix gradient adjective agreement and disambiguate Japanese printer marks
in PDF/Print choices and summaries. Keep file extensions and names intact.

Add Czech catalog rows and regression coverage; update localization evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Losminek
2026-10-12 08:18:13 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 25c60fd0f5
commit e3dec2e87b
15 changed files with 155 additions and 32 deletions
+6 -2
View File
@@ -562,8 +562,12 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(
egui::RichText::new(format!(
"render {:.1} ms · ui {:.1} ms · {:.0} fps",
app.perf.render_ms, app.perf.frame_ms, app.perf.fps
"{} {:.1} ms · {} {:.1} ms · {:.0} fps",
crate::i18n::tr_ctx(crate::i18n::current(), "diagnostics", "render"),
app.perf.render_ms,
crate::i18n::tr_ctx(crate::i18n::current(), "diagnostics", "ui"),
app.perf.frame_ms,
app.perf.fps
))
.size(10.5)
.color(t.text_disabled),
+23 -14
View File
@@ -87,11 +87,14 @@ impl SortKey {
SortKey::LastCommit,
];
/// Menu label and table header, in the UI language (the short count headers stay as they are).
/// Menu label and table header, in the UI language (the symbolic count headers stay as they are).
pub fn label(self) -> (&'static str, &'static str) {
match self {
SortKey::Name => (tl!("Name (A–Z)"), tl!("Name")),
SortKey::Prs => (tl!("Merged PRs"), "PRs"),
SortKey::Prs => {
let header = "PRs";
(tl!("Merged PRs"), tl!(header))
}
SortKey::Commits => (tl!("Commits"), tl!("Commits")),
SortKey::LinesAdded => (tl!("Lines added"), "+LOC"),
SortKey::LinesDeleted => (tl!("Lines deleted"), "−LOC"),
@@ -125,18 +128,24 @@ impl Contributor {
/// One line with everything we know, for tooltips.
pub fn summary(&self) -> String {
format!(
"@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {}",
self.login,
self.prs,
self.commits,
group(self.lines_added),
group(self.lines_deleted),
signed(self.lines_delta()),
self.binary_added,
self.binary_deleted,
day(self.first_commit),
day(self.last_commit),
let lang = crate::i18n::current();
let template = "@{login}: {prs}, {commits}, +{added} / −{deleted} lines (Δ {delta}), +{binary_added} / −{binary_deleted} binary assets, {first} – {last}";
let prs = crate::i18n::trn(lang, self.prs, "{n} PR", "{n} PRs");
let commits = crate::i18n::trn(lang, self.commits, "{n} commit", "{n} commits");
crate::i18n::fmt(
tl!(template),
&[
("login", self.login),
("prs", &prs),
("commits", &commits),
("added", &group(self.lines_added)),
("deleted", &group(self.lines_deleted)),
("delta", &signed(self.lines_delta())),
("binary_added", &self.binary_added.to_string()),
("binary_deleted", &self.binary_deleted.to_string()),
("first", day(self.first_commit)),
("last", day(self.last_commit)),
],
)
}
}
+3 -1
View File
@@ -304,7 +304,9 @@ fn details(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, b: &mut
let saving = !d.bool(SAVING);
d.fields.insert(SAVING.into(), json!(saving));
if saving && d.str("presetName").is_empty() {
d.fields.insert("presetName".into(), json!(format!("Preset {}", app.session.prefs.new_doc_presets.len() + 1)));
let template = "Preset {number}";
let name = crate::i18n::fmt(tl!(template), &[("number", &(app.session.prefs.new_doc_presets.len() + 1).to_string())]);
d.fields.insert("presetName".into(), json!(name));
}
}
});
+9 -2
View File
@@ -272,13 +272,19 @@ pub(super) fn choice<T: Choice>(d: &Dialog, path: &str) -> Option<T> {
/// A dropdown of the choices `T` (options for which `enabled` is false are greyed).
pub(super) fn pick<T: Choice>(ui: &mut egui::Ui, d: &mut Dialog, path: &str, width: f32, enabled: impl Fn(T) -> bool) -> bool {
let current = choice_index::<T>(d, path);
let label = current.and_then(|i| T::LABELS.get(i)).copied().unwrap_or_default();
let chosen = widgets::dropdown_with(ui, path, label, T::LABELS, width, |i| T::ALL.get(i).is_some_and(|c| enabled(*c)));
let labels: Vec<_> = T::LABELS.iter().map(|label| choice_label(crate::i18n::current(), path, label)).collect();
let label = current.and_then(|i| labels.get(i)).copied().unwrap_or_default();
let chosen = widgets::dropdown_with(ui, path, label, &labels, width, |i| T::ALL.get(i).is_some_and(|c| enabled(*c)));
let Some(id) = chosen.and_then(|i| T::IDS.get(i)) else { return false };
set(d, path, json!(id));
true
}
/// Printer mark styles use adjectives, separately from interface language names.
pub(super) fn choice_label<'a>(lang: crate::i18n::Lang, path: &str, label: &'a str) -> &'a str {
if path == "marks.kind" { crate::i18n::tr_ctx(lang, "mark-style", label) } else { crate::i18n::tr(lang, label) }
}
pub(super) fn number(ui: &mut egui::Ui, d: &mut Dialog, path: &str, suffix: &str, enabled: bool) {
let v = get(d, path).as_f64().unwrap_or(0.0);
ui.add_enabled_ui(enabled, |ui| {
@@ -785,6 +791,7 @@ pub(super) fn option_rows<'a>(ui: &mut egui::Ui, changed: impl IntoIterator<Item
for c in changed {
let value = match &c["value"] {
Value::Bool(b) => if *b { tl!("On") } else { tl!("Off") }.to_string(),
Value::String(s) if c["option"] == "marks.kind" => choice_label(crate::i18n::current(), "marks.kind", s).to_string(),
Value::String(s) => s.clone(),
other => other.to_string(),
};
@@ -296,3 +296,11 @@ fn simplify_dialog_ok_keeps_exactly_one_undo_with_or_without_preview() {
assert!(app.session.doc().unwrap().selection.anchors.is_empty());
}
}
#[test]
fn printer_mark_labels_are_distinct_from_language_names() {
let cs = crate::i18n::Lang::from_code("cs").unwrap();
assert_eq!(save_pdf::choice_label(cs, "marks.kind", "Japanese"), "Japonské");
assert_eq!(save_pdf::choice_label(cs, "language", "Japanese"), "Japonština");
assert_eq!(save_pdf::choice_label(crate::i18n::Lang::EN, "marks.kind", "Japanese"), "Japanese");
}
+22 -1
View File
@@ -1651,7 +1651,7 @@ lighting Light Světlo
Small Thumbnail View Malé miniatury
Swatch Libraries Menu Nabídka knihoven vzorníku
User Library Uživatelská knihovna
{kind} Gradient Přechod: {kind}
{kind} Gradient {kind} přechod
{kind}, {model} {kind}, {model}
No symbols in this document V tomto dokumentu nejsou žádné symboly
Select art and click New Symbol to make one. Vyberte kresbu a symbol vytvořte tlačítkem Nový symbol.
@@ -4472,3 +4472,24 @@ effect Regular Pravidelný
Artistic Umělecké
Film Grain… Filmové zrno…
Highlight Area Oblast světel
# Czech render-site follow-up (AI drafts; native review requested).
file-filter All readable files Všechny čitelné soubory
file-filter All placeable files Všechny soubory pro umístění
file-filter Swatch libraries Knihovny vzorníků
file-filter Flattener presets Přednastavení sloučení průhlednosti
file-filter PDF presets Přednastavení PDF
file-filter Print presets Přednastavení tisku
file-filter SVG Compressed Komprimované SVG
file-filter PDF-compatible .ai .ai kompatibilní s PDF
file-filter VectorCraft Template Šablona VectorCraft
file-filter Templates Šablony
diagnostics render vykreslení
diagnostics ui rozhraní
Preset {number} Přednastavení {number}
PRs PR
@{login}: {prs}, {commits}, +{added} / −{deleted} lines (Δ {delta}), +{binary_added} / −{binary_deleted} binary assets, {first} – {last} @{login}: {prs}, {commits}, řádky +{added} / −{deleted} (Δ {delta}), binární soubory +{binary_added} / −{binary_deleted}, {first} – {last}
@plural {n} PR|{n} PRs {n} PR|{n} PR|{n} PR
@plural {n} commit|{n} commits {n} commit|{n} commity|{n} commitů
gradient-kind Radial Kruhový
mark-style Japanese Japonské
Can't render this file because it contains an unexpected character in line 2 and column 73.
+35
View File
@@ -1120,3 +1120,38 @@ fn as_template(s: &str) -> String {
out.push_str(rest);
out
}
/// Render-site labels keep English fallback, placeholders and Czech grammar independent of
/// the plain language-name key. No global language changes: other UI tests may run alongside.
#[test]
fn czech_render_site_follow_up() {
for (source, expected) in [
("All readable files", "Všechny čitelné soubory"),
("All placeable files", "Všechny soubory pro umístění"),
("SVG Compressed", "Komprimované SVG"),
("PDF-compatible .ai", ".ai kompatibilní s PDF"),
("VectorCraft Template", "Šablona VectorCraft"),
] {
assert_eq!(tr_ctx(cs(), "file-filter", source), expected);
assert_eq!(tr_ctx(Lang::EN, "file-filter", source), source);
}
for source in ["SVG", "PNG", "VectorCraft", "unregistered filter"] {
assert_eq!(tr_ctx(cs(), "file-filter", source), source);
}
assert_eq!(tr_ctx(cs(), "diagnostics", "render"), "vykreslení");
assert_eq!(tr_ctx(cs(), "diagnostics", "ui"), "rozhraní");
assert_eq!(fmt(tr(cs(), "Preset {number}"), &[("number", "12")]), "Přednastavení 12");
assert_eq!(fmt(tr(Lang::EN, "Preset {number}"), &[("number", "12")]), "Preset 12");
for (kind, expected) in [("Linear", "Lineární přechod"), ("Radial", "Kruhový přechod"), ("Freeform", "Volný přechod")] {
assert_eq!(fmt(tr(cs(), "{kind} Gradient"), &[("kind", tr_ctx(cs(), "gradient-kind", kind))]), expected);
assert_eq!(fmt(tr(Lang::EN, "{kind} Gradient"), &[("kind", tr_ctx(Lang::EN, "gradient-kind", kind))]), format!("{kind} Gradient"));
}
assert_eq!(tr(cs(), "Japanese"), "Japonština");
assert_eq!(tr_ctx(cs(), "mark-style", "Japanese"), "Japonské");
assert_eq!(tr_ctx(Lang::EN, "mark-style", "Japanese"), "Japanese");
assert_eq!(tr(cs(), "PRs"), "PR");
for (n, expected) in [(0, "0 commitů"), (1, "1 commit"), (2, "2 commity"), (4, "4 commity"), (5, "5 commitů")] {
assert_eq!(trn(cs(), n, "{n} commit", "{n} commits"), expected);
assert_eq!(trn(cs(), n, "{n} PR", "{n} PRs"), format!("{n} PR"));
}
}
+4 -1
View File
@@ -241,7 +241,10 @@ pub(crate) fn describe(paint: &Paint, global: bool, spot: bool) -> String {
};
crate::i18n::fmt(tl!("{kind}, {model}"), &[("kind", kind), ("model", color.model_name())])
}
Paint::Gradient(g) => crate::i18n::fmt(tl!("{kind} Gradient"), &[("kind", tl!(g.gradient.kind.label()))]),
Paint::Gradient(g) => crate::i18n::fmt(
tl!("{kind} Gradient"),
&[("kind", crate::i18n::tr_ctx(crate::i18n::current(), "gradient-kind", g.gradient.kind.label()))],
),
Paint::Pattern { .. } => tl!("Pattern").into(),
Paint::None => tl!("None").into(),
}