Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Weights, evidence and the missing items per area: [target-app-parity.md](docs/ta
| Korean | `ko` | none | 0% |
| Vietnamese | `vi` | none | 0% |

Also shipped: Traditional Chinese (partial, 80%), Italian, Russian and Ukrainian (full), Czech (menus only).
Also shipped: Traditional Chinese (partial, 80%), Italian, Russian and Ukrainian (full), Czech (catalog complete; render-site gaps reduced; native review pending).
No catalog has had a native speaker's review. Details: [localization-parity.md](docs/localization-parity.md).

## Upcoming
Expand Down
9 changes: 7 additions & 2 deletions apps/vectorcraft-web/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,10 @@ fn services(inbox: Inbox, place_inbox: PlaceInbox, ctx: egui::Context, locks: Op
let inbox = picked.clone();
let ctx = place_ctx.clone();
wasm_bindgen_futures::spawn_local(async move {
let dialog = fileio::place_filters().fold(rfd::AsyncFileDialog::new().set_title("Place"), |d, (name, exts)| d.add_filter(name, exts));
let dialog =
fileio::place_filters().fold(rfd::AsyncFileDialog::new().set_title(vectorcraft_ui_egui::i18n::t("Place")), |d, (name, exts)| {
d.add_filter(vectorcraft_ui_egui::i18n::tr_ctx(vectorcraft_ui_egui::i18n::current(), "file-filter", name), exts)
});
let Some(files) = dialog.pick_files().await else {
return;
};
Expand All @@ -419,7 +422,9 @@ fn services(inbox: Inbox, place_inbox: PlaceInbox, ctx: egui::Context, locks: Op
let inbox = open_inbox.clone();
let ctx = ctx.clone();
wasm_bindgen_futures::spawn_local(async move {
let dialog = fileio::open_filters().fold(rfd::AsyncFileDialog::new(), |d, (name, exts)| d.add_filter(name, exts));
let dialog = fileio::open_filters().fold(rfd::AsyncFileDialog::new(), |d, (name, exts)| {
d.add_filter(vectorcraft_ui_egui::i18n::tr_ctx(vectorcraft_ui_egui::i18n::current(), "file-filter", name), exts)
});
let Some(file) = dialog.pick_file().await else {
return;
};
Expand Down
11 changes: 9 additions & 2 deletions apps/vectorcraft/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,17 @@ dialog_settings!(rfd::FileDialog, rfd::AsyncFileDialog);
fn dialog_for<D: DialogSettings>(d: D, request: &PickRequest) -> D {
let pick = match request {
PickRequest::Open(pick) | PickRequest::Save(pick) => pick,
PickRequest::OpenMany => return fileio::place_filters().fold(d.title("Place"), |d, (name, exts)| d.filter(name, exts)),
PickRequest::OpenMany => {
return fileio::place_filters().fold(d.title(vectorcraft_ui_egui::i18n::t("Place")), |d, (name, exts)| {
d.filter(vectorcraft_ui_egui::i18n::tr_ctx(vectorcraft_ui_egui::i18n::current(), "file-filter", name), exts)
});
}
PickRequest::Folder => return d,
};
let d = pick.filters.iter().fold(d, |d, (name, exts)| d.filter(name, exts));
let d = pick
.filters
.iter()
.fold(d, |d, (name, exts)| d.filter(vectorcraft_ui_egui::i18n::tr_ctx(vectorcraft_ui_egui::i18n::current(), "file-filter", name), exts));
let d = match &pick.folder {
Some(folder) => d.folder(folder),
None => d,
Expand Down
8 changes: 6 additions & 2 deletions crates/ui-egui/src/chrome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
37 changes: 23 additions & 14 deletions crates/ui-egui/src/credits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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)),
],
)
}
}
Expand Down
4 changes: 3 additions & 1 deletion crates/ui-egui/src/dialogs/new_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
});
Expand Down
11 changes: 9 additions & 2 deletions crates/ui-egui/src/dialogs/save_pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down Expand Up @@ -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(),
};
Expand Down
8 changes: 8 additions & 0 deletions crates/ui-egui/src/dialogs/tests_modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
23 changes: 22 additions & 1 deletion crates/ui-egui/src/i18n/cs.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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é
35 changes: 35 additions & 0 deletions crates/ui-egui/src/i18n/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
}
5 changes: 4 additions & 1 deletion crates/ui-egui/src/panels/swatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down
5 changes: 4 additions & 1 deletion docs/gaps.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Gaps

> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)

Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from
switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make
Expand Down Expand Up @@ -194,6 +194,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall

## G16 Localization: the five missing key languages

- **Evidence:** Czech render-site bypasses reduced in file dialogs, diagnostics, preset names and contributor tooltips; gradient grammar and printer-mark context corrected (see [localization-parity.md](localization-parity.md)). Native review and the missing-language work remain; estimate unchanged.

- **Missing:** Hindi, Arabic, Indonesian, Korean, Vietnamese catalogs; a right-to-left interface and interface
shaping for Arabic and Devanagari; messages for Simplified Chinese and Brazilian Portuguese; native review of
every catalog. Measured in [localization-parity.md](localization-parity.md).
Expand Down Expand Up @@ -224,6 +226,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall

| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged |
| 2026-10-12 | minor | Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged |
| 2026-10-12 | minor | Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged |
| 2026-10-12 | minor | G11: clear stale component refs after Simplify/Add Anchor Points; preview and point counts give visible feedback (#1272); estimates unchanged |
Expand Down
Loading
Loading