diff --git a/ROADMAP.md b/ROADMAP.md index bd5781965..a6117ec96 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -95,6 +95,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - **UI:** Illustrator 2026 layout restyled to measured values: - Medium Dark theme, categorized and Advanced toolbars, 35 pt document tabs, 33 pt panel tabs. - Hint bar, contextual task bar, 19 dock panels with ≡ menus. + - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan) ships complete, Czech covers every menu label and Japanese the main menus, with system-locale detection on macOS, Windows and Linux, a VectorCraft ▸ Language menu and a Language preference. - Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. - A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas. - Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close). @@ -114,7 +115,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. - Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). - **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity), Character and Paragraph Styles (override-preserving redefine), Area Type Options (rows/columns/inset/first baseline), threaded text across any closed shapes, Fit Headline. -- **Japanese and Czech interface, vertical type:** VectorCraft ▸ Language switches the menus between English, Japanese and Czech (untranslated labels stay English; Czech covers every menu label, tested) and persists; Japanese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech ones from the bundled UI fonts. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko, upright glyphs centred on the column, point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. +- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional Chinese covers the whole interface, Czech every menu label (tested) and Japanese the main menus; untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech ones from the bundled UI fonts. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko, upright glyphs centred on the column, point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. - **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `` and PDF soft masks. - **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type, Gradient Mesh, Shape Builder, Live Paint (every path an edge, open lines included), Image Trace (12 presets), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror). - **Paint and appearance (M3):** diff --git a/apps/vectorcraft/src/native_menu.rs b/apps/vectorcraft/src/native_menu.rs index d85d5f0d6..a00c56070 100644 --- a/apps/vectorcraft/src/native_menu.rs +++ b/apps/vectorcraft/src/native_menu.rs @@ -10,6 +10,7 @@ use muda::accelerator::Accelerator; use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}; use serde_json::Value; use vectorcraft_ui_egui::VectorcraftApp; +use vectorcraft_ui_egui::i18n::{self, Lang, t}; use vectorcraft_ui_egui::menus::{self, Item}; enum Handle { @@ -18,7 +19,6 @@ enum Handle { } pub struct NativeMenu { - language: vectorcraft_ui_egui::i18n::Language, _menu: Menu, items: HashMap, last_refresh: f64, @@ -28,6 +28,8 @@ pub struct NativeMenu { slots: usize, /// Plug-in registry revision the Plug-ins submenus were built for. plugins: u64, + /// The language the labels were built in (a change rebuilds the menu). + language: Lang, } /// Accelerator for a shortcut like "Cmd+Shift+]" (modifier-less shortcuts stay in the app so they @@ -48,7 +50,7 @@ impl NativeMenu { let mut items = HashMap::new(); let mut counter = 0usize; for (title, entries) in menus::menu_tree() { - let sub = Submenu::new(app.ui.language.tr(title), true); + let sub = Submenu::new(t(title), true); build(app, &sub, &entries, &mut items, &mut counter); let _ = menu.append(&sub); } @@ -66,7 +68,7 @@ impl NativeMenu { generation, slots: menus::listed_slots(app), plugins: menus::plugin_revision(), - language: app.ui.language, + language: i18n::current(), } } @@ -83,12 +85,12 @@ impl NativeMenu { return; } self.last_refresh = now; - // Shortcuts edited, workspaces added, saved views, recent files or plug-ins changed: rebuild so - // accelerators and lists are current. + // Shortcuts edited, workspaces added, saved views, recent files, plug-ins or the UI language + // changed: rebuild so accelerators, lists and labels are current. if vectorcraft_ui_egui::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed) != self.generation || menus::listed_slots(app) != self.slots || menus::plugin_revision() != self.plugins - || app.ui.language != self.language + || i18n::current() != self.language { *self = NativeMenu::install(app); return; @@ -121,7 +123,7 @@ impl NativeMenu { "view.cornerWidget" | "view.textThreads" | "view.gradientAnnotator" | "effect.last" | "type.hiddenCharacters" ) { - i.set_text(app.ui.language.tr(&menus::dynamic_label(app, cmd, ""))); + i.set_text(menus::display_label(app, cmd, "")); } } Handle::Check(c) => { @@ -146,13 +148,13 @@ fn build( let _ = parent.append(&PredefinedMenuItem::separator()); } Item::Header(h) => { - let _ = parent.append(&MenuItem::new(app.ui.language.tr(h), false, None)); + let _ = parent.append(&MenuItem::new(t(h), false, None)); } Item::Todo(label, sc) => { - let _ = parent.append(&MenuItem::new(app.ui.language.tr(label), false, accel(sc))); + let _ = parent.append(&MenuItem::new(t(label), false, accel(sc))); } Item::Sub(label, children) => { - let sub = Submenu::new(app.ui.language.tr(label), true); + let sub = Submenu::new(t(label), true); build(app, &sub, children, items, counter); let _ = parent.append(&sub); } @@ -163,11 +165,11 @@ fn build( let id = format!("dc{counter}"); let sc = if params.is_null() { menus::shortcut_of(cmd).and_then(accel) } else { None }; let handle = if menus::checked(app, cmd, params).is_some() { - let c = CheckMenuItem::with_id(id.clone(), app.ui.language.tr(label), true, false, sc); + let c = CheckMenuItem::with_id(id.clone(), menus::display_label(app, cmd, label), true, false, sc); let _ = parent.append(&c); Handle::Check(c) } else { - let i = MenuItem::with_id(id.clone(), app.ui.language.tr(label), true, sc); + let i = MenuItem::with_id(id.clone(), menus::display_label(app, cmd, label), true, sc); let _ = parent.append(&i); Handle::Plain(i) }; diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 80cde8688..cba10e7d8 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -260,6 +260,8 @@ pub const PREF_SPECS: &[PrefSpec] = &[ p!("largeTabs", "User Interface", "", "Large Tabs", bool), p!("uiScaling", "User Interface", "UI Scaling", "Scale", num(0.75, 2.0, "×")), p!("scaleCursorWithUi", "User Interface", "UI Scaling", "Scale Cursor Proportional to UI", bool), + // `auto` or a language code the shell registers (`zh-hant`); the shell shows it as a dropdown. + p!("interfaceLanguage", "User Interface", "Language", "Language", text), // Performance p!("gpuPerformance", "Performance", "GPU Performance", "GPU Performance", bool), p!("animatedZoom", "Performance", "GPU Performance", "Animated Zoom", bool), @@ -390,6 +392,11 @@ pub fn validate(key: &str, v: &Value) -> std::result::Result { Err(format!("`{key}` must be a #rrggbb colour")) } } + PrefKind::Text if key == "interfaceLanguage" => { + let s = v.as_str().map(str::trim).unwrap_or(""); + let ok = !s.is_empty() && s.len() <= 16 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'); + if ok { Ok(json!(s.to_ascii_lowercase())) } else { Err(format!("`{key}` must be `auto` or a language code such as `en` or `zh-hant`")) } + } PrefKind::Text => match v { Value::String(s) => Ok(json!(s)), _ => Err(format!("`{key}` must be a string")), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 8095cbf23..61ffdb24f 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -391,6 +391,9 @@ pub struct Prefs { pub large_tabs: bool, pub ui_scaling: f64, pub scale_cursor_with_ui: bool, + /// UI language: `auto` (follow the system locale) or a language code such as `en`, `zh-hant`. + /// The list of languages belongs to the shell (`ui-egui` i18n); an unknown code reads as `auto`. + pub interface_language: String, // Performance pub gpu_performance: bool, pub animated_zoom: bool, @@ -592,6 +595,7 @@ impl Default for Prefs { large_tabs: false, ui_scaling: 1.0, scale_cursor_with_ui: false, + interface_language: s("auto"), gpu_performance: true, animated_zoom: true, history_states: 500, diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 24155934a..45574f525 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1236,7 +1236,7 @@ fn draw_overlays(p: &egui::Painter, xf: &Xf, overlays: &[Overlay], t: &Tokens) { } Overlay::Label { p: pt, text, color } => { let sp = xf.to_screen(*pt) + vec2(8.0, -14.0); - p.text(sp, egui::Align2::LEFT_TOP, text, egui::FontId::proportional(11.0), c32(*color)); + p.text(sp, egui::Align2::LEFT_TOP, tl!(text), egui::FontId::proportional(11.0), c32(*color)); } Overlay::Highlight { quad, color } => { let c = Color32::from_rgba_unmultiplied(color[0], color[1], color[2], color[3]); @@ -1275,21 +1275,21 @@ fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { let inner = rect.shrink2(vec2((rect.width() - 820.0).max(40.0) / 2.0, 60.0)); let mut child = ui.new_child(egui::UiBuilder::new().max_rect(inner).layout(egui::Layout::top_down(egui::Align::Min))); let ui = &mut child; - ui.label(egui::RichText::new("Welcome to VectorCraft").font(theme::semibold(26.0)).color(t.text)); + ui.label(egui::RichText::new(tl!("Welcome to VectorCraft")).font(theme::semibold(26.0)).color(t.text)); ui.add_space(4.0); - ui.label(egui::RichText::new("Vector illustration — fast, open, scriptable.").size(14.0).color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Vector illustration — fast, open, scriptable.")).size(14.0).color(t.text_dim)); ui.add_space(22.0); ui.horizontal(|ui| { - if widgets::primary_button(ui, "New file").clicked() { + if widgets::primary_button(ui, tl!("New file")).clicked() { app.run("file.newDialog", json!({})).ok(); } ui.add_space(8.0); - if widgets::secondary_button(ui, "Open").clicked() { + if widgets::secondary_button(ui, tl!("Open")).clicked() { app.run("file.open", json!({})).ok(); } }); ui.add_space(28.0); - ui.label(egui::RichText::new("Quickly start a new file").font(theme::semibold(14.0)).color(t.text)); + ui.label(egui::RichText::new(tl!("Quickly start a new file")).font(theme::semibold(14.0)).color(t.text)); ui.add_space(10.0); // A few of New Document's presets (`file.newPresets`), as its cards. let presets = ["Letter", "A4", "Web 1920×1080", "Phone 390×844", "Postcard", "Social Square Post 1080×1080"]; @@ -1303,7 +1303,7 @@ fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { } }); ui.add_space(28.0); - ui.label(egui::RichText::new("Community").font(theme::semibold(14.0)).color(t.text)); + ui.label(egui::RichText::new(tl!("Community")).font(theme::semibold(14.0)).color(t.text)); ui.add_space(10.0); crate::community::links(app, ui); } @@ -1334,18 +1334,18 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { let is_text = first.as_ref().is_some_and(|f| matches!(f.kind, NodeKind::Text(_))); let mut items: Vec<(&str, &str, &str)> = vec![]; // (label, icon, command) if n > 1 { - items.push(("Group", "group", "object.group")); - items.push(("Unite", "squares-unite", "object.pathfinder.unite")); + items.push((tl!("Group"), "group", "object.group")); + items.push((tl!("Unite"), "squares-unite", "object.pathfinder.unite")); } else if is_group { - items.push(("Ungroup", "ungroup", "object.ungroup")); - items.push(("Isolate", "square-dashed", "object.isolate")); + items.push((tl!("Ungroup"), "ungroup", "object.ungroup")); + items.push((tl!("Isolate"), "square-dashed", "object.isolate")); } else if is_text { - items.push(("Create Outlines", "type", "type.createOutlines")); + items.push((tl!("Create Outlines"), "type", "type.createOutlines")); } else { - items.push(("Offset Path", "square-dashed", "object.path.offsetPath")); - items.push(("Simplify", "spline", "object.path.simplify")); + items.push((tl!("Offset Path"), "square-dashed", "object.path.offsetPath")); + items.push((tl!("Simplify"), "spline", "object.path.simplify")); } - items.push(("Duplicate", "copy", "edit.duplicate")); + items.push((tl!("Duplicate"), "copy", "edit.duplicate")); let fill = first.as_ref().map(|f| f.appearance.fill_paint()).unwrap_or_default(); let anchor = xf.to_screen(Point::new(b.center().x, b.y1)); let est_w = 118.0 + items.iter().map(|(l, _, _)| l.len() as f32 * 7.2 + 44.0).sum::(); @@ -1385,14 +1385,14 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { Stroke::new(1.0, t.button_border), StrokeKind::Outside, ); - if resp.on_hover_text("Fill").clicked() { + if resp.on_hover_text(tl!("Fill")).clicked() { app.session.fill_active = true; app.ui.open_panel = Some("swatches".into()); } - if widgets::icon_button(ui, "lock", "Lock (⌘2)", false, 30.0).clicked() { + if widgets::icon_button(ui, "lock", tl!("Lock (⌘2)"), false, 30.0).clicked() { run = Some("object.lock".into()); } - if widgets::icon_button(ui, "ellipsis", "Hide Contextual Task Bar", false, 30.0).clicked() { + if widgets::icon_button(ui, "ellipsis", tl!("Hide Contextual Task Bar"), false, 30.0).clicked() { run = Some("window.taskBar".into()); } }); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 680ce95a7..cb93e998d 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -30,7 +30,7 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { crate::brand::paint_mark(ui, r); ui.add_space(4.0); let on_home = app.ui.home.is_some() || app.session.active().is_none(); - if widgets::icon_button(ui, "house", "Home", on_home, 24.0).clicked() { + if widgets::icon_button(ui, "house", tl!("Home"), on_home, 24.0).clicked() { app.run("app.home", json!({})).ok(); } ui.add_space(2.0); @@ -48,7 +48,10 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let right_edge = if custom { full.right() - titlebar::WIDTH - 10.0 } else { full.right() }; let right = egui::Rect::from_min_max(egui::pos2(menus_end + 8.0, full.top()), egui::pos2(right_edge, full.bottom())); let room = right.width(); - let ws = ui.painter().layout_no_wrap(app.ui.workspace.clone(), egui::FontId::proportional(12.0), t.text); + // Built-in workspace names translate; the user's own stay as typed. + let ws_name = + if crate::workspaces::is_builtin(&app.ui.workspace) { tl!(&app.ui.workspace).to_string() } else { app.ui.workspace.clone() }; + let ws = ui.painter().layout_no_wrap(ws_name, egui::FontId::proportional(12.0), t.text); let ws_w = (ws.size().x + 36.0).clamp(112.0, 190.0); let gap = ui.spacing().item_spacing.x; let with_search = ws_w + 8.0 + gap + 200.0; @@ -62,7 +65,7 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { ui.painter().rect_filled(wr, CornerRadius::same(4), if wresp.hovered() { t.hover } else { t.panel }); ui.painter().with_clip_rect(wr.shrink2(vec2(4.0, 0.0))).galley(wr.left_center() + vec2(10.0, -ws.size().y / 2.0), ws, t.text); icons::paint(ui, "chevron-down", egui::Rect::from_center_size(wr.right_center() - vec2(12.0, 0.0), vec2(12.0, 12.0)), t.text_dim); - let wresp = wresp.on_hover_text("Switch workspace"); + let wresp = wresp.on_hover_text(tl!("Switch workspace")); egui::Popup::menu(&wresp).show(|ui| crate::workspaces::popup(app, ui)); ui.add_space(8.0); // Search box → command palette. @@ -79,13 +82,13 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { ui.painter().text( r.left_center() + vec2(26.0, 0.0), egui::Align2::LEFT_CENTER, - "Search commands and tools", + tl!("Search commands and tools"), egui::FontId::proportional(11.5), t.text_dim, ); resp.clicked() } else { - widgets::icon_button(ui, "search", "Search commands and tools", false, 24.0).clicked() + widgets::icon_button(ui, "search", tl!("Search commands and tools"), false, 24.0).clicked() }; if open_palette { app.ui.palette_open = true; @@ -116,7 +119,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { ui.painter().circle_filled(g.center_top() + vec2(0.0, 2.0 + i as f32 * 4.0), 0.9, t.text_disabled); } let Some(st) = app.session.active() else { - ui.label(egui::RichText::new("No Document").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("No Document")).color(t.text_dim)); return; }; let sel = st.selection.objects.clone(); @@ -124,15 +127,15 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let first = sel.first().and_then(|id| st.doc.node(*id)).cloned(); let anchor_mode = !st.selection.anchors.is_empty(); let label = match &first { - Some(_) if sel.len() == 1 && anchor_mode => "Anchor Point", + Some(_) if sel.len() == 1 && anchor_mode => tl!("Anchor Point"), Some(vectorcraft_doc::Node { kind: NodeKind::Image(im), .. }) if sel.len() == 1 => { if im.link.is_some() { - "Linked File" + tl!("Linked File") } else { - "Embedded" + tl!("Embedded") } } - _ => crate::panels::appearance::object_label(app), + _ => tl!(crate::panels::appearance::object_label(app)), }; ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text)); ui.add_space(6.0); @@ -147,7 +150,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { crate::panels::paint_chip(app, ui, true, 22.0, true); // The link opens the Stroke panel as a popover under it; then the weight spinner // (with presets) and the width profile. - let link = ui.link(egui::RichText::new("Stroke:").size(12.0).color(t.text).underline()).on_hover_text("Stroke options"); + let link = ui.link(egui::RichText::new(tl!("Stroke:")).size(12.0).color(t.text).underline()).on_hover_text(tl!("Stroke options")); stroke_panel::popover(app, &link); stroke_panel::weight_field(app, ui, "cb-stroke", weight, 100.0); if let Some(id) = stroke_panel::profile_dropdown(app, ui, shown_stroke.as_ref().and_then(|s| s.profile.as_ref())) { @@ -155,7 +158,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.add_space(4.0); ui.separator(); - if ui.link(egui::RichText::new("Opacity:").size(12.0).color(t.text).underline()).clicked() { + if ui.link(egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline()).clicked() { app.ui.open_panel = Some("transparency".into()); } if let Some(o) = widgets::plain_field(ui, "cb-opacity", opacity as f64 * 100.0, "%", 0, 56.0) @@ -166,19 +169,19 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { if !sel.is_empty() { // Style picker: the selection's graphic style; its menu applies another. ui.add_space(4.0); - if ui.link(egui::RichText::new("Style:").size(12.0).color(t.text).underline()).clicked() { + if ui.link(egui::RichText::new(tl!("Style:")).size(12.0).color(t.text).underline()).clicked() { app.ui.open_panel = Some("graphicStyles".into()); } let resp = widgets::chip_button(ui, 22.0, true, |ui, r| crate::panels::graphic_styles::paint_linked(app, ui, r)) - .on_hover_text("Graphic Style"); + .on_hover_text(tl!("Graphic Style")); egui::Popup::menu(&resp).show(|ui| crate::panels::graphic_styles::picker(app, ui)); } ui.separator(); if sel.is_empty() { - if widgets::flat_button(ui, "Document Setup", 112.0).clicked() { + if widgets::flat_button(ui, tl!("Document Setup"), 112.0).clicked() { app.run("file.documentSetup", json!({})).ok(); } - if widgets::flat_button(ui, "Preferences", 90.0).clicked() { + if widgets::flat_button(ui, tl!("Preferences"), 90.0).clicked() { app.run("edit.preferences", json!({})).ok(); } return; @@ -192,7 +195,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { ("align-center-horizontal", "Vertical Align Center", json!({"vertical": "center"})), ("align-end-horizontal", "Vertical Align Bottom", json!({"vertical": "bottom"})), ] { - if widgets::icon_button(ui, icon, tip, false, 24.0).clicked() { + if widgets::icon_button(ui, icon, tl!(tip), false, 24.0).clicked() { let mut p = p; if sel.len() == 1 { p["to"] = json!("artboard"); @@ -211,7 +214,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { if k == "height" { crate::panels::transform::constrain_link(app, ui); } - ui.label(egui::RichText::new(lbl).size(12.0).color(t.text_dim)); + ui.label(egui::RichText::new(tl!(lbl)).size(12.0).color(t.text_dim)); if let Some(nv) = widgets::num_field(ui, ("cb", k), Some(v), units, 80.0) { app.run("object.setBounds", json!({k: nv, "reference": 4, "proportional": link})).ok(); } @@ -225,10 +228,10 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { /// "Name* @ 66.67 % (/Opacity Mask)". fn tab_title(d: &vectorcraft_engine::DocState, zoom: f64, outline: bool) -> String { let mode = if d.doc.mask_edit.is_some() { - "/Opacity Mask".to_string() + format!("<{0}>/{0}", tl!("Opacity Mask")) } else { let color = if d.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk { "CMYK" } else { "RGB" }; - format!("{color}/{}", if outline { "Outline" } else { "Preview" }) + format!("{color}/{}", if outline { tl!("Outline") } else { tl!("Preview") }) }; format!("{}{} @ {} ({mode})", d.title(), if d.is_dirty() { "*" } else { "" }, zoom_label(zoom).replace('%', " %")) } @@ -305,6 +308,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { if pattern_edit.is_some() { let mut x = bar.right() - 6.0; for (label, cmd) in [("Cancel", "object.pattern.cancel"), ("Done", "object.pattern.done"), ("Save a Copy", "object.pattern.saveCopy")] { + let label = tl!(label); let w = 12.0 + 7.0 * label.len() as f32; let r = egui::Rect::from_min_max(egui::pos2(x - w, bar.top() + 3.0), egui::pos2(x, bar.bottom() - 3.0)); let resp = ui.interact(r, ui.id().with(("pat-bar", cmd)), Sense::click()); @@ -346,10 +350,10 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) { } } ui.separator(); - if ui.selectable_label(false, "Fit on Screen").clicked() { + if ui.selectable_label(false, tl!("Fit on Screen")).clicked() { app.run("view.fitArtboard", json!({})).ok(); } - if ui.selectable_label(false, "Fit All").clicked() { + if ui.selectable_label(false, tl!("Fit All")).clicked() { app.run("view.fitAll", json!({})).ok(); } }, @@ -377,7 +381,13 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) { widgets::icon_button(ui, icon, "", false, 18.0); } ui.separator(); - let tool = vectorcraft_tools::tool_info(app.session.tool_id()).map(|t| t.label.trim_end_matches(" Tool")).unwrap_or(""); + let tool = vectorcraft_tools::tool_info(app.session.tool_id()) + .map(|t| { + // The catalog keys carry the full label ("Selection Tool"); only English drops the suffix. + let shown = tl!(t.label); + if crate::i18n::current() == crate::i18n::Lang::EN { shown.trim_end_matches(" Tool") } else { shown } + }) + .unwrap_or(""); ui.label(egui::RichText::new(tool).size(11.5).color(t.text_dim)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.label( @@ -563,19 +573,51 @@ fn hint_for(tool: &str) -> Option<&'static [(&'static str, bool)]> { /// The hint bar's segments for `tool`, with keys named as the menus name them (Alt and Ctrl on /// Windows and Linux, ⌥ and ⌘ on macOS). Tools without a hint point to Search Commands. +/// Every hint-bar fragment (the action after the last `+` of a chord included), so the catalog tests +/// can insist a complete language covers them. +pub fn hint_strings() -> Vec<&'static str> { + let mut v = Vec::new(); + for tool in vectorcraft_tools::catalog::all_tools() { + for &(text, bold) in hint_for(tool.id).unwrap_or(&[]) { + let text = if bold { text.rsplit_once('+').map_or(text, |(_, k)| k) } else { text }; + if !text.is_empty() && !text.chars().all(|c| c.is_ascii_digit()) && !v.contains(&text) { + v.push(text); + } + } + } + v +} + fn hint_segments(tool: &str) -> Vec<(Cow<'static, str>, bool)> { let search; let segments = match hint_for(tool) { Some(s) => s, None => { search = match menus::shortcut_of("help.commandPalette") { - Some(sc) => [("Press ", false), (sc, true), (" to search every command", false)], - None => [("Use ", false), ("Help › Search Commands…", true), (" to search every command", false)], + Some(sc) => [(tl!("Press "), false), (sc, true), (tl!(" to search every command"), false)], + None => [(tl!("Use "), false), (tl!("Help › Search Commands…"), true), (tl!(" to search every command"), false)], }; &search } }; - segments.iter().map(|&(text, bold)| (if bold { Cow::Owned(menus::pretty_shortcut(text)) } else { Cow::Borrowed(text) }, bold)).collect() + segments + .iter() + .map(|&(text, bold)| { + // Key names ("Click", "Esc") translate; in a chord ("Alt+Drag") the modifiers are named for + // the platform and only the action after the last `+` translates. + if !bold { + return (Cow::Borrowed(tl!(text)), bold); + } + match text.rsplit_once('+') { + Some((_, key)) if !key.is_empty() => { + let pretty = menus::pretty_shortcut(text); + let shown = pretty.strip_suffix(key).map_or(pretty.clone(), |m| format!("{m}{}", tl!(key))); + (Cow::Owned(shown), bold) + } + _ => (Cow::Owned(menus::pretty_shortcut(tl!(text))), bold), + } + }) + .collect() } pub fn hint_bar(app: &mut VectorcraftApp, ui: &mut Ui) { diff --git a/crates/ui-egui/src/community.rs b/crates/ui-egui/src/community.rs index 16ac8ea5a..ef63f4739 100644 --- a/crates/ui-egui/src/community.rs +++ b/crates/ui-egui/src/community.rs @@ -12,7 +12,7 @@ const DISCORD: Color32 = Color32::from_rgb(0x58, 0x65, 0xF2); /// The Discord button's label, height and width. fn discord_layout(ui: &Ui, large: bool) -> (std::sync::Arc, f32, f32) { - let (h, font, label) = if large { (36.0, theme::semibold(14.0), "Join our Discord") } else { (24.0, theme::semibold(12.0), "Discord") }; + let (h, font, label) = if large { (36.0, theme::semibold(14.0), tl!("Join our Discord")) } else { (24.0, theme::semibold(12.0), "Discord") }; let galley = ui.painter().layout_no_wrap(label.to_string(), font, Color32::WHITE); let w = galley.size().x + h * 0.55 + h * 0.9; (galley, h, w) @@ -33,7 +33,7 @@ pub fn discord_button(app: &mut VectorcraftApp, ui: &mut Ui, large: bool) -> egu let ir = egui::Rect::from_center_size(r.left_center() + vec2(h * 0.35 + icon / 2.0, 0.0), vec2(icon, icon)); icons::paint(ui, "message-circle", ir, Color32::WHITE); ui.painter().galley(egui::pos2(ir.right() + h * 0.2, r.center().y - galley.size().y / 2.0), galley, Color32::WHITE); - let resp = resp.on_hover_text("Join the ArtCraft community on Discord (discord.gg/artcraft)"); + let resp = resp.on_hover_text(tl!("Join the ArtCraft community on Discord (discord.gg/artcraft)")); if resp.clicked() { app.open_link("help.discord"); } @@ -58,9 +58,9 @@ pub fn links(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(8.0); let l = app.session.execute("help.links", &serde_json::json!({})).unwrap_or_default(); let s = |k: &str| l[k].as_str().unwrap_or("").to_string(); - link(app, ui, "globe", "ArtCraft website", "help.website", &s("website")); - link(app, ui, "external-link", "VectorCraft on getartcraft.com", "help.appPage", &s("appPage")); - link(app, ui, "git-branch", "Source code on GitHub", "help.github", &s("github")); + link(app, ui, "globe", tl!("ArtCraft website"), "help.website", &s("website")); + link(app, ui, "external-link", tl!("VectorCraft on getartcraft.com"), "help.appPage", &s("appPage")); + link(app, ui, "git-branch", tl!("Source code on GitHub"), "help.github", &s("github")); } #[cfg(test)] diff --git a/crates/ui-egui/src/dialogs/about.rs b/crates/ui-egui/src/dialogs/about.rs index eada4f22e..c28d08d3e 100644 --- a/crates/ui-egui/src/dialogs/about.rs +++ b/crates/ui-egui/src/dialogs/about.rs @@ -9,7 +9,7 @@ pub(super) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { return; } let mut open = true; - egui::Window::new("About VectorCraft") + egui::Window::new(tl!("About VectorCraft")) .collapsible(false) .resizable(false) .open(&mut open) @@ -22,16 +22,19 @@ pub(super) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { crate::brand::paint_mark(ui, r); ui.vertical(|ui| { ui.label(egui::RichText::new("VectorCraft").font(theme::semibold(22.0))); - ui.label(format!("Version {} — open-source vector illustration in pure Rust.", env!("CARGO_PKG_VERSION"))); + ui.label(crate::i18n::fmt( + tl!("Version {version} — open-source vector illustration in pure Rust."), + &[("version", env!("CARGO_PKG_VERSION"))], + )); }); }); ui.add_space(12.0); crate::community::links(app, ui); ui.add_space(12.0); ui.label( - egui::RichText::new( - "Part of ArtCraft. MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + VectorCraft.", - ) + egui::RichText::new(tl!( + "Part of ArtCraft. MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + VectorCraft." + )) .size(11.0), ); }); diff --git a/crates/ui-egui/src/dialogs/all_tools.rs b/crates/ui-egui/src/dialogs/all_tools.rs index a9f6dd767..a40c777b4 100644 --- a/crates/ui-egui/src/dialogs/all_tools.rs +++ b/crates/ui-egui/src/dialogs/all_tools.rs @@ -4,14 +4,16 @@ use super::DialogSpec; use crate::VectorcraftApp; use crate::state::Dialog; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "All Tools".into(), body, ok: None, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("All Tools").into(), body, ok: None, ..DialogSpec::FORM }; fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, _: &mut Dialog) -> bool { let mut picked = false; for g in vectorcraft_tools::TOOL_GROUPS { ui.horizontal_wrapped(|ui| { for tool in g.iter() { - if ui.button(tool.label.trim_end_matches(" Tool")).clicked() { + let shown = tl!(tool.label); + let shown = if crate::i18n::current() == crate::i18n::Lang::EN { shown.trim_end_matches(" Tool") } else { shown }; + if ui.button(shown).clicked() { app.select_tool(tool.id); picked = true; } diff --git a/crates/ui-egui/src/dialogs/artboard_options.rs b/crates/ui-egui/src/dialogs/artboard_options.rs index b47c92b79..ec2d05072 100644 --- a/crates/ui-egui/src/dialogs/artboard_options.rs +++ b/crates/ui-egui/src/dialogs/artboard_options.rs @@ -7,7 +7,7 @@ use super::{DialogSpec, run_and_close}; use crate::VectorcraftApp; use crate::state::Dialog; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Artboard Options".into(), confirm, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Artboard Options").into(), confirm, ..DialogSpec::FORM }; fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let mut p = Value::Object(d.fields.clone()); diff --git a/crates/ui-egui/src/dialogs/blend_options.rs b/crates/ui-egui/src/dialogs/blend_options.rs index 83af48e2e..86deabcba 100644 --- a/crates/ui-egui/src/dialogs/blend_options.rs +++ b/crates/ui-egui/src/dialogs/blend_options.rs @@ -28,7 +28,7 @@ const LABEL_W: f32 = 82.0; const SPACING: [(&str, &str); 3] = [("smooth", "Smooth Color"), ("steps", "Specified Steps"), ("distance", "Specified Distance")]; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Blend Options".into(), body, confirm, preview: true, min_width: 300.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Blend Options").into(), body, confirm, preview: true, min_width: 300.0, ..DialogSpec::FORM }; /// Open Blend Options on the selected blend's options (else those new blends start with). pub fn open(app: &mut VectorcraftApp) -> Result { @@ -63,7 +63,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let mode = d.str("spacing"); let shown = SPACING.iter().find(|(v, _)| *v == mode).map_or(SPACING[0].1, |(_, l)| l); let labels = SPACING.map(|(_, l)| l); - widgets::label_row(ui, "Spacing:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Spacing:"), LABEL_W, |ui| { if let Some((value, _)) = widgets::dropdown(ui, "blend-spacing", shown, &labels, 150.0).and_then(|i| SPACING.get(i)) { d.fields.insert("spacing".into(), json!(value)); } @@ -84,9 +84,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } }); ui.add_space(6.0); - widgets::label_row(ui, "Orientation:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Orientation:"), LABEL_W, |ui| { let path = d.str("orientation") == "path"; - for (value, along, tip) in [("page", false, "Align to Page"), ("path", true, "Align to Path")] { + for (value, along, tip) in [("page", false, tl!("Align to Page")), ("path", true, tl!("Align to Path"))] { if orientation_button(ui, along, path == along, tip) { d.fields.insert("orientation".into(), json!(value)); } diff --git a/crates/ui-egui/src/dialogs/color_balance.rs b/crates/ui-egui/src/dialogs/color_balance.rs index 930041073..65ca83056 100644 --- a/crates/ui-egui/src/dialogs/color_balance.rs +++ b/crates/ui-egui/src/dialogs/color_balance.rs @@ -21,7 +21,7 @@ pub const KIND: &str = "colorBalance"; const CMD: &str = "edit.colors.adjustBalance"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Adjust Colors".into(), body, confirm, preview: true, min_width: 340.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Adjust Colors").into(), body, confirm, preview: true, min_width: 340.0, ..DialogSpec::FORM }; /// A colour mode: its `mode` id, label and channels as (field, label). struct Mode { @@ -83,28 +83,28 @@ fn track(key: &str, x: f32) -> egui::Color32 { fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let m = mode(d); ui.horizontal(|ui| { - widgets::dim_label(ui, "Color Mode:"); + widgets::dim_label(ui, tl!("Color Mode:")); let labels: Vec<&str> = MODES.iter().map(|m| m.label).collect(); if let Some(i) = widgets::dropdown(ui, "balance-mode", m.label, &labels, 120.0) { d.fields.insert("mode".into(), json!(MODES[i].id)); } ui.add_space(12.0); - if widgets::check(ui, "Convert", d.bool("convert"), m.id != GLOBAL) { + if widgets::check(ui, tl!("Convert"), d.bool("convert"), m.id != GLOBAL) { d.fields.insert("convert".into(), json!(!d.bool("convert"))); } }); ui.add_space(8.0); let m = mode(d); if m.id == GLOBAL { - widgets::dim_label(ui, "Shifts the tints of global and spot colors; other colors stay."); + widgets::dim_label(ui, tl!("Shifts the tints of global and spot colors; other colors stay.")); } for (k, label) in m.channels { form::slider(ui, d, k, label, -100.0..=100.0, "%", &|x| track(k, x)); } ui.add_space(8.0); ui.horizontal(|ui| { - widgets::dim_label(ui, "Adjust Options:"); - for (k, label) in [("fill", "Fill"), ("stroke", "Stroke")] { + widgets::dim_label(ui, tl!("Adjust Options:")); + for (k, label) in [("fill", tl!("Fill")), ("stroke", tl!("Stroke"))] { if widgets::check(ui, label, d.bool(k), true) { d.fields.insert(k.into(), json!(!d.bool(k))); } diff --git a/crates/ui-egui/src/dialogs/color_guide_options.rs b/crates/ui-egui/src/dialogs/color_guide_options.rs index 55975166c..9e61ec57e 100644 --- a/crates/ui-egui/src/dialogs/color_guide_options.rs +++ b/crates/ui-egui/src/dialogs/color_guide_options.rs @@ -17,7 +17,8 @@ use crate::theme::Tokens; /// The dialog kind of Color Guide Options. pub const KIND: &str = "colorGuideOptions"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Color Guide Options".into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Color Guide Options").into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM }; /// Open the dialog with the panel's current options. pub fn open(app: &mut VectorcraftApp) { @@ -38,14 +39,14 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let v = (60.0 + 160.0 * x) as u8; egui::Color32::from_rgb(v, v, v) }; - form::slider(ui, d, "steps", "Steps:", 1.0..=max, "", &ramp); - form::slider(ui, d, "amount", "Variation:", 0.0..=100.0, "%", &ramp); + form::slider(ui, d, "steps", tl!("Steps:"), 1.0..=max, "", &ramp); + form::slider(ui, d, "amount", tl!("Variation:"), 0.0..=100.0, "%", &ramp); ui.horizontal(|ui| { ui.add_space(form::SLIDER_LABEL + ui.spacing().item_spacing.x); let (r, _) = ui.allocate_exact_size(vec2(form::SLIDER_WIDTH, 14.0), Sense::hover()); let font = egui::FontId::proportional(11.0); - ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, "Less", font.clone(), t.text_dim); - ui.painter().text(r.right_center(), egui::Align2::RIGHT_CENTER, "More", font, t.text_dim); + ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, tl!("Less"), font.clone(), t.text_dim); + ui.painter().text(r.right_center(), egui::Align2::RIGHT_CENTER, tl!("More"), font, t.text_dim); }); ui.add_space(10.0); // Preview: the base colour's row of variations. diff --git a/crates/ui-egui/src/dialogs/color_picker.rs b/crates/ui-egui/src/dialogs/color_picker.rs index 790f9f23e..11ed35716 100644 --- a/crates/ui-egui/src/dialogs/color_picker.rs +++ b/crates/ui-egui/src/dialogs/color_picker.rs @@ -24,7 +24,7 @@ use crate::{VectorcraftApp, widgets}; pub(super) const KIND: &str = "colorPicker"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Color Picker".into(), body, confirm, min_width: 600.0, max_width: Some(640.0), ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Color Picker").into(), body, confirm, min_width: 600.0, max_width: Some(640.0), ..DialogSpec::FORM }; /// Side of the colour field (and height of the channel slider). const FIELD: f32 = 240.0; @@ -222,7 +222,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { } }); ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { - toggle_swatches = widgets::flat_button(ui, if swatches { "Color Models" } else { "Color Swatches" }, 110.0).clicked(); + toggle_swatches = widgets::flat_button(ui, if swatches { tl!("Color Models") } else { tl!("Color Swatches") }, 110.0).clicked(); }); }); ui.add_space(10.0); @@ -232,7 +232,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { }); }); ui.add_space(8.0); - if widgets::check(ui, "Only Web Colors", web, true) { + if widgets::check(ui, tl!("Only Web Colors"), web, true) { d.fields.insert("webOnly".into(), json!(!web)); } if let Some(ch) = new_channel { @@ -254,8 +254,8 @@ fn new_original_chips(ui: &mut Ui, new: &Color, original: &Color) -> bool { widgets::paint_chip(ui, top, &Paint::solid(*new)); widgets::paint_chip(ui, bottom, &Paint::solid(*original)); ui.painter().rect_stroke(r, 0.0, egui::Stroke::new(1.0, t.border), egui::StrokeKind::Outside); - ui.interact(top, ui.id().with("cp-new"), Sense::hover()).on_hover_text("New"); - ui.interact(bottom, ui.id().with("cp-original"), Sense::click()).on_hover_text("Original: click to restore").clicked() + ui.interact(top, ui.id().with("cp-new"), Sense::hover()).on_hover_text(tl!("New")); + ui.interact(bottom, ui.id().with("cp-original"), Sense::click()).on_hover_text(tl!("Original: click to restore")).clicked() } /// The HSB and RGB fields with the channel radios, hex, Lab and CMYK. Returns the edited colour @@ -334,7 +334,7 @@ fn swatch_list(app: &VectorcraftApp, ui: &mut Ui, current: &Color) -> Option Result { @@ -79,28 +79,28 @@ fn unit(d: &Dialog) -> Unit { fn general(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let units: Vec<(&str, &str)> = Unit::ALL.iter().map(|u| (u.label(), u.label())).collect(); - choice(ui, d, "units", "Units:", &units); + choice(ui, d, "units", tl!("Units:"), &units); ui.add_space(6.0); let unit = unit(d); - widgets::label_row(ui, "Bleed:", LABEL, |ui| form::bleed(ui, d, unit, 64.0)); + widgets::label_row(ui, tl!("Bleed:"), LABEL, |ui| form::bleed(ui, d, unit, 64.0)); ui.add_space(6.0); widgets::label_row(ui, "", LABEL, |ui| { // Applies the settings, then edits the artboards with the Artboard tool. - if widgets::flat_button(ui, "Edit Artboards", 120.0).clicked() && confirm(app, d).is_ok() { + if widgets::flat_button(ui, tl!("Edit Artboards"), 120.0).clicked() && confirm(app, d).is_ok() { app.select_tool("artboard"); } }); ui.add_space(12.0); - check(ui, d, "outlineImages", "Show Images in Outline Mode"); - check(ui, d, "highlightSubstitutedFonts", "Highlight Substituted Fonts"); - check(ui, d, "highlightSubstitutedGlyphs", "Highlight Substituted Glyphs"); + check(ui, d, "outlineImages", tl!("Show Images in Outline Mode")); + check(ui, d, "highlightSubstitutedFonts", tl!("Highlight Substituted Fonts")); + check(ui, d, "highlightSubstitutedGlyphs", tl!("Highlight Substituted Glyphs")); } fn transparency(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let t = Tokens::get(ui.ctx()); - widgets::subheader(ui, "Transparency Grid"); + widgets::subheader(ui, tl!("Transparency Grid")); let sizes: Vec<(&str, &str)> = GridSize::ALL.iter().map(|g| (g.id(), g.label())).collect(); - choice(ui, d, "gridSize", "Grid Size:", &sizes); + choice(ui, d, "gridSize", tl!("Grid Size:"), &sizes); let colors: Vec = d .fields .get("gridColors") @@ -112,7 +112,7 @@ fn transparency(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { .find(|(_, pair)| colors.len() == 2 && colors.iter().zip(pair).all(|(h, c)| h.eq_ignore_ascii_case(&c.to_hex()))) .map_or("Custom", |(n, _)| n); let names: Vec<&str> = GRID_COLOR_PRESETS.iter().map(|(n, _)| *n).chain(["Custom"]).collect(); - widgets::label_row(ui, "Grid Colors:", LABEL, |ui| { + widgets::label_row(ui, tl!("Grid Colors:"), LABEL, |ui| { if let Some((_, pair)) = widgets::dropdown(ui, "gridColors", preset, &names, 220.0).and_then(|i| GRID_COLOR_PRESETS.get(i)) { d.fields.insert("gridColors".into(), json!(pair.map(|c| c.to_hex()))); } @@ -136,27 +136,29 @@ fn transparency(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { } }); ui.add_space(4.0); - check(ui, d, "simulatePaper", "Simulate Colored Paper"); + check(ui, d, "simulatePaper", tl!("Simulate Colored Paper")); ui.add_space(14.0); - widgets::subheader(ui, "Export and Clipboard Transparency Flattener Settings"); + widgets::subheader(ui, tl!("Export and Clipboard Transparency Flattener Settings")); let presets: Vec = app.session.flattener_presets().into_iter().map(|p| p.name).collect(); let options: Vec<(&str, &str)> = presets.iter().map(|p| (p.as_str(), p.as_str())).collect(); - choice(ui, d, "flattenerPreset", "Preset:", &options); - check(ui, d, "discardWhiteOverprint", "Discard White Overprint in Output"); + choice(ui, d, "flattenerPreset", tl!("Preset:"), &options); + check(ui, d, "discardWhiteOverprint", tl!("Discard White Overprint in Output")); } fn typography(ui: &mut egui::Ui, d: &mut Dialog) { let t = Tokens::get(ui.ctx()); let langs: Vec<(&str, &str)> = LANGUAGES.iter().map(|(l, _)| (*l, *l)).collect(); let before = d.str("language"); - choice(ui, d, "language", "Language:", &langs); + choice(ui, d, "language", tl!("Language:"), &langs); // A language brings its quotes. if d.str("language") != before && let Some(q) = vectorcraft_doc::setup::language_quotes(&d.str("language")) { d.fields.insert("quotes".into(), json!({"double": q.double.iter().collect::(), "single": q.single.iter().collect::()})); } - for (key, label, choices) in [("double", "Double Quotes:", Quotes::DOUBLE_CHOICES), ("single", "Single Quotes:", Quotes::SINGLE_CHOICES)] { + for (key, label, choices) in + [("double", tl!("Double Quotes:"), Quotes::DOUBLE_CHOICES), ("single", tl!("Single Quotes:"), Quotes::SINGLE_CHOICES)] + { let pairs: Vec = choices.iter().map(|p| p.iter().collect()).collect(); let labels: Vec<&str> = pairs.iter().map(String::as_str).collect(); let cur = d.fields.get("quotes").and_then(|q| q.get(key)).and_then(Value::as_str).unwrap_or("").to_string(); @@ -168,16 +170,16 @@ fn typography(ui: &mut egui::Ui, d: &mut Dialog) { } }); } - check(ui, d, "typographersQuotes", "Use Typographer's Quotes"); + check(ui, d, "typographersQuotes", tl!("Use Typographer's Quotes")); ui.add_space(14.0); - widgets::subheader(ui, "Options"); + widgets::subheader(ui, tl!("Options")); egui::Grid::new("docsetup-scripts").num_columns(3).spacing([16.0, 6.0]).show(ui, |ui| { ui.label(""); - for h in ["Size", "Position"] { + for h in [tl!("Size"), tl!("Position")] { ui.label(egui::RichText::new(h).size(11.0).color(t.text_dim)); } ui.end_row(); - for (key, label) in [("superscript", "Superscript:"), ("subscript", "Subscript:")] { + for (key, label) in [("superscript", tl!("Superscript:")), ("subscript", tl!("Subscript:"))] { ui.label(egui::RichText::new(label).color(t.text)); for part in ["size", "position"] { let v = d.fields.get(key).and_then(|m| m.get(part)).and_then(Value::as_f64).unwrap_or(0.0); @@ -189,14 +191,14 @@ fn typography(ui: &mut egui::Ui, d: &mut Dialog) { } ui.end_row(); } - ui.label(egui::RichText::new("Small Caps:").color(t.text)); + ui.label(egui::RichText::new(tl!("Small Caps:")).color(t.text)); if let Some(n) = widgets::plain_field(ui, "smallCaps", d.f64("smallCapsSize", 70.0), "%", 1, 70.0) { d.fields.insert("smallCapsSize".into(), json!(n)); } ui.end_row(); }); ui.add_space(6.0); - choice(ui, d, "exportText", "Export:", &[("editable", "Preserve Text Editability"), ("appearance", "Preserve Text Appearance")]); + choice(ui, d, "exportText", tl!("Export:"), &[("editable", tl!("Preserve Text Editability")), ("appearance", tl!("Preserve Text Appearance"))]); } #[cfg(test)] diff --git a/crates/ui-egui/src/dialogs/dxf_import.rs b/crates/ui-egui/src/dialogs/dxf_import.rs index f5f4c6bd2..896e3ae24 100644 --- a/crates/ui-egui/src/dialogs/dxf_import.rs +++ b/crates/ui-egui/src/dialogs/dxf_import.rs @@ -22,7 +22,7 @@ use crate::{VectorcraftApp, io, widgets}; pub const KIND: &str = "dxfImport"; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |d| if d.str("mode") == "place" { "Place DXF" } else { "DXF Import Options" }.into(), + heading: |d| if d.str("mode") == "place" { tl!("Place DXF") } else { tl!("DXF Import Options") }.into(), body, confirm, min_width: 560.0, @@ -141,19 +141,19 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let layouts: Vec = d.fields.get("layouts").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect(); let rows: Vec<(&str, &str)> = layouts.iter().map(|l| (l.as_str(), l.as_str())).collect(); - choice(ui, d, "layout", "Layout:", &rows); + choice(ui, d, "layout", tl!("Layout:"), &rows); ui.add_space(10.0); - widgets::subheader(ui, "Scaling"); - check(ui, d, "fit", "Scale to Fit Artboard"); + widgets::subheader(ui, tl!("Scaling")); + check(ui, d, "fit", tl!("Scale to Fit Artboard")); ui.add_enabled_ui(!d.bool("fit"), |ui| super::dxf_options::scale_row(ui, d)); ui.horizontal(|ui| { ui.add_space(LABEL + 8.0); - check(ui, d, "scaleLineweights", "Scale Lineweights"); + check(ui, d, "scaleLineweights", tl!("Scale Lineweights")); }); ui.add_space(10.0); - widgets::subheader(ui, "Options"); - check(ui, d, "center", "Center Artwork"); - check(ui, d, "mergeLayers", "Merge Layers"); + widgets::subheader(ui, tl!("Options")); + check(ui, d, "center", tl!("Center Artwork")); + check(ui, d, "mergeLayers", tl!("Merge Layers")); }); }); false diff --git a/crates/ui-egui/src/dialogs/dxf_options.rs b/crates/ui-egui/src/dialogs/dxf_options.rs index d3240ca86..3593dfe91 100644 --- a/crates/ui-egui/src/dialogs/dxf_options.rs +++ b/crates/ui-egui/src/dialogs/dxf_options.rs @@ -18,8 +18,15 @@ use crate::{VectorcraftApp, io, widgets}; pub(super) const KIND: &str = "dxfOptions"; -pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "DXF Options".into(), body, confirm, ok: Some("Export"), min_width: 420.0, max_width: Some(460.0), ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("DXF Options").into(), + body, + confirm, + ok: Some("Export"), + min_width: 420.0, + max_width: Some(460.0), + ..DialogSpec::FORM +}; /// Fields that are not remembered between exports (they belong to one export). const PER_EXPORT: [&str; 7] = ["format", "path", "useArtboards", "range", "artboard", "artboards", "selectedOnly"]; @@ -59,24 +66,24 @@ pub fn open(app: &mut VectorcraftApp, params: &Value) { fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let versions: Vec<(&str, &str)> = DxfVersion::ALL.iter().map(|v| (v.id(), v.label())).collect(); - choice(ui, d, "version", "Version:", &versions); + choice(ui, d, "version", tl!("Version:"), &versions); ui.add_space(4.0); scale_row(ui, d); ui.horizontal(|ui| { ui.add_space(LABEL + 8.0); - check(ui, d, "scaleLineweights", "Scale Lineweights"); + check(ui, d, "scaleLineweights", tl!("Scale Lineweights")); }); ui.add_space(4.0); let depths: Vec<(&str, &str)> = ColorDepth::ALL.iter().map(|c| (c.id(), c.label())).collect(); - choice(ui, d, "colors", "Number of Colors:", &depths); + choice(ui, d, "colors", tl!("Number of Colors:"), &depths); let version = DxfVersion::from_id(&d.str("version")).unwrap_or_default(); if ColorDepth::from_id(&d.str("colors")) == Some(ColorDepth::True) && version < DxfVersion::R2004 { - note(ui, "True colour needs version 2004 or later: the nearest of 256 colours is written."); + note(ui, tl!("True colour needs version 2004 or later: the nearest of 256 colours is written.")); } let rasters: Vec<(&str, &str)> = RasterFormat::ALL.iter().map(|r| (r.id(), r.label())).collect(); - choice(ui, d, "rasterFormat", "Raster File Format:", &rasters); + choice(ui, d, "rasterFormat", tl!("Raster File Format:"), &rasters); ui.add_space(10.0); - widgets::subheader(ui, "Export Option"); + widgets::subheader(ui, tl!("Export Option")); let preserve = Preserve::from_id(&d.str("preserve")).unwrap_or_default(); ui.horizontal(|ui| { for p in Preserve::ALL { @@ -88,21 +95,21 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); ui.add_space(4.0); let selection = app.session.active().is_some_and(|st| !st.selection.is_empty()); - if widgets::check(ui, "Export Selected Art Only", d.bool("selectedOnly") && selection, selection) { + if widgets::check(ui, tl!("Export Selected Art Only"), d.bool("selectedOnly") && selection, selection) { d.fields.insert("selectedOnly".into(), json!(!d.bool("selectedOnly"))); } - check(ui, d, "alterPaths", "Alter Paths for Appearance"); + check(ui, d, "alterPaths", tl!("Alter Paths for Appearance")); // Preserve Appearance always outlines type. let appearance = preserve == Preserve::Appearance; - if widgets::check(ui, "Outline Text", appearance || d.bool("outlineText"), !appearance) { + if widgets::check(ui, tl!("Outline Text"), appearance || d.bool("outlineText"), !appearance) { d.fields.insert("outlineText".into(), json!(!d.bool("outlineText"))); } ui.add_space(8.0); super::form::caption( ui, match preserve { - Preserve::Appearance => "Type becomes outlines, and strokes a CAD line can't draw become filled shapes.", - Preserve::Editability => "Type stays text, and every stroke is a line with its lineweight and dashes.", + Preserve::Appearance => tl!("Type becomes outlines, and strokes a CAD line can't draw become filled shapes."), + Preserve::Editability => tl!("Type stays text, and every stroke is a line with its lineweight and dashes."), }, ); false @@ -113,7 +120,7 @@ pub(super) fn scale_row(ui: &mut egui::Ui, d: &mut Dialog) { let t = crate::theme::Tokens::get(ui.ctx()); let units: Vec<&str> = Unit::ALL.iter().filter(|u| **u != Unit::FeetInches).map(|u| u.label()).collect(); let unit = Unit::named(&d.str("unit")).unwrap_or(Unit::Millimeters); - widgets::label_row(ui, "Scale:", LABEL, |ui| { + widgets::label_row(ui, tl!("Scale:"), LABEL, |ui| { ui.label(egui::RichText::new("1").color(t.text)); if let Some(u) = widgets::dropdown(ui, "dxf-unit", unit.label(), &units, 120.0).and_then(|i| units.get(i)) { d.fields.insert("unit".into(), json!(u)); @@ -124,7 +131,7 @@ pub(super) fn scale_row(ui: &mut egui::Ui, d: &mut Dialog) { let (lo, hi) = (*vectorcraft_cad::SCALE_RANGE.start(), *vectorcraft_cad::SCALE_RANGE.end()); d.fields.insert("scale".into(), json!(v.clamp(lo, hi))); } - ui.label(egui::RichText::new("Units").color(t.text)); + ui.label(egui::RichText::new(tl!("Units")).color(t.text)); }); } diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index 071370c25..27adf9cba 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -17,7 +17,16 @@ use crate::state::Dialog; use crate::theme::Tokens; use crate::{VectorcraftApp, widgets}; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| d.str("__label"), body, confirm, preview: true, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| heading_label(d), body, confirm, preview: true, ..DialogSpec::FORM }; + +/// The effect's name (`__label`, English) in the UI language: the catalog may carry it with or +/// without its trailing ellipsis. +fn heading_label(d: &Dialog) -> String { + let label = d.str("__label"); + let dotted = format!("{label}…"); + let shown = tl!(&dotted); + if shown != dotted { shown.trim_end_matches('…').to_string() } else { tl!(&label).to_string() } +} /// The dialog kind asking whether to edit an effect that is already applied or add another. /// Fields: `__effect`, `__label`, `__item?` (as given to `effect.dialog`), `__target` and @@ -26,9 +35,9 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| d.str("__label"), pub const EXISTS: &str = "effectExists"; pub(super) const EXISTS_SPEC: DialogSpec = DialogSpec { - heading: |d| format!("{} is already applied", d.str("__label")), + heading: |d| crate::i18n::fmt(tl!("{effect} is already applied"), &[("effect", &heading_label(d))]), body: |_, ui, _| { - ui.label(egui::RichText::new("Edit the applied effect, or add another one?").color(Tokens::get(ui.ctx()).text_dim)); + ui.label(egui::RichText::new(tl!("Edit the applied effect, or add another one?")).color(Tokens::get(ui.ctx()).text_dim)); false }, confirm: confirm_exists, @@ -123,7 +132,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }; ui.add_space(6.0); let mut pv = d.bool("preview"); - let pv_changed = ui.checkbox(&mut pv, "Preview").changed(); + let pv_changed = ui.checkbox(&mut pv, tl!("Preview")).changed(); d.fields.insert("preview".into(), json!(pv)); if pv && (changed || pv_changed || !app.session.in_interaction()) { let label = d.str("__label"); @@ -189,7 +198,14 @@ fn adjust_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool { form::slider_w(ui, d, (key, label, LABEL_W), *min..=*max, "", rail); } if d.fields.contains_key("channel") { - form::choice(ui, d, "channel", "Channel", (LABEL_W, 120.0), &[("rgb", "RGB"), ("red", "Red"), ("green", "Green"), ("blue", "Blue")]); + form::choice( + ui, + d, + "channel", + tl!("Channel"), + (LABEL_W, 120.0), + &[("rgb", "RGB"), ("red", tl!("Red")), ("green", tl!("Green")), ("blue", tl!("Blue"))], + ); } if d.fields.contains_key("points") { ui.horizontal(|ui| { @@ -197,7 +213,7 @@ fn adjust_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool { curve_graph(ui, d); }); } - for (key, label, width) in [("points", "Points", 220.0), ("color", "Color", 120.0)] { + for (key, label, width) in [("points", tl!("Points"), 220.0), ("color", tl!("Color"), 120.0)] { if d.fields.contains_key(key) { widgets::label_row(ui, label, LABEL_W, |ui| { form::text(ui, d, key, width); @@ -205,14 +221,14 @@ fn adjust_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool { } } if d.fields.contains_key("gamma") { - widgets::label_row(ui, "Gamma", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Gamma"), LABEL_W, |ui| { let mut g = d.f64("gamma", 1.0); if ui.add(egui::DragValue::new(&mut g).speed(0.01).range(0.1..=10.0).max_decimals(2)).changed() { d.fields.insert("gamma".into(), json!(g)); } }); } - for (key, label) in [("colorize", "Colorize"), ("preserveLightness", "Preserve Lightness")] { + for (key, label) in [("colorize", tl!("Colorize")), ("preserveLightness", tl!("Preserve Lightness"))] { if d.fields.contains_key(key) { form::check(ui, d, key, label); } diff --git a/crates/ui-egui/src/dialogs/envelope.rs b/crates/ui-egui/src/dialogs/envelope.rs index 06516902f..0a4d4aa36 100644 --- a/crates/ui-egui/src/dialogs/envelope.rs +++ b/crates/ui-egui/src/dialogs/envelope.rs @@ -44,10 +44,10 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, preview fn heading(d: &Dialog) -> String { match d.kind.as_str() { - WARP => "Warp Options", - MESH if d.bool("reset") => "Reset Envelope Mesh", - MESH => "Envelope Mesh", - _ => "Envelope Options", + WARP => tl!("Warp Options"), + MESH if d.bool("reset") => tl!("Reset Envelope Mesh"), + MESH => tl!("Envelope Mesh"), + _ => tl!("Envelope Options"), } .into() } @@ -143,44 +143,44 @@ fn count(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) { fn warp_body(ui: &mut egui::Ui, d: &mut Dialog) { let styles: Vec<(&str, &str)> = WARP_STYLES.iter().map(|(id, label)| (*id, label.trim_end_matches('…'))).collect(); - form::choice(ui, d, "style", "Style:", (LABEL_W, 160.0), &styles); + form::choice(ui, d, "style", tl!("Style:"), (LABEL_W, 160.0), &styles); let horizontal = d.bool("horizontal"); widgets::label_row(ui, "", LABEL_W, |ui| { - for (h, label) in [(true, "Horizontal"), (false, "Vertical")] { + for (h, label) in [(true, tl!("Horizontal")), (false, tl!("Vertical"))] { if widgets::radio(ui, label, horizontal == h, true) { set(d, "horizontal", json!(h)); } ui.add_space(10.0); } }); - percent(ui, d, "bend", "Bend:"); + percent(ui, d, "bend", tl!("Bend:")); ui.add_space(6.0); - widgets::subheader(ui, "Distortion"); - percent(ui, d, "h", "Horizontal:"); - percent(ui, d, "v", "Vertical:"); + widgets::subheader(ui, tl!("Distortion")); + percent(ui, d, "h", tl!("Horizontal:")); + percent(ui, d, "v", tl!("Vertical:")); } fn mesh_body(ui: &mut egui::Ui, d: &mut Dialog) { - count(ui, d, "rows", "Rows:"); - count(ui, d, "cols", "Columns:"); + count(ui, d, "rows", tl!("Rows:")); + count(ui, d, "cols", tl!("Columns:")); if d.bool("reset") { ui.add_space(4.0); - form::check(ui, d, "maintainShape", "Maintain Envelope Shape"); + form::check(ui, d, "maintainShape", tl!("Maintain Envelope Shape")); } } fn options_body(ui: &mut egui::Ui, d: &mut Dialog) { - widgets::subheader(ui, "Rasters"); + widgets::subheader(ui, tl!("Rasters")); ui.horizontal(|ui| { ui.add_space(12.0); - form::check(ui, d, "antiAlias", "Anti-Alias"); + form::check(ui, d, "antiAlias", tl!("Anti-Alias")); }); ui.add_space(4.0); - widgets::subheader(ui, "Preserve Shape Using:"); + widgets::subheader(ui, tl!("Preserve Shape Using:")); let clip = d.str("preserveShape") != "transparency"; ui.horizontal(|ui| { ui.add_space(12.0); - for (value, label) in [("clippingMask", "Clipping Mask"), ("transparency", "Transparency")] { + for (value, label) in [("clippingMask", tl!("Clipping Mask")), ("transparency", tl!("Transparency"))] { if widgets::radio(ui, label, clip == (value == "clippingMask"), true) { set(d, "preserveShape", json!(value)); } @@ -189,12 +189,12 @@ fn options_body(ui: &mut egui::Ui, d: &mut Dialog) { }); ui.add_space(4.0); let rail = Tokens::get(ui.ctx()).input_border; - form::slider_w(ui, d, ("fidelity", "Fidelity:", LABEL_W), 0.0..=100.0, "", &move |_| rail); + form::slider_w(ui, d, ("fidelity", tl!("Fidelity:"), LABEL_W), 0.0..=100.0, "", &move |_| rail); ui.add_space(4.0); - form::check(ui, d, "distortAppearance", "Distort Appearance"); + form::check(ui, d, "distortAppearance", tl!("Distort Appearance")); // Gradients and patterns bend only with the appearance. let appearance = d.bool("distortAppearance"); - for (key, label) in [("distortLinearGradients", "Distort Linear Gradients"), ("distortPatternFills", "Distort Pattern Fills")] { + for (key, label) in [("distortLinearGradients", tl!("Distort Linear Gradients")), ("distortPatternFills", tl!("Distort Pattern Fills"))] { let on = d.bool(key); ui.horizontal(|ui| { ui.add_space(20.0); @@ -256,7 +256,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let Some((editing, bar)) = bar_of(app) else { return }; let mut run: Option<(&str, Value)> = None; ui.separator(); - for (contents, icon, tip) in [(false, "dc-mesh", "Edit Envelope"), (true, "shapes", "Edit Contents")] { + for (contents, icon, tip) in [(false, "dc-mesh", tl!("Edit Envelope")), (true, "shapes", tl!("Edit Contents"))] { if widgets::icon_button(ui, icon, tip, editing == contents, 24.0).clicked() && editing != contents { run = Some(("object.envelope.editContents", json!({ "editing": contents }))); } @@ -270,23 +270,23 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) { Bar::Warp { style, bend, h, v, horizontal } => { let styles: Vec<&str> = WARP_STYLES.iter().map(|(_, l)| l.trim_end_matches('…')).collect(); let current = WARP_STYLES.iter().find(|(id, _)| id == style).map_or(style.as_str(), |(_, l)| l.trim_end_matches('…')); - widgets::dim_label(ui, "Style:"); + widgets::dim_label(ui, tl!("Style:")); if let Some((id, _)) = widgets::dropdown(ui, "cb-envelope-style", current, &styles, 110.0).and_then(|i| WARP_STYLES.get(i)) { run = Some((OPTIONS_CMD, json!({ "style": id }))); } - for (h, label) in [(true, "Horizontal"), (false, "Vertical")] { + for (h, label) in [(true, tl!("Horizontal")), (false, tl!("Vertical"))] { if widgets::radio(ui, label, *horizontal == h, true) { run = Some((OPTIONS_CMD, json!({ "horizontal": h }))); } } - for (key, label, value) in [("bend", "Bend:", *bend), ("h", "H:", *h), ("v", "V:", *v)] { + for (key, label, value) in [("bend", tl!("Bend:"), *bend), ("h", tl!("H:"), *h), ("v", tl!("V:"), *v)] { if let Some(n) = field(ui, key, label, value, "%") { run = Some((OPTIONS_CMD, json!({ key: n.round().clamp(-100.0, 100.0) }))); } } } Bar::Mesh { rows, cols } => { - for (key, label, value) in [("rows", "Rows:", *rows), ("cols", "Columns:", *cols)] { + for (key, label, value) in [("rows", tl!("Rows:"), *rows), ("cols", tl!("Columns:"), *cols)] { if let Some(n) = field(ui, key, label, f64::from(value), "") { run = Some((RESET_MESH, json!({ key: n.round().clamp(1.0, 50.0), "maintainShape": true }))); } @@ -301,11 +301,11 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) { Bar::TopObject => None, }; if let Some(r) = reset - && widgets::flat_button(ui, "Reset", 50.0).on_hover_text("Reset Envelope Shape").clicked() + && widgets::flat_button(ui, tl!("Reset"), 50.0).on_hover_text(tl!("Reset Envelope Shape")).clicked() { run = Some(r); } - if widgets::icon_button(ui, "dc-options", "Envelope Options", false, 24.0).clicked() { + if widgets::icon_button(ui, "dc-options", tl!("Envelope Options"), false, 24.0).clicked() { crate::menus::invoke(app, OPTIONS_CMD, json!({})); } if let Some((cmd, p)) = run diff --git a/crates/ui-egui/src/dialogs/eps_options.rs b/crates/ui-egui/src/dialogs/eps_options.rs index bae6c2636..e2c24e12b 100644 --- a/crates/ui-egui/src/dialogs/eps_options.rs +++ b/crates/ui-egui/src/dialogs/eps_options.rs @@ -18,8 +18,15 @@ use crate::{VectorcraftApp, io, widgets}; pub(super) const KIND: &str = "epsOptions"; -pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "EPS Options".into(), body, confirm, ok: Some("Export"), min_width: 420.0, max_width: Some(460.0), ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("EPS Options").into(), + body, + confirm, + ok: Some("Export"), + min_width: 420.0, + max_width: Some(460.0), + ..DialogSpec::FORM +}; /// Fields that are not remembered between exports (they belong to one export). const PER_EXPORT: [&str; 7] = ["format", "path", "useArtboards", "range", "artboard", "artboards", "selectedOnly"]; @@ -62,15 +69,15 @@ pub fn open(app: &mut VectorcraftApp, params: &Value) { } fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { - widgets::subheader(ui, "Preview"); + widgets::subheader(ui, tl!("Preview")); let previews: Vec<(&str, &str)> = Preview::ALL.iter().map(|p| (p.id(), p.label())).collect(); - choice(ui, d, "previewFormat", "Format:", &previews); + choice(ui, d, "previewFormat", tl!("Format:"), &previews); // Only the colour preview can keep transparency. let color = Preview::from_id(&d.str("previewFormat")) == Some(Preview::TiffColor); let transparent = d.bool("transparentPreview"); ui.horizontal(|ui| { ui.add_space(LABEL + 8.0); - for (label, on) in [("Transparent", true), ("Opaque", false)] { + for (label, on) in [(tl!("Transparent"), true), (tl!("Opaque"), false)] { if widgets::radio(ui, label, color && transparent == on, color) { d.fields.insert("transparentPreview".into(), json!(on)); } @@ -78,22 +85,22 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } }); ui.add_space(8.0); - widgets::subheader(ui, "Transparency"); + widgets::subheader(ui, tl!("Transparency")); let overprints: Vec<(&str, &str)> = Overprint::ALL.iter().map(|o| (o.id(), o.label())).collect(); - choice(ui, d, "overprints", "Overprints:", &overprints); + choice(ui, d, "overprints", tl!("Overprints:"), &overprints); let presets = app.session.flattener_presets(); let names: Vec<(&str, &str)> = presets.iter().map(|p| (p.name.as_str(), p.name.as_str())).collect(); - choice(ui, d, "flattenerPreset", "Preset:", &names); + choice(ui, d, "flattenerPreset", tl!("Preset:"), &names); ui.add_space(8.0); - widgets::subheader(ui, "Fonts"); + widgets::subheader(ui, tl!("Fonts")); // Type is written as glyph outlines, which need no fonts. - widgets::check(ui, "Embed Fonts (for other applications)", true, false); + widgets::check(ui, tl!("Embed Fonts (for other applications)"), true, false); ui.add_space(8.0); - widgets::subheader(ui, "Options"); - check(ui, d, "includeLinkedFiles", "Include Linked Files"); - check(ui, d, "thumbnails", "Include Document Thumbnails"); - check(ui, d, "cmykPostScript", "Include CMYK PostScript in RGB Files"); - check(ui, d, "compatibleGradients", "Compatible Gradient and Gradient Mesh Printing"); + widgets::subheader(ui, tl!("Options")); + check(ui, d, "includeLinkedFiles", tl!("Include Linked Files")); + check(ui, d, "thumbnails", tl!("Include Document Thumbnails")); + check(ui, d, "cmykPostScript", tl!("Include CMYK PostScript in RGB Files")); + check(ui, d, "compatibleGradients", tl!("Compatible Gradient and Gradient Mesh Printing")); ui.add_space(4.0); let levels: Vec<(&str, &str)> = Level::ALL.iter().map(|l| (l.id(), l.label())).collect(); // A level set as a number (`level: 2`) reads as its id. @@ -101,14 +108,14 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let id = n.to_string(); d.fields.insert("level".into(), json!(id)); } - choice(ui, d, "level", "PostScript:", &levels); + choice(ui, d, "level", tl!("PostScript:"), &levels); ui.add_space(8.0); super::form::caption( ui, if Level::from_id(&d.str("level")) == Some(Level::Two) || d.bool("compatibleGradients") { - "Transparency is flattened with the preset. Type is written as outlines; gradients as bands of colour." + tl!("Transparency is flattened with the preset. Type is written as outlines; gradients as bands of colour.") } else { - "Transparency is flattened with the preset. Type is written as outlines; gradients stay smooth." + tl!("Transparency is flattened with the preset. Type is written as outlines; gradients stay smooth.") }, ); false diff --git a/crates/ui-egui/src/dialogs/expand.rs b/crates/ui-egui/src/dialogs/expand.rs index a14204cdb..dba30e3aa 100644 --- a/crates/ui-egui/src/dialogs/expand.rs +++ b/crates/ui-egui/src/dialogs/expand.rs @@ -21,7 +21,7 @@ const CMD: &str = "object.expand"; /// The options and their checkbox labels. const OPTIONS: [(&str, &str); 3] = [("object", "Object"), ("fill", "Fill"), ("stroke", "Stroke")]; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Expand".into(), body, confirm, min_width: 260.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Expand").into(), body, confirm, min_width: 260.0, ..DialogSpec::FORM }; /// Open Expand for the selection, every option on (as `object.expand {}` does). pub fn open(app: &mut VectorcraftApp) -> Result { @@ -38,7 +38,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let set = |d: &mut Dialog, key: &str, v: Value| { d.fields.insert(key.into(), v); }; - widgets::subheader(ui, "Expand"); + widgets::subheader(ui, tl!("Expand")); for (key, label) in OPTIONS { let on = d.bool(key); ui.horizontal(|ui| { @@ -49,26 +49,26 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); } ui.add_space(10.0); - widgets::subheader(ui, "Expand Gradient To"); + widgets::subheader(ui, tl!("Expand Gradient To")); // Only gradient fills that Fill expands care. let enabled = d.bool("__fill") && d.bool("fill"); let objects = d.str("gradient") != "mesh"; ui.horizontal(|ui| { ui.add_space(12.0); - if widgets::radio(ui, "Gradient Mesh", !objects, enabled) { + if widgets::radio(ui, tl!("Gradient Mesh"), !objects, enabled) { set(d, "gradient", json!("mesh")); } }); ui.horizontal(|ui| { ui.add_space(12.0); - if widgets::radio(ui, "Specify:", objects, enabled) { + if widgets::radio(ui, tl!("Specify:"), objects, enabled) { set(d, "gradient", json!("objects")); } ui.add_enabled_ui(enabled && objects, |ui| { if let Some(n) = widgets::plain_field(ui, "expand-steps", d.f64("steps", DEFAULT_STEPS as f64), "", 0, 52.0) { set(d, "steps", json!(n.round().clamp(1.0, MAX_STEPS as f64))); } - widgets::dim_label(ui, "Objects"); + widgets::dim_label(ui, tl!("Objects")); }); }); false diff --git a/crates/ui-egui/src/dialogs/export_as.rs b/crates/ui-egui/src/dialogs/export_as.rs index 9f9b99d22..82e3d255b 100644 --- a/crates/ui-egui/src/dialogs/export_as.rs +++ b/crates/ui-egui/src/dialogs/export_as.rs @@ -15,7 +15,7 @@ use crate::theme::Tokens; use crate::{VectorcraftApp, io, widgets}; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Export As".into(), body, confirm, ok: Some("Export…"), min_width: 380.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Export As").into(), body, confirm, ok: Some("Export…"), min_width: 380.0, ..DialogSpec::FORM }; /// The formats Export As writes (the native format is Save's). static FORMATS: LazyLock> = LazyLock::new(|| fileio::FORMATS.iter().filter(|f| f.write && f.id != "vectorcraft").collect()); @@ -63,7 +63,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let Some(f) = chosen(d) else { return false }; let missing = unavailable(d); egui::Grid::new("export-as").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| { - ui.label(egui::RichText::new("Format:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Format:")).color(t.text_dim)); let shown = missing.map_or(f.label, |(label, _)| label); if let Some(g) = widgets::dropdown_with(ui, "export-as-format", shown, &LABELS, 160.0, |i| i < FORMATS.len()).and_then(|i| FORMATS.get(i)) { d.fields.insert("format".into(), json!(g.id)); @@ -75,13 +75,13 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { form::caption(ui, hint); } ui.add_space(10.0); - form::check(ui, d, "useArtboards", "Use Artboards"); + form::check(ui, d, "useArtboards", tl!("Use Artboards")); let use_artboards = d.bool("useArtboards"); ui.add_enabled_ui(use_artboards, |ui| { ui.horizontal(|ui| { ui.add_space(22.0); let mut all = d.bool("all"); - if ui.radio_value(&mut all, true, "All").changed() | ui.radio_value(&mut all, false, "Range:").changed() { + if ui.radio_value(&mut all, true, tl!("All")).changed() | ui.radio_value(&mut all, false, tl!("Range:")).changed() { d.fields.insert("all".into(), json!(all)); } let mut range = d.str("range"); @@ -93,9 +93,9 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(8.0); let ext = f.extensions[0]; let hint = match (use_artboards, f.id) { - (true, "pdf") => "One page per artboard.".to_string(), - (true, _) => format!("One file per artboard: -.{ext}"), - (false, _) => "The bounds of the visible art.".to_string(), + (true, "pdf") => tl!("One page per artboard.").to_string(), + (true, _) => crate::i18n::fmt(tl!("One file per artboard: -.{ext}"), &[("ext", ext)]), + (false, _) => tl!("The bounds of the visible art.").to_string(), }; ui.label(egui::RichText::new(hint).color(t.text_dim).size(11.5)); false diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index a0beef9fe..92cdc42d1 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -22,7 +22,7 @@ use crate::theme::Tokens; use crate::{VectorcraftApp, io, widgets}; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Export for Screens".into(), + heading: |_| tl!("Export for Screens").into(), body, confirm, ok: Some("Export Artboard"), @@ -256,7 +256,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let tab = usize::from(d.str("tab") == "assets"); - if let Some(i) = widgets::tab_bar(ui, &["Artboards", "Assets"], tab) { + if let Some(i) = widgets::tab_bar(ui, &[tl!("Artboards"), tl!("Assets")], tab) { d.fields.insert("tab".into(), json!(if i == 1 { "assets" } else { "artboards" })); } ui.add_space(8.0); @@ -282,7 +282,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { formats(ui, d); ui.add_space(6.0); ui.horizontal(|ui| { - widgets::dim_label(ui, "Prefix:"); + widgets::dim_label(ui, tl!("Prefix:")); form::text(ui, d, "prefix", 140.0); }); } @@ -303,13 +303,13 @@ fn artboards(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { boards.resize(n, true); let before = boards.clone(); let select = d.str("select"); - widgets::dim_label(ui, "Select:"); + widgets::dim_label(ui, tl!("Select:")); ui.horizontal(|ui| { - if widgets::radio(ui, "All", select == "all", true) { + if widgets::radio(ui, tl!("All"), select == "all", true) { set_select(d, "all"); boards.iter_mut().for_each(|b| *b = true); } - if widgets::radio(ui, "Range:", select == "range", true) { + if widgets::radio(ui, tl!("Range:"), select == "range", true) { set_select(d, "range"); } ui.add_enabled_ui(select == "range", |ui| { @@ -320,20 +320,20 @@ fn artboards(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { } }); }); - if widgets::radio(ui, "Full Document", select == "full", true) { + if widgets::radio(ui, tl!("Full Document"), select == "full", true) { set_select(d, "full"); } ui.add_space(4.0); - form::check(ui, d, "includeBleed", "Include Bleed"); + form::check(ui, d, "includeBleed", tl!("Include Bleed")); ui.add_space(6.0); let typed = boards != before; let checked = boards.clone(); ui.add_enabled_ui(d.str("select") != "full", |ui| { ui.horizontal(|ui| { - if ui.small_button("Select All").clicked() { + if ui.small_button(tl!("Select All")).clicked() { boards.iter_mut().for_each(|b| *b = true); } - if ui.small_button("Clear").clicked() { + if ui.small_button(tl!("Clear")).clicked() { boards.iter_mut().for_each(|b| *b = false); } }); @@ -381,16 +381,16 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let Some(st) = app.session.active() else { return }; if st.doc.assets.is_empty() { ui.add_space(24.0); - widgets::dim_label(ui, "No assets yet. Collect art with Object › Collect for Export, or drag it into the Asset Export panel."); + widgets::dim_label(ui, tl!("No assets yet. Collect art with Object › Collect for Export, or drag it into the Asset Export panel.")); return; } let mut on = checked_assets(app, d); let before = on.clone(); ui.horizontal(|ui| { - if ui.small_button("Select All").clicked() { + if ui.small_button(tl!("Select All")).clicked() { on = st.doc.assets.iter().map(|a| a.id).collect(); } - if ui.small_button("Clear").clicked() { + if ui.small_button(tl!("Clear")).clicked() { on.clear(); } }); @@ -417,23 +417,23 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { /// Export to: the folder (typed or picked), Open Location after Export and Create Sub-folders; the /// web downloads instead. fn destination(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { - widgets::dim_label(ui, "Export to:"); + widgets::dim_label(ui, tl!("Export to:")); if io::is_web(app) { - widgets::dim_label(ui, "Your browser downloads the files (several as one .zip)."); + widgets::dim_label(ui, tl!("Your browser downloads the files (several as one .zip).")); } else { let pick = app.services.pick_folder.is_some(); ui.horizontal(|ui| { form::text(ui, d, "folder", if pick { 300.0 } else { 334.0 }); if pick - && widgets::icon_button(ui, "folder-open", "Choose a folder", false, 26.0).clicked() + && widgets::icon_button(ui, "folder-open", tl!("Choose a folder"), false, 26.0).clicked() && let Some(f) = app.services.pick_folder.as_mut().and_then(|pick| pick()) { d.fields.insert("folder".into(), json!(f)); } }); - form::check(ui, d, "openLocation", "Open Location after Export"); + form::check(ui, d, "openLocation", tl!("Open Location after Export")); } - form::check(ui, d, "subfolders", "Create Sub-folders"); + form::check(ui, d, "subfolders", tl!("Create Sub-folders")); } /// The preset, the Format Settings gear (which sets `__settings`), the format rows (scale, suffix, @@ -442,9 +442,9 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) { let mut rows: Vec = d.fields.get("formats").and_then(Value::as_array).cloned().unwrap_or_default(); let mut edited = false; ui.horizontal(|ui| { - widgets::dim_label(ui, "Formats:"); + widgets::dim_label(ui, tl!("Formats:")); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if widgets::icon_button(ui, "settings", "Format Settings", false, 24.0).clicked() { + if widgets::icon_button(ui, "settings", tl!("Format Settings"), false, 24.0).clicked() { let first = rows.first().and_then(|r| r["format"].as_str()).filter(|f| SETTINGS.iter().any(|(id, _)| id == f)); d.fields.insert("__settings".into(), json!(first.unwrap_or("png"))); } @@ -469,7 +469,7 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) { let k = ((ui.available_width() - 66.0) / 262.0).clamp(0.6, 1.0); let (scale_w, suffix_w, format_w) = (96.0 * k, 70.0 * k, 96.0 * k); egui::Grid::new("efs-formats").num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| { - for head in ["Scale", "Suffix", "Format", ""] { + for head in [tl!("Scale"), tl!("Suffix"), tl!("Format"), ""] { widgets::dim_label(ui, head); } ui.end_row(); @@ -507,7 +507,7 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) { edited = true; } ui.add_enabled_ui(!one, |ui| { - if widgets::icon_button(ui, "x", "Remove", false, 22.0).clicked() { + if widgets::icon_button(ui, "x", tl!("Remove"), false, 22.0).clicked() { remove = Some(i); } }); @@ -518,7 +518,7 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) { rows.remove(i); edited = true; } - if widgets::secondary_button(ui, "+ Add Scale").clicked() { + if widgets::secondary_button(ui, tl!("+ Add Scale")).clicked() { // One more than the largest factor so far. let largest = rows .iter() @@ -541,7 +541,7 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) { /// Format Settings: the options of format `id` that apply to every row of it, then Done. fn format_settings(ui: &mut egui::Ui, d: &mut Dialog, id: &str) { let t = Tokens::get(ui.ctx()); - widgets::subheader(ui, "Format Settings"); + widgets::subheader(ui, tl!("Format Settings")); ui.add_space(6.0); let at = SETTINGS.iter().position(|(f, _)| *f == id).unwrap_or(0); if let Some((f, _)) = widgets::dropdown(ui, "efs-settings-format", SETTINGS_LABELS[at], &SETTINGS_LABELS, 150.0).and_then(|i| SETTINGS.get(i)) { @@ -557,7 +557,7 @@ fn format_settings(ui: &mut egui::Ui, d: &mut Dialog, id: &str) { let labels: Vec<&str> = d.fields.get("__presets").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).collect()).unwrap_or_default(); ui.horizontal(|ui| { - ui.label(egui::RichText::new("PDF Preset:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("PDF Preset:")).color(t.text_dim)); if let Some(p) = widgets::dropdown(ui, "efs-pdf-preset", &s.str("preset"), &labels, 220.0).and_then(|i| labels.get(i)) { s.fields.insert("preset".into(), json!(p)); } @@ -571,7 +571,7 @@ fn format_settings(ui: &mut egui::Ui, d: &mut Dialog, id: &str) { all.insert(id.into(), Value::Object(s.fields)); } ui.add_space(10.0); - if widgets::secondary_button(ui, "Done").clicked() { + if widgets::secondary_button(ui, tl!("Done")).clicked() { d.fields.insert("__settings".into(), json!("")); } } diff --git a/crates/ui-egui/src/dialogs/eyedropper.rs b/crates/ui-egui/src/dialogs/eyedropper.rs index 0b758da5a..1243d6d8a 100644 --- a/crates/ui-egui/src/dialogs/eyedropper.rs +++ b/crates/ui-egui/src/dialogs/eyedropper.rs @@ -15,7 +15,7 @@ use crate::{VectorcraftApp, widgets}; /// The dialog kind of Eyedropper Options. pub const KIND: &str = "eyedropperOptions"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Eyedropper Options".into(), body, confirm, min_width: 460.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Eyedropper Options").into(), body, confirm, min_width: 460.0, ..DialogSpec::FORM }; /// The raster sample sizes: (pixels square, label). const SAMPLE_SIZES: [(u64, &str); 3] = [(1, "Point Sample"), (3, "3 x 3 Average"), (5, "5 x 5 Average")]; @@ -70,7 +70,7 @@ fn set_all(v: &mut Value, on: bool) { /// One tree (`key`: `pickUp` or `apply`) of checkboxes; a branch's box sets everything under it. fn tree(ui: &mut egui::Ui, d: &mut Dialog, key: &str, title: &str) { ui.vertical(|ui| { - ui.label(egui::RichText::new(title).strong()); + ui.label(egui::RichText::new(tl!(title)).strong()); ui.add_space(4.0); let Some(t) = d.fields.get_mut(key) else { return }; for (path, label, depth) in TREE { @@ -90,7 +90,7 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let size = d.fields.get("sampleSize").and_then(Value::as_u64).unwrap_or(1); let current = SAMPLE_SIZES.iter().find(|(n, _)| *n == size).map_or(SAMPLE_SIZES[0].1, |s| s.1); ui.horizontal(|ui| { - widgets::dim_label(ui, "Raster Sample Size:"); + widgets::dim_label(ui, tl!("Raster Sample Size:")); let labels = SAMPLE_SIZES.map(|s| s.1); if let Some(i) = widgets::dropdown(ui, "eyedropper-sample", current, &labels, 150.0) { d.fields.insert("sampleSize".into(), json!(SAMPLE_SIZES[i].0)); @@ -98,9 +98,9 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); ui.add_space(10.0); ui.horizontal_top(|ui| { - tree(ui, d, "pickUp", "Eyedropper Picks Up:"); + tree(ui, d, "pickUp", tl!("Eyedropper Picks Up:")); ui.add_space(24.0); - tree(ui, d, "apply", "Eyedropper Applies:"); + tree(ui, d, "apply", tl!("Eyedropper Applies:")); }); false } diff --git a/crates/ui-egui/src/dialogs/file_info.rs b/crates/ui-egui/src/dialogs/file_info.rs index 18333521b..b0d8185bb 100644 --- a/crates/ui-egui/src/dialogs/file_info.rs +++ b/crates/ui-egui/src/dialogs/file_info.rs @@ -22,7 +22,7 @@ const PENDING: &str = "__keyword"; const FIELD: f32 = 320.0; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "File Info".into(), body, confirm, min_width: 500.0, max_width: Some(500.0), ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("File Info").into(), body, confirm, min_width: 500.0, max_width: Some(500.0), ..DialogSpec::FORM }; /// Open File Info on the active document's. pub fn open(app: &mut VectorcraftApp) -> Result { @@ -62,19 +62,19 @@ fn add_keywords(d: &mut Dialog) { fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); - for (key, label) in [("title", "Document Title:"), ("author", "Author:"), ("authorTitle", "Author Title:")] { + for (key, label) in [("title", tl!("Document Title:")), ("author", tl!("Author:")), ("authorTitle", tl!("Author Title:"))] { widgets::label_row(ui, label, LABEL, |ui| { form::text(ui, d, key, FIELD); }); ui.add_space(4.0); } - top_row(ui, "Description:", |ui| { + top_row(ui, tl!("Description:"), |ui| { form::text_area(ui, d, "description", FIELD, 3); }); ui.add_space(4.0); - widgets::label_row(ui, "Rating:", LABEL, |ui| rating(ui, d)); + widgets::label_row(ui, tl!("Rating:"), LABEL, |ui| rating(ui, d)); ui.add_space(4.0); - top_row(ui, "Keywords:", |ui| { + top_row(ui, tl!("Keywords:"), |ui| { ui.vertical(|ui| { ui.set_max_width(FIELD + 12.0); chips(ui, d); @@ -87,28 +87,28 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { r.request_focus(); } let typed = !d.str(PENDING).trim().is_empty(); - if (widgets::flat_button(ui, "Add", 56.0).clicked() || enter) && typed { + if (widgets::flat_button(ui, tl!("Add"), 56.0).clicked() || enter) && typed { add_keywords(d); } }); - form::caption(ui, "Separate several keywords with commas."); + form::caption(ui, tl!("Separate several keywords with commas.")); }); }); ui.add_space(10.0); - widgets::subheader(ui, "Copyright"); + widgets::subheader(ui, tl!("Copyright")); let statuses: Vec<(&str, &str)> = CopyrightStatus::ALL.iter().map(|c| (c.id(), c.label())).collect(); - choice(ui, d, "copyrightStatus", "Copyright Status:", &statuses); + choice(ui, d, "copyrightStatus", tl!("Copyright Status:"), &statuses); ui.add_space(4.0); - top_row(ui, "Copyright Notice:", |ui| { + top_row(ui, tl!("Copyright Notice:"), |ui| { form::text_area(ui, d, "copyrightNotice", FIELD, 2); }); ui.add_space(4.0); - widgets::label_row(ui, "Copyright Info URL:", LABEL, |ui| { + widgets::label_row(ui, tl!("Copyright Info URL:"), LABEL, |ui| { form::text(ui, d, "copyrightUrl", FIELD); }); ui.add_space(10.0); - widgets::subheader(ui, "Dates"); - for (key, label) in [("created", "Created:"), ("modified", "Modified:")] { + widgets::subheader(ui, tl!("Dates")); + for (key, label) in [("created", tl!("Created:")), ("modified", tl!("Modified:"))] { // ISO 8601 UTC from the engine, shown as "2026-10-05 14:03:00 UTC". let shown = d .fields @@ -127,7 +127,7 @@ fn top_row(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui)) { let t = Tokens::get(ui.ctx()); ui.horizontal_top(|ui| { let (r, _) = ui.allocate_exact_size(egui::vec2(LABEL, 24.0), egui::Sense::hover()); - ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(12.5), t.text); + ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, tl!(label), egui::FontId::proportional(12.5), t.text); add(ui); }); } @@ -148,7 +148,7 @@ fn rating(ui: &mut egui::Ui, d: &mut Dialog) { t.text_dim }; icons::paint(ui, "star", rect.shrink(2.0), tint); - let resp = resp.on_hover_text(if i == 1 { "1 star".to_string() } else { format!("{i} stars") }); + let resp = resp.on_hover_text(crate::i18n::tn(u64::from(i), "{n} star", "{n} stars")); if resp.clicked() { d.fields.insert("rating".into(), json!(if i == current { 0 } else { i })); } @@ -173,7 +173,9 @@ fn chips(ui: &mut egui::Ui, d: &mut Dialog) { ui.painter().rect_stroke(rect, egui::CornerRadius::same(11), egui::Stroke::new(1.0, t.input_border), egui::StrokeKind::Inside); ui.painter().galley(egui::pos2(rect.left() + 9.0, rect.center().y - galley.size().y / 2.0), galley, t.text_strong); let x = egui::Rect::from_center_size(egui::pos2(rect.right() - 11.0, rect.center().y), egui::vec2(14.0, 14.0)); - let resp = ui.interact(x, ui.id().with(("keyword-remove", i)), egui::Sense::click()).on_hover_text(format!("Remove “{k}”")); + let resp = ui + .interact(x, ui.id().with(("keyword-remove", i)), egui::Sense::click()) + .on_hover_text(crate::i18n::fmt(tl!("Remove “{keyword}”"), &[("keyword", k.as_str())])); icons::paint(ui, "x", x.shrink(2.0), if resp.hovered() { t.text_strong } else { t.text_dim }); if resp.clicked() { remove = Some(i); diff --git a/crates/ui-egui/src/dialogs/flatten.rs b/crates/ui-egui/src/dialogs/flatten.rs index 923f13b7f..c30b267d6 100644 --- a/crates/ui-egui/src/dialogs/flatten.rs +++ b/crates/ui-egui/src/dialogs/flatten.rs @@ -31,7 +31,7 @@ const PRESET_NAME: &str = "__presetName"; const CUSTOM: &str = "[Custom]"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Flatten Transparency".into(), body, confirm, preview: true, min_width: 420.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Flatten Transparency").into(), body, confirm, preview: true, min_width: 420.0, ..DialogSpec::FORM }; /// Open Flatten Transparency for the selection with the default preset. pub fn open(app: &mut VectorcraftApp) { @@ -86,12 +86,12 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let current = options(d, saved); let presets = app.session.flattener_presets(); ui.horizontal(|ui| { - widgets::dim_label(ui, "Preset:"); + widgets::dim_label(ui, tl!("Preset:")); if let Some(p) = preset_dropdown(ui, "flatten-preset", &presets, &d.str("preset"), current.as_ref().ok(), 220.0) { put_options(&mut d.fields, &p.options); applied(d, &p.name); } - if widgets::flat_button(ui, "Save Preset…", 100.0).clicked() { + if widgets::flat_button(ui, tl!("Save Preset…"), 100.0).clicked() { let open = !d.bool(SAVING); d.fields.insert(SAVING.into(), json!(open)); d.fields.insert(PRESET_NAME.into(), json!(app.session.new_preset_name())); @@ -118,22 +118,22 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { fn save_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { ui.add_space(6.0); ui.horizontal(|ui| { - widgets::dim_label(ui, "Name:"); + widgets::dim_label(ui, tl!("Name:")); let r = form::text_edit(ui, d, PRESET_NAME, 180.0); // Enter saves the preset instead of flattening. let enter = r.lost_focus() && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Enter)); - if (widgets::flat_button(ui, "Save", 60.0).clicked() || enter) + if (widgets::flat_button(ui, tl!("Save"), 60.0).clicked() || enter) && let Err(e) = save_preset(app, d) { app.status(e); } - if widgets::flat_button(ui, "Cancel", 60.0).clicked() { + if widgets::flat_button(ui, tl!("Cancel"), 60.0).clicked() { d.fields.insert(SAVING.into(), json!(false)); } }); let name = d.str(PRESET_NAME); if app.session.prefs.flattener_presets.iter().any(|p| p.name.eq_ignore_ascii_case(name.trim())) { - widgets::dim_label(ui, "Replaces the saved preset of that name."); + widgets::dim_label(ui, tl!("Replaces the saved preset of that name.")); } } @@ -177,25 +177,25 @@ pub(crate) fn options_editor(ui: &mut egui::Ui, id: &str, o: &mut FlattenOptions let t = Tokens::get(ui.ctx()); let before = o.clone(); let dim = |ui: &mut egui::Ui, s: &str| { - ui.label(egui::RichText::new(s).color(t.text_dim).size(11.5)); + ui.label(egui::RichText::new(tl!(s)).color(t.text_dim).size(11.5)); }; ui.add_enabled_ui(enabled, |ui| { - widgets::dim_label(ui, "Raster/Vector Balance:"); + widgets::dim_label(ui, tl!("Raster/Vector Balance:")); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 6.0; - dim(ui, "Rasters"); + dim(ui, tl!("Rasters")); let rail = (ui.available_width() - FIELD - 56.0).clamp(80.0, 200.0); if let (Some(x), _) = widgets::color_slider(ui, (id, "balance-slider"), (o.balance / 100.0) as f32, rail, &|_| t.input_border) { o.balance = (x as f64 * 100.0).round(); } - dim(ui, "Vectors"); + dim(ui, tl!("Vectors")); if let Some(v) = widgets::plain_field(ui, (id, "balance"), o.balance, "", 0, FIELD - 12.0) { o.balance = v.round().clamp(0.0, 100.0); } }); ui.add_space(4.0); egui::Grid::new((id, "resolutions")).num_columns(2).spacing([8.0, 4.0]).show(ui, |ui| { - for (label, key) in [("Line Art and Text Resolution:", 0), ("Gradient and Mesh Resolution:", 1)] { + for (label, key) in [(tl!("Line Art and Text Resolution:"), 0), (tl!("Gradient and Mesh Resolution:"), 1)] { widgets::dim_label(ui, label); ui.horizontal(|ui| { let v = if key == 0 { &mut o.line_art_ppi } else { &mut o.gradient_ppi }; @@ -209,12 +209,12 @@ pub(crate) fn options_editor(ui: &mut egui::Ui, id: &str, o: &mut FlattenOptions }); ui.add_space(4.0); let switches: [(&str, &mut bool); 6] = [ - ("Convert All Text to Outlines", &mut o.text_to_outlines), - ("Convert All Strokes to Outlines", &mut o.strokes_to_outlines), - ("Clip Complex Regions", &mut o.clip_complex_regions), - ("Anti-alias Rasters", &mut o.anti_alias), - ("Preserve Alpha Transparency", &mut o.preserve_alpha), - ("Preserve Overprints and Spot Colors", &mut o.preserve_overprints), + (tl!("Convert All Text to Outlines"), &mut o.text_to_outlines), + (tl!("Convert All Strokes to Outlines"), &mut o.strokes_to_outlines), + (tl!("Clip Complex Regions"), &mut o.clip_complex_regions), + (tl!("Anti-alias Rasters"), &mut o.anti_alias), + (tl!("Preserve Alpha Transparency"), &mut o.preserve_alpha), + (tl!("Preserve Overprints and Spot Colors"), &mut o.preserve_overprints), ]; for (label, v) in switches { if widgets::check(ui, label, *v, enabled) { diff --git a/crates/ui-egui/src/dialogs/flattener_presets.rs b/crates/ui-egui/src/dialogs/flattener_presets.rs index 7f26852bc..909f3bf7c 100644 --- a/crates/ui-egui/src/dialogs/flattener_presets.rs +++ b/crates/ui-egui/src/dialogs/flattener_presets.rs @@ -24,7 +24,7 @@ pub const KIND: &str = "flattenerPresets"; const SHOWN: &str = "__shown"; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Transparency Flattener Presets".into(), + heading: |_| tl!("Transparency Flattener Presets").into(), body, confirm: |app, _| { app.ui.dialog = None; @@ -67,13 +67,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.horizontal_top(|ui| { ui.vertical(|ui| { ui.set_width(232.0); - widgets::dim_label(ui, "Presets:"); + widgets::dim_label(ui, tl!("Presets:")); widgets::list_box(ui, |ui| { egui::ScrollArea::vertical().id_salt("flattener-presets").min_scrolled_height(250.0).max_height(250.0).show(ui, |ui| { ui.set_width(ui.available_width()); ui.set_min_height(250.0); for (k, p) in presets.iter().enumerate() { - let label = if k < builtins { format!("[{}]", p.name) } else { p.name.clone() }; + let label = if k < builtins { format!("[{}]", tl!(&p.name)) } else { p.name.clone() }; if ui.selectable_label(k == i, label).clicked() { act = Some(Action::Select(p.name.clone())); } @@ -83,16 +83,16 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(6.0); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 4.0; - if widgets::flat_button(ui, "New", 44.0).on_hover_text("A saved copy of the selected preset").clicked() { + if widgets::flat_button(ui, tl!("New"), 44.0).on_hover_text(tl!("A saved copy of the selected preset")).clicked() { act = Some(Action::New); } - if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Delete", 52.0)).inner.clicked() { + if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, tl!("Delete"), 52.0)).inner.clicked() { act = Some(Action::Delete); } - if widgets::flat_button(ui, "Import…", 62.0).clicked() { + if widgets::flat_button(ui, tl!("Import…"), 62.0).clicked() { act = Some(Action::Import); } - if widgets::flat_button(ui, "Export…", 62.0).on_hover_text("Save the selected preset to a file").clicked() { + if widgets::flat_button(ui, tl!("Export…"), 62.0).on_hover_text(tl!("Save the selected preset to a file")).clicked() { act = Some(Action::Export); } }); @@ -100,7 +100,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(18.0); ui.vertical(|ui| { ui.horizontal(|ui| { - widgets::dim_label(ui, "Name:"); + widgets::dim_label(ui, tl!("Name:")); if builtin { ui.label(egui::RichText::new(&preset.name).color(t.text_strong)); } else { @@ -114,7 +114,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } if builtin { ui.add_space(6.0); - ui.label(egui::RichText::new("Built-in presets don't change: New makes a copy you can edit.").color(t.text_dim).size(11.5)); + ui.label(egui::RichText::new(tl!("Built-in presets don't change: New makes a copy you can edit.")).color(t.text_dim).size(11.5)); } }); }); diff --git a/crates/ui-egui/src/dialogs/form.rs b/crates/ui-egui/src/dialogs/form.rs index b77483a29..16a4c8948 100644 --- a/crates/ui-egui/src/dialogs/form.rs +++ b/crates/ui-egui/src/dialogs/form.rs @@ -18,7 +18,7 @@ pub(super) fn field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) { /// [`field`] `width` points wide. pub(super) fn field_w(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, width: f32) { let t = Tokens::get(ui.ctx()); - ui.label(egui::RichText::new(label).color(t.text_dim)); + ui.label(egui::RichText::new(tl!(label)).color(t.text_dim)); text(ui, d, key, width); ui.end_row(); } @@ -26,7 +26,7 @@ pub(super) fn field_w(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, /// A labelled [`length`] field (one grid row). pub(super) fn length_field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, unit: Unit) { let t = Tokens::get(ui.ctx()); - ui.label(egui::RichText::new(label).color(t.text_dim)); + ui.label(egui::RichText::new(tl!(label)).color(t.text_dim)); length(ui, d, key, unit, FIELD_W); ui.end_row(); } @@ -101,7 +101,7 @@ pub(super) fn text_area(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32 /// A checkbox bound to `d.fields[key]`. pub(super) fn check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) { let mut b = d.bool(key); - if ui.checkbox(&mut b, label).changed() { + if ui.checkbox(&mut b, tl!(label)).changed() { d.fields.insert(key.into(), Value::Bool(b)); } } @@ -130,9 +130,9 @@ pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) { continue; } if lengths.contains(&k.as_str()) { - length_field(ui, d, &k, &humanize(&k), unit); + length_field(ui, d, &k, &humanized(&k), unit); } else { - field(ui, d, &k, &humanize(&k)); + field(ui, d, &k, &humanized(&k)); } } }); @@ -153,7 +153,7 @@ pub(super) fn param_fields(ui: &mut egui::Ui, d: &mut Dialog, is_length: &dyn Fn let keys: Vec<(String, Value)> = d.fields.iter().filter(|(k, _)| !k.starts_with("__") && k.as_str() != "preview").map(|(k, v)| (k.clone(), v.clone())).collect(); for (k, v) in keys { - ui.label(egui::RichText::new(humanize(&k)).color(t.text)); + ui.label(egui::RichText::new(humanized(&k)).color(t.text)); if let Some(cur) = crate::widgets::blend_param(&k, &v) { if let Some(m) = crate::widgets::blend_param_dropdown(ui, ("fx-blend", &k), cur) { d.fields.insert(k, m); @@ -214,7 +214,7 @@ pub(super) fn schema_fields(ui: &mut egui::Ui, d: &mut Dialog, specs: &[(String, let mut changed = false; egui::Grid::new("plugin-grid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { for (k, spec) in specs { - ui.label(egui::RichText::new(humanize(k)).color(t.text)); + ui.label(egui::RichText::new(humanized(k)).color(t.text)); let cur = d.fields.get(k).cloned().unwrap_or_else(|| spec.default_value()); let new = match spec { ParamSpec::Number { min, max, .. } => { @@ -272,6 +272,15 @@ pub(super) fn humanize(k: &str) -> String { } } +/// [`humanize`] translated where it is painted: the ":" stays outside the translated text. +fn humanized(k: &str) -> String { + let h = humanize(k); + match h.strip_suffix(':') { + Some(base) => format!("{}:", tl!(base)), + None => tl!(&h).to_string(), + } +} + /// Widths of [`slider`]'s label column and rail. pub(super) const SLIDER_LABEL: f32 = 64.0; pub(super) const SLIDER_WIDTH: f32 = 180.0; @@ -304,7 +313,7 @@ pub(super) fn slider_w( let v = d.f64(key, 0.0).clamp(min, max); let mut new = None; ui.horizontal(|ui| { - ui.add_sized([label_w, 22.0], egui::Label::new(egui::RichText::new(label).color(t.text))); + ui.add_sized([label_w, 22.0], egui::Label::new(egui::RichText::new(tl!(label)).color(t.text))); if let (Some(x), _) = crate::widgets::color_slider(ui, ("dlg-slider", key), ((v - min) / (max - min)) as f32, SLIDER_WIDTH, track) { new = Some((min + x as f64 * (max - min)).round()); } @@ -327,7 +336,7 @@ pub(super) fn preview(app: &mut crate::VectorcraftApp, ui: &mut egui::Ui, d: &mu const LAST: &str = PREVIEWED; ui.add_space(8.0); let mut on = d.bool("preview"); - if crate::widgets::check(ui, "Preview", on, true) { + if crate::widgets::check(ui, tl!("Preview"), on, true) { on = !on; d.fields.insert("preview".into(), json!(on)); } @@ -386,7 +395,7 @@ pub(super) fn bleed(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Un ui.spacing_mut().item_spacing.x = 6.0; for (i, side) in ["Top", "Bottom", "Left", "Right"].into_iter().enumerate() { ui.vertical(|ui| { - ui.label(egui::RichText::new(side).size(11.0).color(t.text_dim)); + ui.label(egui::RichText::new(tl!(side)).size(11.0).color(t.text_dim)); if let Some(v) = crate::widgets::num_field(ui, ("bleed", i), Some(b[i]), unit, width) { changed = Some((i, v.clamp(0.0, vectorcraft_doc::setup::MAX_BLEED))); } @@ -394,7 +403,7 @@ pub(super) fn bleed(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Un } ui.vertical(|ui| { ui.add_space(15.0); - let tip = if linked { "Make the bleed values differ" } else { "Make all bleed settings the same" }; + let tip = if linked { tl!("Make the bleed values differ") } else { tl!("Make all bleed settings the same") }; if crate::widgets::icon_button(ui, if linked { "link" } else { "link-2-off" }, tip, linked, 24.0).clicked() { d.fields.insert("bleedLinked".into(), json!(!linked)); if !linked { @@ -418,5 +427,5 @@ pub(super) fn bleed(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Un /// A small grey caption above a field (New Document's details column). pub(super) fn caption(ui: &mut egui::Ui, text: &str) { let t = Tokens::get(ui.ctx()); - ui.label(egui::RichText::new(text).size(11.0).color(t.text_dim)); + ui.label(egui::RichText::new(tl!(text)).size(11.0).color(t.text_dim)); } diff --git a/crates/ui-egui/src/dialogs/gradient_stop.rs b/crates/ui-egui/src/dialogs/gradient_stop.rs index 319e7a57a..4d102f339 100644 --- a/crates/ui-egui/src/dialogs/gradient_stop.rs +++ b/crates/ui-egui/src/dialogs/gradient_stop.rs @@ -100,7 +100,7 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { egui::Frame::popup(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(10)).show(ui, |ui| { ui.set_width(250.0); ui.horizontal(|ui| { - for (icon, tip, on) in [("palette", "Color", !swatches), ("swatch-book", "Swatches", swatches)] { + for (icon, tip, on) in [("palette", tl!("Color"), !swatches), ("swatch-book", tl!("Swatches"), swatches)] { if widgets::icon_button(ui, icon, tip, on, 24.0).clicked() { tab = Some(if icon == "palette" { "color" } else { "swatches" }); } @@ -115,13 +115,13 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { } ui.add_space(6.0); ui.horizontal(|ui| { - widgets::dim_label(ui, "Opacity:"); + widgets::dim_label(ui, tl!("Opacity:")); if let Some(v) = widgets::plain_field(ui, "stop-pop-op", target.opacity() as f64 * 100.0, "%", 0, 54.0) { apply(app, &target, &json!({ "opacity": v })); } let (label, key, value) = match &target { - Target::Stop(stops, i) => ("Location:", "location", stops[*i].offset), - Target::Point(p, _) => ("Spread:", "spread", p.spread), + Target::Stop(stops, i) => (tl!("Location:"), "location", stops[*i].offset), + Target::Point(p, _) => (tl!("Spread:"), "spread", p.spread), }; widgets::dim_label(ui, label); if let Some(v) = widgets::plain_field(ui, ("stop-pop", key), value as f64 * 100.0, "%", 1, 54.0) { @@ -145,7 +145,7 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { fn swatch_grid(app: &mut VectorcraftApp, ui: &mut egui::Ui, target: &Target) { let colors = swatch_colors(app); if colors.is_empty() { - widgets::dim_label(ui, "The document has no colour swatches."); + widgets::dim_label(ui, tl!("The document has no colour swatches.")); return; } let current = target.color(); diff --git a/crates/ui-egui/src/dialogs/graphic_style_options.rs b/crates/ui-egui/src/dialogs/graphic_style_options.rs index 0691a7d82..48aeeaee2 100644 --- a/crates/ui-egui/src/dialogs/graphic_style_options.rs +++ b/crates/ui-egui/src/dialogs/graphic_style_options.rs @@ -16,7 +16,8 @@ use crate::state::Dialog; /// The dialog kind of Graphic Style Options. pub const KIND: &str = "graphicStyleOptions"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Graphic Style Options".into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Graphic Style Options").into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM }; /// Open Graphic Style Options for style `name`, or (`None`) to name a new style from the selection. pub fn open(app: &mut VectorcraftApp, name: Option<&str>) -> Result { @@ -45,7 +46,7 @@ pub fn open_merge(app: &mut VectorcraftApp, names: Vec) -> Result bool { grid(ui, |ui| { - label(ui, "Style Name:"); + label(ui, tl!("Style Name:")); form::text(ui, d, "name", 190.0); ui.end_row(); }); diff --git a/crates/ui-egui/src/dialogs/halftone.rs b/crates/ui-egui/src/dialogs/halftone.rs index c28cd6b11..02a1364bf 100644 --- a/crates/ui-egui/src/dialogs/halftone.rs +++ b/crates/ui-egui/src/dialogs/halftone.rs @@ -21,7 +21,7 @@ const CMD: &str = "object.vectorHalftone"; /// Width of the label column. const LABEL_W: f32 = 84.0; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Vector Halftone".into(), body, confirm, preview: true, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Vector Halftone").into(), body, confirm, preview: true, ..DialogSpec::FORM }; /// The dialog's fields as it opens: the command's defaults, previewed. pub fn fields() -> Value { @@ -29,18 +29,19 @@ pub fn fields() -> Value { } fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { - let shapes = [("circle", "Circle"), ("ellipse", "Ellipse"), ("square", "Square"), ("diamond", "Diamond"), ("line", "Line")]; - form::choice(ui, d, "shape", "Shape", (LABEL_W, 140.0), &shapes); - form::slider_w(ui, d, ("frequency", "Frequency", LABEL_W), 1.0..=150.0, " lpi", &|x| c32(&Color::gray(1.0 - x))); - form::slider_w(ui, d, ("angle", "Angle", LABEL_W), -90.0..=90.0, "°", &|x| c32(&Color::gray(0.2 + 0.6 * x))); - form::choice(ui, d, "mode", "Screens", (LABEL_W, 140.0), &[("mono", "Mono"), ("cmyk", "CMYK")]); + let shapes = + [("circle", tl!("Circle")), ("ellipse", tl!("Ellipse")), ("square", tl!("Square")), ("diamond", tl!("Diamond")), ("line", tl!("Line"))]; + form::choice(ui, d, "shape", tl!("Shape"), (LABEL_W, 140.0), &shapes); + form::slider_w(ui, d, ("frequency", tl!("Frequency"), LABEL_W), 1.0..=150.0, " lpi", &|x| c32(&Color::gray(1.0 - x))); + form::slider_w(ui, d, ("angle", tl!("Angle"), LABEL_W), -90.0..=90.0, "°", &|x| c32(&Color::gray(0.2 + 0.6 * x))); + form::choice(ui, d, "mode", tl!("Screens"), (LABEL_W, 140.0), &[("mono", tl!("Mono")), ("cmyk", tl!("CMYK"))]); if d.str("mode") != "cmyk" { - widgets::label_row(ui, "Color", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Color"), LABEL_W, |ui| { form::text(ui, d, "color", 120.0); }); } ui.add_space(4.0); - for (key, label) in [("invert", "Invert"), ("clip", "Clip to Art"), ("keepOriginal", "Keep Original")] { + for (key, label) in [("invert", tl!("Invert")), ("clip", tl!("Clip to Art")), ("keepOriginal", tl!("Keep Original"))] { form::check(ui, d, key, label); } let p = form::params(d); diff --git a/crates/ui-egui/src/dialogs/import_pdf.rs b/crates/ui-egui/src/dialogs/import_pdf.rs index e5cc74d4d..34d151cc9 100644 --- a/crates/ui-egui/src/dialogs/import_pdf.rs +++ b/crates/ui-egui/src/dialogs/import_pdf.rs @@ -22,7 +22,7 @@ use crate::{VectorcraftApp, io, widgets}; pub(super) const KIND: &str = "importPdf"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |d| if d.str("mode") == "place" { "Place PDF" } else { "Import PDF" }.into(), body, confirm, ..DialogSpec::FORM }; + DialogSpec { heading: |d| if d.str("mode") == "place" { tl!("Place PDF") } else { tl!("Import PDF") }.into(), body, confirm, ..DialogSpec::FORM }; /// The longest side of the page preview (pt). const PREVIEW: f32 = 200.0; @@ -223,7 +223,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { if d.bool("__locked") { let name = d.str("name"); let file = std::path::Path::new(&name).file_name().map_or(name.clone(), |f| f.to_string_lossy().into_owned()); - ui.label(egui::RichText::new(format!("“{file}” is protected by a password.")).color(t.text)); + ui.label(egui::RichText::new(crate::i18n::fmt(tl!("“{file}” is protected by a password."), &[("file", &file)])).color(t.text)); ui.add_space(8.0); password_row(ui, d); return false; @@ -240,16 +240,16 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { widgets::icon_button_enabled(ui, icon, tip, false, enabled, 22.0).clicked().then_some(to) }; let go = [ - nav(ui, "chevrons-left", "First Page", 1, current > 1), - nav(ui, "chevron-left", "Previous Page", current.saturating_sub(1).max(1), current > 1), + nav(ui, "chevrons-left", tl!("First Page"), 1, current > 1), + nav(ui, "chevron-left", tl!("Previous Page"), current.saturating_sub(1).max(1), current > 1), ]; if let Some(v) = widgets::plain_field(ui, "pdf-page", current as f64, "", 0, 40.0) { current = (v.max(1.0) as usize).min(count.max(1)); } - ui.label(egui::RichText::new(format!("of {count}")).color(t.text_dim)); + ui.label(egui::RichText::new(crate::i18n::fmt(tl!("of {count}"), &[("count", &count.to_string())])).color(t.text_dim)); let go2 = [ - nav(ui, "chevron-right", "Next Page", (current + 1).min(count), current < count), - nav(ui, "chevrons-right", "Last Page", count, current < count), + nav(ui, "chevron-right", tl!("Next Page"), (current + 1).min(count), current < count), + nav(ui, "chevrons-right", tl!("Last Page"), count, current < count), ]; if let Some(to) = go.into_iter().chain(go2).flatten().next() { current = to; @@ -258,7 +258,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); ui.add_space(14.0); ui.vertical(|ui| { - widgets::label_row(ui, "Crop To", LABEL_WIDTH, |ui| { + widgets::label_row(ui, tl!("Crop To"), LABEL_WIDTH, |ui| { if let Some(i) = widgets::dropdown(ui, "pdf-crop", crop(d).label(), CropTo::LABELS, 130.0) && let Some(id) = CropTo::IDS.get(i) { @@ -267,20 +267,20 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); if d.str("mode") != "place" { ui.add_space(10.0); - ui.label(egui::RichText::new("Pages").color(t.text)); + ui.label(egui::RichText::new(tl!("Pages")).color(t.text)); let all = d.bool("allPages"); - if widgets::radio(ui, "All", all, true) { + if widgets::radio(ui, tl!("All"), all, true) { d.fields.insert("allPages".into(), json!(true)); } ui.horizontal(|ui| { - if widgets::radio(ui, "Range:", !all, true) { + if widgets::radio(ui, tl!("Range:"), !all, true) { d.fields.insert("allPages".into(), json!(false)); } if super::form::text_edit(ui, d, "range", 90.0).gained_focus() { d.fields.insert("allPages".into(), json!(false)); } }); - widgets::dim_label(ui, "e.g. 1-3, 5"); + widgets::dim_label(ui, tl!("e.g. 1-3, 5")); } }); }); @@ -289,7 +289,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } fn password_row(ui: &mut egui::Ui, d: &mut Dialog) { - widgets::label_row(ui, "Password", LABEL_WIDTH, |ui| { + widgets::label_row(ui, tl!("Password"), LABEL_WIDTH, |ui| { let mut s = d.str("password"); let r = ui.add(egui::TextEdit::singleline(&mut s).password(true).desired_width(180.0)); if r.changed() { diff --git a/crates/ui-egui/src/dialogs/liquify.rs b/crates/ui-egui/src/dialogs/liquify.rs index 630ae81cb..cc2f841bb 100644 --- a/crates/ui-egui/src/dialogs/liquify.rs +++ b/crates/ui-egui/src/dialogs/liquify.rs @@ -53,7 +53,7 @@ fn kind(d: &Dialog) -> LiquifyKind { } fn heading(d: &Dialog) -> String { - format!("{} Tool Options", kind(d).label()) + crate::i18n::fmt(tl!("{tool} Tool Options"), &[("tool", tl!(kind(d).label()))]) } /// Open the options of Liquify tool `tool` with its current values. @@ -79,58 +79,60 @@ fn slider(ui: &mut egui::Ui, d: &mut Dialog, (key, label): (&str, &str), range: fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let k = kind(d); let unit = app.session.general_unit(); - widgets::subheader(ui, "Global Brush Dimensions"); + widgets::subheader(ui, tl!("Global Brush Dimensions")); ui.add_space(4.0); - for (key, label) in [("width", "Width:"), ("height", "Height:")] { + for (key, label) in [("width", tl!("Width:")), ("height", tl!("Height:"))] { widgets::label_row(ui, label, LABEL_W, |ui| { form::length(ui, d, key, unit, 110.0); }); } - widgets::label_row(ui, "Angle:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Angle:"), LABEL_W, |ui| { if let Some(v) = widgets::plain_field(ui, "lq-angle", d.f64("angle", 0.0), "°", 1, 110.0) { d.fields.insert("angle".into(), json!(v.clamp(-360.0, 360.0))); } }); let pressure = d.bool("usePressure"); - widgets::label_row(ui, "Intensity:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Intensity:"), LABEL_W, |ui| { ui.add_enabled_ui(!pressure, |ui| { if let Some(v) = widgets::plain_field(ui, "lq-intensity", d.f64("intensity", 50.0), "%", 0, 110.0) { d.fields.insert("intensity".into(), json!(v.clamp(1.0, 100.0))); } }); }); - widgets::label_row(ui, "", LABEL_W, |ui| check(ui, d, "usePressure", "Use Pressure Pen")); + widgets::label_row(ui, "", LABEL_W, |ui| check(ui, d, "usePressure", tl!("Use Pressure Pen"))); ui.add_space(10.0); - widgets::subheader(ui, &format!("{} Options", k.label())); + widgets::subheader(ui, &crate::i18n::fmt(tl!("{tool} Options"), &[("tool", tl!(k.label()))])); ui.add_space(4.0); if k == LiquifyKind::Twirl { - slider(ui, d, ("rate", "Twirl Rate:"), -180.0..=180.0, "°"); + slider(ui, d, ("rate", tl!("Twirl Rate:")), -180.0..=180.0, "°"); } if k == LiquifyKind::Wrinkle { - slider(ui, d, ("horizontal", "Horizontal:"), 0.0..=100.0, "%"); - slider(ui, d, ("vertical", "Vertical:"), 0.0..=100.0, "%"); + slider(ui, d, ("horizontal", tl!("Horizontal:")), 0.0..=100.0, "%"); + slider(ui, d, ("vertical", tl!("Vertical:")), 0.0..=100.0, "%"); } if k.has_affects() { - slider(ui, d, ("complexity", "Complexity:"), 0.0..=15.0, ""); + slider(ui, d, ("complexity", tl!("Complexity:")), 0.0..=15.0, ""); } - slider(ui, d, ("detail", "Detail:"), 1.0..=10.0, ""); + slider(ui, d, ("detail", tl!("Detail:")), 1.0..=10.0, ""); if k.simplifies() { - widgets::label_row(ui, "", LABEL_W, |ui| check(ui, d, "simplifyOn", "Simplify")); + widgets::label_row(ui, "", LABEL_W, |ui| check(ui, d, "simplifyOn", tl!("Simplify"))); let on = d.bool("simplifyOn"); - ui.add_enabled_ui(on, |ui| slider(ui, d, ("simplify", "Simplify:"), 1.0..=100.0, "")); + ui.add_enabled_ui(on, |ui| slider(ui, d, ("simplify", tl!("Simplify:")), 1.0..=100.0, "")); } else { ui.add_space(4.0); - widgets::label_row(ui, "Brush Affects:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Brush Affects:"), LABEL_W, |ui| { ui.vertical(|ui| { - for (key, label) in [("affectAnchors", "Anchor Points"), ("affectIn", "In Tangent Handles"), ("affectOut", "Out Tangent Handles")] { + for (key, label) in + [("affectAnchors", tl!("Anchor Points")), ("affectIn", tl!("In Tangent Handles")), ("affectOut", tl!("Out Tangent Handles"))] + { check(ui, d, key, label); } }); }); } ui.add_space(10.0); - check(ui, d, "showBrush", "Show Brush Size"); - let hint = format!("{} with the tool sizes the brush", crate::menus::pretty_shortcut("Alt+Drag")); + check(ui, d, "showBrush", tl!("Show Brush Size")); + let hint = crate::i18n::fmt(tl!("{shortcut} with the tool sizes the brush"), &[("shortcut", crate::menus::pretty_shortcut("Alt+Drag").as_str())]); ui.label(egui::RichText::new(hint).size(11.5).color(Tokens::get(ui.ctx()).text_dim)); false } diff --git a/crates/ui-egui/src/dialogs/missing_links.rs b/crates/ui-egui/src/dialogs/missing_links.rs index c564ab9d2..c436e3be4 100644 --- a/crates/ui-egui/src/dialogs/missing_links.rs +++ b/crates/ui-egui/src/dialogs/missing_links.rs @@ -19,7 +19,7 @@ use crate::{VectorcraftApp, widgets}; pub const KIND: &str = "missingLinks"; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Linked File Not Found".into(), + heading: |_| tl!("Linked File Not Found").into(), body, confirm, ok: Some("Replace…"), @@ -62,8 +62,11 @@ fn ask_update(app: &mut VectorcraftApp, ids: Vec) { app.status(format!("{} linked image(s) changed on disk: Update Links shows the new versions", ids.len())); return; } - let detail = format!("{} linked image(s) changed since this document was saved. Show the new versions?", ids.len()); - super::confirm::ask(app, "Update Modified Links", &detail, "links.update", json!({ "ids": ids })); + let detail = crate::i18n::fmt( + tl!("{count} linked image(s) changed since this document was saved. Show the new versions?"), + &[("count", &ids.len().to_string())], + ); + super::confirm::ask(app, tl!("Update Modified Links"), &detail, "links.update", json!({ "ids": ids })); } fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { @@ -86,11 +89,11 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); }); ui.add_space(8.0); - widgets::dim_label(ui, "Replace it with another file, or ignore it: its images keep showing the preview saved with the document."); + widgets::dim_label(ui, tl!("Replace it with another file, or ignore it: its images keep showing the preview saved with the document.")); if rest > 0 { ui.add_space(10.0); let all = d.bool("applyToAll"); - let label = format!("Apply to All ({} more missing)", rest); + let label = crate::i18n::fmt(tl!("Apply to All ({count} more missing)"), &[("count", &rest.to_string())]); if widgets::check(ui, &label, all, true) { d.fields.insert("applyToAll".into(), json!(!all)); } diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index 468c40ce1..9a9736024 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -173,6 +173,9 @@ macro_rules! registry { } } + /// Every variant, in registry order. + pub const ALL: &[DialogKind] = &[$(Self::$variant,)+]; + fn spec(self) -> &'static DialogSpec { match self { $(Self::$variant => { @@ -265,6 +268,19 @@ registry! { PerspectivePlane: [perspective_plane::KIND] => perspective_plane::SPEC, } +/// The button labels the shared dialog frame can show (OK, discard and the fixed Cancel/Close), +/// so the catalog tests can insist they are translated. +pub fn button_labels() -> Vec<&'static str> { + let mut v = vec!["Cancel", "Close"]; + for spec in std::iter::once(&DialogSpec::FORM).chain(DialogKind::ALL.iter().map(|k| k.spec())) { + v.extend(spec.ok); + v.extend(spec.discard); + } + v.sort_unstable(); + v.dedup(); + v +} + /// The spec for a `Dialog::kind` ([`DialogSpec::FORM`] when unregistered). fn spec(kind: &str) -> &'static DialogSpec { static FALLBACK: DialogSpec = DialogSpec::FORM; @@ -324,7 +340,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let room = (ctx.content_rect().width() - 2.0 * (f32::from(MARGIN) + EDGE_GAP)).max(EDGE_GAP); ui.set_min_width(spec.min_width.min(room)); ui.set_max_width(spec.max_width.map_or(room, |w| w.min(room))); - ui.label(egui::RichText::new(heading.as_str()).font(theme::semibold(16.0)).color(t.text)); + ui.label(egui::RichText::new(tl!(heading.as_str())).font(theme::semibold(16.0)).color(t.text)); ui.add_space(12.0); cancel = (spec.body)(app, ui, &mut d); ui.add_space(16.0); diff --git a/crates/ui-egui/src/dialogs/new_color_group.rs b/crates/ui-egui/src/dialogs/new_color_group.rs index fd4762596..063f84ce5 100644 --- a/crates/ui-egui/src/dialogs/new_color_group.rs +++ b/crates/ui-egui/src/dialogs/new_color_group.rs @@ -15,7 +15,7 @@ use crate::{VectorcraftApp, widgets}; /// The dialog kind of New Color Group. pub const KIND: &str = "newColorGroup"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "New Color Group".into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("New Color Group").into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM }; /// Open New Color Group for the swatches selected in the panel. It starts from the artwork when /// art is selected and no swatches are. @@ -40,20 +40,20 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { d.fields.insert(key.into(), json!(v)); }; grid(ui, |ui| { - label(ui, "Name:"); + label(ui, tl!("Name:")); form::text(ui, d, "name", 190.0); ui.end_row(); - label(ui, "Create From:"); - if widgets::radio(ui, "Selected Swatches", !art, true) { + label(ui, tl!("Create From:")); + if widgets::radio(ui, tl!("Selected Swatches"), !art, true) { set(d, "fromArtwork", false); } ui.end_row(); ui.label(""); - if widgets::radio(ui, "Selected Artwork", art, d.bool("__art")) { + if widgets::radio(ui, tl!("Selected Artwork"), art, d.bool("__art")) { set(d, "fromArtwork", true); } ui.end_row(); - for (key, text) in [("toGlobal", "Convert Process to Global"), ("includeTints", "Include Swatches for Tints")] { + for (key, text) in [("toGlobal", tl!("Convert Process to Global")), ("includeTints", tl!("Include Swatches for Tints"))] { ui.label(""); ui.horizontal(|ui| { ui.add_space(18.0); diff --git a/crates/ui-egui/src/dialogs/new_document.rs b/crates/ui-egui/src/dialogs/new_document.rs index 47150f0ae..8c2afb420 100644 --- a/crates/ui-egui/src/dialogs/new_document.rs +++ b/crates/ui-egui/src/dialogs/new_document.rs @@ -225,16 +225,16 @@ fn presets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let list = names.get(cat).and_then(|c| newdoc::category(&app.session, c)).unwrap_or_default(); ui.add_space(14.0); let heading = if cat == 0 { - "RECENT" + tl!("RECENT") } else if cat == 1 { - "SAVED" + tl!("SAVED") } else { - "BLANK DOCUMENT PRESETS" + tl!("BLANK DOCUMENT PRESETS") }; ui.label(egui::RichText::new(format!("{heading} ({})", list.len())).font(theme::semibold(11.0)).color(t.text_dim)); ui.add_space(10.0); if list.is_empty() { - let hint = if cat == 1 { "Presets you save with the Save Preset button appear here." } else { "Documents you create appear here." }; + let hint = if cat == 1 { tl!("Presets you save with the Save Preset button appear here.") } else { tl!("Documents you create appear here.") }; ui.label(egui::RichText::new(hint).color(t.text_dim)); return; } @@ -271,7 +271,7 @@ pub fn preset_card(ui: &mut egui::Ui, s: &DocSettings, selected: bool) -> egui:: p.rect_filled(page, 0.0, egui::Color32::WHITE); // The name on up to two lines, then the size. let mut job = egui::text::LayoutJob::single_section( - s.name.clone(), + tl!(&s.name).to_string(), egui::TextFormat { font_id: theme::semibold(11.5), color: t.text_strong, ..Default::default() }, ); job.halign = egui::Align::Center; @@ -280,7 +280,7 @@ pub fn preset_card(ui: &mut egui::Ui, s: &DocSettings, selected: bool) -> egui:: let name_h = name.size().y; p.galley(egui::pos2(r.center().x, r.top() + 94.0), name, t.text_strong); p.text(egui::pos2(r.center().x, r.top() + 98.0 + name_h), egui::Align2::CENTER_TOP, s.size_label(), egui::FontId::proportional(11.0), t.text_dim); - resp.on_hover_text(&s.name) + resp.on_hover_text(tl!(&s.name)) } /// The Preset Details column. @@ -288,11 +288,11 @@ fn details(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, b: &mut let t = Tokens::get(ui.ctx()); ui.spacing_mut().item_spacing = egui::vec2(6.0, 4.0); let top = ui.cursor().top(); - ui.label(egui::RichText::new("PRESET DETAILS").font(theme::semibold(11.0)).color(t.text_dim)); + ui.label(egui::RichText::new(tl!("PRESET DETAILS")).font(theme::semibold(11.0)).color(t.text_dim)); ui.add_space(8.0); ui.horizontal(|ui| { form::text(ui, d, "name", DETAILS - 50.0); - if widgets::icon_button(ui, "save", "Save Preset", d.bool(SAVING), 26.0).clicked() { + if widgets::icon_button(ui, "save", tl!("Save Preset"), d.bool(SAVING), 26.0).clicked() { let saving = !d.bool(SAVING); d.fields.insert(SAVING.into(), json!(saving)); if saving && d.str("presetName").is_empty() { @@ -305,7 +305,7 @@ fn details(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, b: &mut } ui.add_space(8.0); let unit = unit(d); - form::caption(ui, "Width"); + form::caption(ui, tl!("Width")); ui.horizontal(|ui| { form::length(ui, d, "width", unit, DETAILS - 124.0); units_dropdown(ui, d, 112.0); @@ -313,48 +313,48 @@ fn details(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, b: &mut ui.add_space(4.0); ui.horizontal_top(|ui| { ui.vertical(|ui| { - form::caption(ui, "Height"); + form::caption(ui, tl!("Height")); form::length(ui, d, "height", unit, 104.0); }); ui.add_space(6.0); ui.vertical(|ui| { - form::caption(ui, "Orientation"); + form::caption(ui, tl!("Orientation")); orientation(ui, d); }); ui.add_space(6.0); ui.vertical(|ui| { - form::caption(ui, "Artboards"); + form::caption(ui, tl!("Artboards")); artboards(ui, d, 76.0); }); }); ui.add_space(8.0); - form::caption(ui, "Bleed"); + form::caption(ui, tl!("Bleed")); form::bleed(ui, d, unit, 52.0); ui.add_space(8.0); - form::caption(ui, "Background Contents"); + form::caption(ui, tl!("Background Contents")); background(ui, d, DETAILS - 34.0); ui.add_space(8.0); advanced(ui, d, 110.0, DETAILS - 116.0); ui.add_space(12.0); - if widgets::flat_button(ui, "More Settings", DETAILS).clicked() { + if widgets::flat_button(ui, tl!("More Settings"), DETAILS).clicked() { d.kind = MORE.into(); } // Close and Create at the bottom right. ui.add_space((HEIGHT - (ui.cursor().top() - top) - 28.0).max(12.0)); ui.allocate_ui_with_layout(egui::vec2(ui.available_width(), 28.0), egui::Layout::right_to_left(egui::Align::Center), |ui| { - b.create = widgets::primary_button(ui, "Create").clicked(); + b.create = widgets::primary_button(ui, tl!("Create")).clicked(); ui.add_space(8.0); - b.close = widgets::secondary_button(ui, "Close").clicked(); + b.close = widgets::secondary_button(ui, tl!("Close")).clicked(); }); } /// Save Preset: the preset's name, Save and Cancel. fn save_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { ui.add_space(4.0); - form::caption(ui, "Preset Name"); + form::caption(ui, tl!("Preset Name")); ui.horizontal(|ui| { let enter = form::text_edit(ui, d, "presetName", DETAILS - 140.0).lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); - if widgets::flat_button(ui, "Save", 54.0).clicked() || enter { + if widgets::flat_button(ui, tl!("Save"), 54.0).clicked() || enter { let mut p: Map = PARAMS.iter().filter_map(|k| Some((k.to_string(), d.fields.get(*k)?.clone()))).collect(); p.insert("name".into(), json!(d.str("presetName"))); p.remove("preset"); @@ -368,7 +368,7 @@ fn save_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { Err(e) => app.status(e), } } - if widgets::flat_button(ui, "Cancel", 60.0).clicked() { + if widgets::flat_button(ui, tl!("Cancel"), 60.0).clicked() { d.fields.insert(SAVING.into(), json!(false)); } }); @@ -387,7 +387,7 @@ fn orientation(ui: &mut egui::Ui, d: &mut Dialog) { let landscape = w > h; ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; - for (is_landscape, tip) in [(false, "Portrait"), (true, "Landscape")] { + for (is_landscape, tip) in [(false, tl!("Portrait")), (true, tl!("Landscape"))] { if widgets::orientation_button(ui, is_landscape, landscape == is_landscape, tip) && landscape != is_landscape { d.fields.insert("width".into(), json!(h)); d.fields.insert("height".into(), json!(w)); @@ -444,7 +444,7 @@ fn advanced(ui: &mut egui::Ui, d: &mut Dialog, label: f32, field: f32) { .horizontal(|ui| { let (r, _) = ui.allocate_exact_size(egui::vec2(14.0, 18.0), egui::Sense::hover()); crate::icons::paint(ui, if open { "chevron-down" } else { "chevron-right" }, r, t.icon); - ui.label(egui::RichText::new("Advanced Options").color(t.text)); + ui.label(egui::RichText::new(tl!("Advanced Options")).color(t.text)); }) .response .interact(egui::Sense::click()); @@ -458,9 +458,11 @@ fn advanced(ui: &mut egui::Ui, d: &mut Dialog, label: f32, field: f32) { let ppi: Vec<(Value, String)> = RASTER_PPI.iter().map(|(v, l)| (json!(v), l.to_string())).collect(); let previews: Vec<(Value, String)> = PreviewMode::ALL.iter().map(|m| (json!(m.id()), form::humanize(m.id()).trim_end_matches(':').to_string())).collect(); - for (key, name, options) in - [("colorMode", "Color Mode", &colors), ("rasterEffectsPpi", "Raster Effects", &ppi), ("previewMode", "Preview Mode", &previews)] - { + for (key, name, options) in [ + ("colorMode", tl!("Color Mode"), &colors), + ("rasterEffectsPpi", tl!("Raster Effects"), &ppi), + ("previewMode", tl!("Preview Mode"), &previews), + ] { widgets::label_row(ui, name, label, |ui| choice(ui, d, key, options, field)); } } @@ -475,15 +477,15 @@ fn show_more(app: &mut VectorcraftApp, ctx: &egui::Context) { const L: f32 = 150.0; window(ctx, MORE, 22, |ui| { ui.set_width(560.0); - ui.label(egui::RichText::new("More Settings").font(theme::semibold(16.0)).color(t.text)); + ui.label(egui::RichText::new(tl!("More Settings")).font(theme::semibold(16.0)).color(t.text)); ui.add_space(12.0); - widgets::label_row(ui, "Name:", L, |ui| { + widgets::label_row(ui, tl!("Name:"), L, |ui| { form::text(ui, &mut d, "name", 300.0); }); // Profile: the preset categories (the tabs); Size: the profile's presets. let profiles: Vec<&str> = newdoc::CATEGORIES.iter().map(|c| c.0).collect(); let profile = profiles.iter().find(|p| p.eq_ignore_ascii_case(&d.str("category"))).copied().unwrap_or("[Custom]"); - widgets::label_row(ui, "Profile:", L, |ui| { + widgets::label_row(ui, tl!("Profile:"), L, |ui| { if let Some(p) = widgets::dropdown(ui, "newdoc-profile", profile, &profiles, 220.0).and_then(|i| profiles.get(i)) { d.fields.insert("category".into(), json!(p)); if let Some(first) = newdoc::category(&app.session, p).and_then(|v| v.into_iter().next()) { @@ -495,7 +497,7 @@ fn show_more(app: &mut VectorcraftApp, ctx: &egui::Context) { let (lay, cols, spacing, rtl) = layout(&d); let n = d.f64("artboards", 1.0); let several = n > 1.0; - widgets::label_row(ui, "Number of Artboards:", L, |ui| { + widgets::label_row(ui, tl!("Number of Artboards:"), L, |ui| { artboards(ui, &mut d, 76.0); ui.add_space(10.0); for (l, icon) in [ @@ -509,17 +511,17 @@ fn show_more(app: &mut VectorcraftApp, ctx: &egui::Context) { } } ui.add_space(6.0); - if widgets::icon_button_enabled(ui, "chevrons-left", "Change to Right-to-Left Layout", rtl, several, 26.0).clicked() { + if widgets::icon_button_enabled(ui, "chevrons-left", tl!("Change to Right-to-Left Layout"), rtl, several, 26.0).clicked() { set_layout(&mut d, "rightToLeft", json!(!rtl)); } }); ui.add_enabled_ui(several, |ui| { - widgets::label_row(ui, "Spacing:", L, |ui| { + widgets::label_row(ui, tl!("Spacing:"), L, |ui| { if let Some(v) = widgets::num_field(ui, "newdoc-spacing", Some(spacing), unit(&d), 100.0) { set_layout(&mut d, "spacing", json!(v.max(0.0))); } ui.add_space(20.0); - ui.label(egui::RichText::new("Columns:").color(t.text)); + ui.label(egui::RichText::new(tl!("Columns:")).color(t.text)); let grid = matches!(lay, ArtboardLayout::GridByRow | ArtboardLayout::GridByColumn); ui.add_enabled_ui(grid, |ui| { if let Some(v) = widgets::spin_plain(ui, "newdoc-columns", cols as f64, "", 0, 76.0, 1.0, 1.0, &[]) { @@ -532,39 +534,39 @@ fn show_more(app: &mut VectorcraftApp, ctx: &egui::Context) { let sizes = newdoc::category(&app.session, profile).unwrap_or_default(); let size_names: Vec<&str> = sizes.iter().map(|s| s.name.as_str()).collect(); let preset = d.str("preset"); - widgets::label_row(ui, "Size:", L, |ui| { + widgets::label_row(ui, tl!("Size:"), L, |ui| { let shown = if preset.is_empty() { "Custom" } else { preset.as_str() }; if let Some(s) = widgets::dropdown(ui, "newdoc-size", shown, &size_names, 220.0).and_then(|i| sizes.get(i)) { apply(&mut d, s); } }); let unit = unit(&d); - widgets::label_row(ui, "Width:", L, |ui| { + widgets::label_row(ui, tl!("Width:"), L, |ui| { form::length(ui, &mut d, "width", unit, 120.0); ui.add_space(20.0); - ui.label(egui::RichText::new("Units:").color(t.text)); + ui.label(egui::RichText::new(tl!("Units:")).color(t.text)); units_dropdown(ui, &mut d, 120.0); }); - widgets::label_row(ui, "Height:", L, |ui| { + widgets::label_row(ui, tl!("Height:"), L, |ui| { form::length(ui, &mut d, "height", unit, 120.0); ui.add_space(20.0); - ui.label(egui::RichText::new("Orientation:").color(t.text)); + ui.label(egui::RichText::new(tl!("Orientation:")).color(t.text)); orientation(ui, &mut d); }); ui.add_space(4.0); - widgets::label_row(ui, "Bleed:", L, |ui| form::bleed(ui, &mut d, unit, 64.0)); + widgets::label_row(ui, tl!("Bleed:"), L, |ui| form::bleed(ui, &mut d, unit, 64.0)); ui.add_space(4.0); - widgets::label_row(ui, "Background Contents:", L, |ui| background(ui, &mut d, 220.0)); + widgets::label_row(ui, tl!("Background Contents:"), L, |ui| background(ui, &mut d, 220.0)); ui.add_space(8.0); advanced(ui, &mut d, L, 220.0); ui.add_space(16.0); ui.horizontal(|ui| { - templates = widgets::secondary_button(ui, "Templates…").on_hover_text("New from Template").clicked(); + templates = widgets::secondary_button(ui, tl!("Templates…")).on_hover_text(tl!("New from Template")).clicked(); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - b.create = widgets::primary_button(ui, "Create Document").clicked(); + b.create = widgets::primary_button(ui, tl!("Create Document")).clicked(); ui.add_space(8.0); // Cancel goes back to New Document. - if widgets::secondary_button(ui, "Cancel").clicked() { + if widgets::secondary_button(ui, tl!("Cancel")).clicked() { d.kind = KIND.into(); } }); diff --git a/crates/ui-egui/src/dialogs/new_swatch.rs b/crates/ui-egui/src/dialogs/new_swatch.rs index 3af0608ec..bbca73333 100644 --- a/crates/ui-egui/src/dialogs/new_swatch.rs +++ b/crates/ui-egui/src/dialogs/new_swatch.rs @@ -19,7 +19,7 @@ use crate::state::Dialog; /// The dialog kind of New Swatch. pub const KIND: &str = "newSwatch"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "New Swatch".into(), body, confirm, min_width: 340.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("New Swatch").into(), body, confirm, min_width: 340.0, ..DialogSpec::FORM }; /// Open New Swatch for the active paint: a spot colour with `spot`, going into colour `group`. pub fn open(app: &mut VectorcraftApp, spot: bool, group: Option<&str>) -> Result { diff --git a/crates/ui-egui/src/dialogs/office_export.rs b/crates/ui-egui/src/dialogs/office_export.rs index cd8e16780..27dd40560 100644 --- a/crates/ui-egui/src/dialogs/office_export.rs +++ b/crates/ui-egui/src/dialogs/office_export.rs @@ -12,7 +12,7 @@ use crate::{VectorcraftApp, io, widgets}; pub(crate) const KIND: &str = "saveForOffice"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Save for Office Documents".into(), body, confirm, ok: Some("Save…"), min_width: 340.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Save for Office Documents").into(), body, confirm, ok: Some("Save…"), min_width: 340.0, ..DialogSpec::FORM }; /// Open the dialog (150 ppi on white, the first artboard). pub fn open(app: &mut VectorcraftApp) -> Result { @@ -28,7 +28,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let ppi = d.f64("ppi", RESOLUTIONS[1]); let board = (d.f64("artboard", 0.0).max(0.0) as usize).min(doc.artboards.len().saturating_sub(1)); egui::Grid::new("office-export").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| { - label(ui, "Resolution:"); + label(ui, tl!("Resolution:")); let at = RESOLUTIONS.iter().position(|r| *r == ppi).unwrap_or(1); if let Some(i) = widgets::dropdown(ui, "office-ppi", RESOLUTION_LABELS[at], &RESOLUTION_LABELS[..RESOLUTIONS.len()], 160.0) && let Some(r) = RESOLUTIONS.get(i) @@ -37,7 +37,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } ui.end_row(); if doc.artboards.len() > 1 { - label(ui, "Artboard:"); + label(ui, tl!("Artboard:")); let names: Vec<&str> = doc.artboards.iter().map(|a| a.name.as_str()).collect(); if let Some(i) = widgets::dropdown(ui, "office-artboard", names.get(board).copied().unwrap_or_default(), &names, 160.0) { d.fields.insert("artboard".into(), json!(i)); @@ -45,14 +45,14 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.end_row(); } if let Some(a) = doc.artboards.get(board) { - label(ui, "Size:"); + label(ui, tl!("Size:")); let (w, h) = vectorcraft_render::region_pixels(a.rect, ppi / 72.0); ui.label(egui::RichText::new(format!("{w} × {h} px")).color(t.text)); ui.end_row(); } }); ui.add_space(8.0); - form::check(ui, d, "transparent", "Transparent Background"); + form::check(ui, d, "transparent", tl!("Transparent Background")); false } diff --git a/crates/ui-egui/src/dialogs/package.rs b/crates/ui-egui/src/dialogs/package.rs index ba9d846bc..2859a2ebd 100644 --- a/crates/ui-egui/src/dialogs/package.rs +++ b/crates/ui-egui/src/dialogs/package.rs @@ -26,8 +26,15 @@ const OPTIONS: [(&str, &str); 5] = [ ("report", "Create Report"), ]; -pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Package".into(), body, confirm, ok: Some("Package"), min_width: 440.0, max_width: Some(460.0), ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Package").into(), + body, + confirm, + ok: Some("Package"), + min_width: 440.0, + max_width: Some(460.0), + ..DialogSpec::FORM +}; /// Is there no file system to write a folder to (the web downloads a zip)? fn downloads(app: &VectorcraftApp) -> bool { @@ -38,7 +45,13 @@ fn downloads(app: &VectorcraftApp) -> bool { pub fn open(app: &mut VectorcraftApp) -> Result { let st = app.session.active().ok_or("no document")?; let Some(path) = st.path.clone() else { - ask(app, "Save the document first?", "Package collects a saved document with its linked files and fonts.", "file.saveAs", json!({})); + ask( + app, + tl!("Save the document first?"), + tl!("Package collects a saved document with its linked files and fonts."), + "file.saveAs", + json!({}), + ); return Ok(Value::Null); }; let p = std::path::Path::new(&path); @@ -55,10 +68,10 @@ pub fn open(app: &mut VectorcraftApp) -> Result { fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); if !downloads(app) { - widgets::label_row(ui, "Location:", LABEL, |ui| { + widgets::label_row(ui, tl!("Location:"), LABEL, |ui| { form::text(ui, d, "folder", 230.0); if let Some(pick) = app.services.pick_folder.as_mut() - && ui.button("Choose…").clicked() + && ui.button(tl!("Choose…")).clicked() && let Some(f) = pick() { d.fields.insert("folder".into(), json!(f)); @@ -66,11 +79,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); ui.add_space(4.0); } - widgets::label_row(ui, "Folder Name:", LABEL, |ui| { + widgets::label_row(ui, tl!("Folder Name:"), LABEL, |ui| { form::text(ui, d, "name", 230.0); }); ui.add_space(10.0); - widgets::subheader(ui, "Options"); + widgets::subheader(ui, tl!("Options")); for (k, label) in OPTIONS { // The link options apply only when the links are copied. let enabled = !matches!(k, "linksFolder" | "relink") || d.bool("copyLinks"); @@ -80,7 +93,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } } ui.add_space(6.0); - ui.label(egui::RichText::new("Fonts whose licence doesn't allow embedding are not copied.").size(11.5).color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Fonts whose licence doesn't allow embedding are not copied.")).size(11.5).color(t.text_dim)); false } @@ -110,8 +123,16 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { } let folder = r["folder"].as_str().unwrap_or_default().to_string(); app.status(format!("Packaged {n} file(s) in {folder}{note}")); - let detail = format!("{n} file(s) in {folder}{note}. Show the package folder?"); - ask(app, "Package created", &detail, "file.showPackage", json!({ "folder": folder })); + let shown_note = if missing > 0 { + format!(" ({})", crate::i18n::fmt(tl!("{count} linked file(s) not found"), &[("count", &missing.to_string())])) + } else { + String::new() + }; + let detail = crate::i18n::fmt( + tl!("{count} file(s) in {folder}{note}. Show the package folder?"), + &[("count", &n.to_string()), ("folder", &folder), ("note", &shown_note)], + ); + ask(app, tl!("Package created"), &detail, "file.showPackage", json!({ "folder": folder })); Ok(r) } diff --git a/crates/ui-egui/src/dialogs/path_ops.rs b/crates/ui-egui/src/dialogs/path_ops.rs index 24b600f51..01f9d3cbc 100644 --- a/crates/ui-egui/src/dialogs/path_ops.rs +++ b/crates/ui-egui/src/dialogs/path_ops.rs @@ -10,10 +10,10 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(&d.kind).int fn title(kind: &str) -> &'static str { match kind { - "average" => "Average", - "offsetPath" => "Offset Path", - "simplify" => "Simplify", - _ => "Split Into Grid", + "average" => tl!("Average"), + "offsetPath" => tl!("Offset Path"), + "simplify" => tl!("Simplify"), + _ => tl!("Split Into Grid"), } } diff --git a/crates/ui-egui/src/dialogs/pdf_presets.rs b/crates/ui-egui/src/dialogs/pdf_presets.rs index 06151bdd7..0d63fdd11 100644 --- a/crates/ui-egui/src/dialogs/pdf_presets.rs +++ b/crates/ui-egui/src/dialogs/pdf_presets.rs @@ -20,7 +20,7 @@ use crate::{VectorcraftApp, widgets}; pub const KIND: &str = "pdfPresets"; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "PDF Presets".into(), + heading: |_| tl!("PDF Presets").into(), body, confirm: |app, _| { app.ui.dialog = None; @@ -57,13 +57,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.horizontal_top(|ui| { ui.vertical(|ui| { ui.set_width(300.0); - widgets::dim_label(ui, "Presets:"); + widgets::dim_label(ui, tl!("Presets:")); widgets::list_box(ui, |ui| { egui::ScrollArea::vertical().id_salt("pdf-presets").min_scrolled_height(260.0).max_height(260.0).show(ui, |ui| { ui.set_width(ui.available_width()); ui.set_min_height(260.0); for (k, p) in presets.iter().enumerate() { - let label = if k < builtins { format!("[{}]", p.name) } else { p.name.clone() }; + let label = if k < builtins { format!("[{}]", tl!(&p.name)) } else { p.name.clone() }; if ui.selectable_label(k == i, label).clicked() { act = Some(Action::Select(p.name.clone())); } @@ -73,19 +73,19 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(6.0); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 4.0; - if widgets::flat_button(ui, "New…", 48.0).on_hover_text("A new preset starting from the selected one").clicked() { + if widgets::flat_button(ui, tl!("New…"), 48.0).on_hover_text(tl!("A new preset starting from the selected one")).clicked() { act = Some(Action::New); } - if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Edit…", 48.0)).inner.clicked() { + if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, tl!("Edit…"), 48.0)).inner.clicked() { act = Some(Action::Edit); } - if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Delete", 52.0)).inner.clicked() { + if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, tl!("Delete"), 52.0)).inner.clicked() { act = Some(Action::Delete); } - if widgets::flat_button(ui, "Import…", 64.0).clicked() { + if widgets::flat_button(ui, tl!("Import…"), 64.0).clicked() { act = Some(Action::Import); } - if widgets::flat_button(ui, "Export…", 64.0).on_hover_text("Save the selected preset to a file").clicked() { + if widgets::flat_button(ui, tl!("Export…"), 64.0).on_hover_text(tl!("Save the selected preset to a file")).clicked() { act = Some(Action::Export); } }); @@ -113,14 +113,14 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { /// What sets the preset apart: its settings that differ from the app default, and the notes on /// what Save PDF can't honour or apply yet. fn details(ui: &mut egui::Ui, t: &Tokens, preset: &vectorcraft_pdf::PdfPreset, builtin: bool) { - let dim = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(text).color(t.text_dim).size(11.5)); - widgets::dim_label(ui, &format!("Differences from {}:", pdf::DEFAULT_PRESET)); + let dim = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(tl!(text)).color(t.text_dim).size(11.5)); + widgets::dim_label(ui, &crate::i18n::fmt(tl!("Differences from {preset}:"), &[("preset", pdf::DEFAULT_PRESET)])); let base = vectorcraft_pdf::builtin_preset(pdf::DEFAULT_PRESET).map(|p| p.settings).unwrap_or_default(); let mut changed = pdf::changes(&preset.settings, &base); changed.sort_by_key(|c| section_of(c["option"].as_str().unwrap_or_default())); egui::ScrollArea::vertical().id_salt("pdf-preset-details").max_height(180.0).show(ui, |ui| { if changed.is_empty() { - dim(ui, "None."); + dim(ui, tl!("None.")); } option_rows(ui, &changed, option_label); }); @@ -133,7 +133,7 @@ fn details(ui: &mut egui::Ui, t: &Tokens, preset: &vectorcraft_pdf::PdfPreset, b } if builtin { ui.add_space(6.0); - dim(ui, "Built-in presets are read-only: New… starts an editable copy."); + dim(ui, tl!("Built-in presets are read-only: New… starts an editable copy.")); } } diff --git a/crates/ui-egui/src/dialogs/perspective_grid.rs b/crates/ui-egui/src/dialogs/perspective_grid.rs index 92123945c..b6eb5ff6a 100644 --- a/crates/ui-egui/src/dialogs/perspective_grid.rs +++ b/crates/ui-egui/src/dialogs/perspective_grid.rs @@ -61,9 +61,9 @@ const TYPES: [&str; 3] = ["One Point Perspective", "Two Point Perspective", "Thr pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| { match d.str(MODE).as_str() { - "save" => "Save Grid as Preset", - "edit" => "Perspective Grid Preset", - _ => "Define Perspective Grid", + "save" => tl!("Save Grid as Preset"), + "edit" => tl!("Perspective Grid Preset"), + _ => tl!("Define Perspective Grid"), } .into() }, @@ -204,7 +204,7 @@ fn color(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) { /// Preset…, else the preset's name. fn name_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { if !d.str(MODE).is_empty() { - widgets::label_row(ui, "Name:", LABEL, |ui| { + widgets::label_row(ui, tl!("Name:"), LABEL, |ui| { form::text(ui, d, "name", 200.0); }); return; @@ -219,7 +219,7 @@ fn name_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { } let shown = current.map_or(CUSTOM, |p| p.name.as_str()); let names: Vec<&str> = std::iter::once(CUSTOM).chain(presets.iter().map(|p| p.name.as_str())).collect(); - widgets::label_row(ui, "Preset:", LABEL, |ui| { + widgets::label_row(ui, tl!("Preset:"), LABEL, |ui| { if let Some(p) = widgets::dropdown(ui, "persp-preset", shown, &names, 200.0).and_then(|i| i.checked_sub(1)).and_then(|i| presets.get(i)) { let mut fields = json!(p); fields[SHOWN] = fields["units"].clone(); @@ -227,7 +227,7 @@ fn name_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { d.fields.extend(o.clone()); } } - if widgets::flat_button(ui, "Save Preset…", 96.0).on_hover_text("Save these settings as a preset").clicked() { + if widgets::flat_button(ui, tl!("Save Preset…"), 96.0).on_hover_text(tl!("Save these settings as a preset")).clicked() { d.fields.insert(MODE.into(), json!("save")); d.fields.insert(FROM.into(), json!("define")); d.fields.insert("name".into(), json!(app.session.new_perspective_preset_name())); @@ -239,16 +239,16 @@ fn name_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); name_row(app, ui, d); - widgets::subheader(ui, "Perspective Grid Settings"); + widgets::subheader(ui, tl!("Perspective Grid Settings")); ui.add_space(6.0); let k = kind(d); - widgets::label_row(ui, "Type:", LABEL, |ui| { + widgets::label_row(ui, tl!("Type:"), LABEL, |ui| { if let Some(i) = widgets::dropdown(ui, "persp-type", TYPES[usize::from(k - 1)], &TYPES, 200.0) { d.fields.insert("kind".into(), json!(i + 1)); } }); let units: Vec<(&str, &str)> = Unit::ALL.iter().filter(|u| **u != Unit::FeetInches).map(|u| (u.key(), u.label())).collect(); - form::choice(ui, d, "units", "Units:", (LABEL, 200.0), &units); + form::choice(ui, d, "units", tl!("Units:"), (LABEL, 200.0), &units); // A new unit (the menu or `ui.dialog.set`) keeps the lengths: they are converted. let unit = unit(d); let before = Unit::named(&d.str(SHOWN)).unwrap_or(unit); @@ -269,7 +269,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let named = SCALES.iter().find(|(_, s)| *s == sc).map(|(l, _)| *l); let mut labels: Vec<&str> = SCALES.iter().map(|(l, _)| *l).collect(); labels.push("Custom"); - widgets::label_row(ui, "Scale:", LABEL, |ui| { + widgets::label_row(ui, tl!("Scale:"), LABEL, |ui| { if let Some(i) = widgets::dropdown(ui, "persp-scale", named.unwrap_or("Custom"), &labels, 200.0) { // Custom starts from the scale shown. let s = SCALES.get(i).map_or(sc, |(_, s)| *s); @@ -278,7 +278,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); if named.is_none() { widgets::label_row(ui, "", LABEL, |ui| { - for (i, label) in ["Artboard:", "Real World:"].into_iter().enumerate() { + for (i, label) in [tl!("Artboard:"), tl!("Real World:")].into_iter().enumerate() { ui.label(egui::RichText::new(label).color(t.text_dim)); if let Some(v) = widgets::plain_field(ui, ("persp-scale", i), sc[i], "", 3, 56.0).filter(|v| *v > 0.0 && v.is_finite()) { let mut s = sc; @@ -288,41 +288,44 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } }); } - widgets::label_row(ui, "Gridline every:", LABEL, |ui| { + widgets::label_row(ui, tl!("Gridline every:"), LABEL, |ui| { length(ui, d, "gridline", unit, FIELD); }); if sc[0] != sc[1] { let real = unit.number(unit.to_pt(sc[1] / sc[0])); - let note = format!("1 {s} on the artboard stands for {real} {s} in the scene.", s = unit.suffix()); + let note = crate::i18n::fmt( + tl!("1 {unit} on the artboard stands for {real} {unit} in the scene."), + &[("unit", unit.suffix()), ("real", &real.to_string())], + ); widgets::label_row(ui, "", LABEL, |ui| { ui.label(egui::RichText::new(note).size(11.5).color(t.text_dim)); }); } ui.add_space(4.0); - widgets::label_row(ui, "Viewing Angle:", LABEL, |ui| { + widgets::label_row(ui, tl!("Viewing Angle:"), LABEL, |ui| { let a = d.f64("angle", 45.0); if let Some(v) = ui.add_enabled_ui(k != 1, |ui| widgets::plain_field(ui, "persp-angle", a, "°", 2, FIELD)).inner { d.fields.insert("angle".into(), json!(v)); } }); - widgets::label_row(ui, "Viewing Distance:", LABEL, |ui| { + widgets::label_row(ui, tl!("Viewing Distance:"), LABEL, |ui| { length(ui, d, "distance", unit, FIELD); }); - widgets::label_row(ui, "Horizon Height:", LABEL, |ui| { + widgets::label_row(ui, tl!("Horizon Height:"), LABEL, |ui| { length(ui, d, "horizonHeight", unit, FIELD); }); - widgets::label_row(ui, "Third Vanishing Point:", LABEL, |ui| { + widgets::label_row(ui, tl!("Third Vanishing Point:"), LABEL, |ui| { third_vp(ui, d, 0, unit, k == 3); third_vp(ui, d, 1, unit, k == 3); }); ui.add_space(12.0); - widgets::subheader(ui, "Grid Color & Opacity"); + widgets::subheader(ui, tl!("Grid Color & Opacity")); ui.add_space(6.0); - color(ui, d, "leftColor", "Left Grid:"); - color(ui, d, "rightColor", "Right Grid:"); - color(ui, d, "groundColor", "Horizontal Grid:"); + color(ui, d, "leftColor", tl!("Left Grid:")); + color(ui, d, "rightColor", tl!("Right Grid:")); + color(ui, d, "groundColor", tl!("Horizontal Grid:")); // Opacity: a slider and its field, in the label column the rows above use. - widgets::label_row(ui, "Opacity:", LABEL, |ui| { + widgets::label_row(ui, tl!("Opacity:"), LABEL, |ui| { let v = d.f64("opacity", 50.0).clamp(0.0, 100.0); let track = |x: f32| egui::Color32::from_gray((60.0 + x * 160.0) as u8); let mut new = diff --git a/crates/ui-egui/src/dialogs/perspective_options.rs b/crates/ui-egui/src/dialogs/perspective_options.rs index 21936c441..91e74de0e 100644 --- a/crates/ui-egui/src/dialogs/perspective_options.rs +++ b/crates/ui-egui/src/dialogs/perspective_options.rs @@ -15,7 +15,7 @@ use crate::{VectorcraftApp, widgets}; pub const KIND: &str = "perspectiveGridOptions"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Perspective Grid Options".into(), body, confirm, min_width: 340.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Perspective Grid Options").into(), body, confirm, min_width: 340.0, ..DialogSpec::FORM }; /// Open the dialog with the current options. pub fn open(app: &mut VectorcraftApp) { @@ -32,11 +32,11 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let on = d.bool("show"); - if widgets::check(ui, "Show Active Plane Widget", on, true) { + if widgets::check(ui, tl!("Show Active Plane Widget"), on, true) { d.fields.insert("show".into(), json!(!on)); } ui.add_space(8.0); let corners: Vec<(&str, &str)> = WidgetCorner::ALL.iter().map(|c| (c.id(), c.label())).collect(); - ui.add_enabled_ui(on, |ui| form::choice(ui, d, "position", "Widget Position:", (110.0, 160.0), &corners)); + ui.add_enabled_ui(on, |ui| form::choice(ui, d, "position", tl!("Widget Position:"), (110.0, 160.0), &corners)); false } diff --git a/crates/ui-egui/src/dialogs/perspective_plane.rs b/crates/ui-egui/src/dialogs/perspective_plane.rs index 4ac217fe0..7e1e1d837 100644 --- a/crates/ui-egui/src/dialogs/perspective_plane.rs +++ b/crates/ui-egui/src/dialogs/perspective_plane.rs @@ -23,9 +23,9 @@ const OBJECTS: [(&str, &str); 3] = [("none", "Do Not Move"), ("move", "Move All fn heading(d: &Dialog) -> String { match Plane::parse(&d.str("plane")) { - Some(Plane::Right) => "Right Plane", - Some(Plane::Ground) => "Horizontal Plane", - _ => "Left Plane", + Some(Plane::Right) => tl!("Right Plane"), + Some(Plane::Ground) => tl!("Horizontal Plane"), + _ => tl!("Left Plane"), } .into() } @@ -49,7 +49,7 @@ pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result { fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let unit = app.session.general_unit(); grid(ui, |ui| { - label(ui, "Location:"); + label(ui, tl!("Location:")); if let Some(v) = widgets::num_field(ui, "pp-location", Some(d.f64("location", 0.0)), unit, 120.0) { d.fields.insert("location".into(), json!(v)); } diff --git a/crates/ui-egui/src/dialogs/perspective_presets.rs b/crates/ui-egui/src/dialogs/perspective_presets.rs index 52d8c2e6e..c720a036d 100644 --- a/crates/ui-egui/src/dialogs/perspective_presets.rs +++ b/crates/ui-egui/src/dialogs/perspective_presets.rs @@ -32,7 +32,7 @@ pub const SLOT: &str = "ui.perspectiveUserPreset"; pub const SLOTS: usize = 5; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Perspective Grid Presets".into(), + heading: |_| tl!("Perspective Grid Presets").into(), body, confirm: |app, _| { app.ui.dialog = None; @@ -64,19 +64,26 @@ fn details(p: &GridDefinition) -> Vec<(&'static str, String)> { let u = p.unit(); let len = |v: f64| u.format(u.to_pt(v)); let mut rows = vec![ - ("Type:", ["One Point", "Two Point", "Three Point"].get(usize::from(p.kind.clamp(1, 3) - 1)).copied().unwrap_or_default().to_string()), - ("Units:", u.label().to_string()), - ("Scale:", format!("{}:{}", Unit::Points.number(p.scale[0]), Unit::Points.number(p.scale[1]))), - ("Gridline every:", len(p.gridline)), + ( + tl!("Type:"), + [tl!("One Point"), tl!("Two Point"), tl!("Three Point")] + .get(usize::from(p.kind.clamp(1, 3) - 1)) + .copied() + .unwrap_or_default() + .to_string(), + ), + (tl!("Units:"), u.label().to_string()), + (tl!("Scale:"), format!("{}:{}", Unit::Points.number(p.scale[0]), Unit::Points.number(p.scale[1]))), + (tl!("Gridline every:"), len(p.gridline)), ]; if p.kind != 1 { - rows.push(("Viewing Angle:", format!("{:.1}°", p.angle))); + rows.push((tl!("Viewing Angle:"), format!("{:.1}°", p.angle))); } - rows.extend([("Viewing Distance:", len(p.distance)), ("Horizon Height:", len(p.horizon_height))]); + rows.extend([(tl!("Viewing Distance:"), len(p.distance)), (tl!("Horizon Height:"), len(p.horizon_height))]); if p.kind == 3 { - rows.push(("Third Vanishing Point:", format!("{}, {}", len(p.third_vp[0]), len(p.third_vp[1])))); + rows.push((tl!("Third Vanishing Point:"), format!("{}, {}", len(p.third_vp[0]), len(p.third_vp[1])))); } - rows.push(("Opacity:", format!("{:.0}%", p.opacity))); + rows.push((tl!("Opacity:"), format!("{:.0}%", p.opacity))); rows } @@ -91,13 +98,14 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.horizontal_top(|ui| { ui.vertical(|ui| { ui.set_width(260.0); - widgets::dim_label(ui, "Presets:"); + widgets::dim_label(ui, tl!("Presets:")); widgets::list_box(ui, |ui| { egui::ScrollArea::vertical().id_salt("perspective-presets").min_scrolled_height(240.0).max_height(240.0).show(ui, |ui| { ui.set_width(ui.available_width()); ui.set_min_height(240.0); for (k, p) in presets.iter().enumerate() { - if ui.selectable_label(k == i, &p.name).clicked() { + let shown = if is_builtin(&p.name) { tl!(&p.name) } else { p.name.as_str() }; + if ui.selectable_label(k == i, shown).clicked() { act = Some(Action::Select(p.name.clone())); } } @@ -106,19 +114,19 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(6.0); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 4.0; - if widgets::flat_button(ui, "New…", 48.0).on_hover_text("A new preset starting from the selected one").clicked() { + if widgets::flat_button(ui, tl!("New…"), 48.0).on_hover_text(tl!("A new preset starting from the selected one")).clicked() { act = Some(Action::New); } - if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Edit…", 48.0)).inner.clicked() { + if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, tl!("Edit…"), 48.0)).inner.clicked() { act = Some(Action::Edit); } - if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Delete", 52.0)).inner.clicked() { + if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, tl!("Delete"), 52.0)).inner.clicked() { act = Some(Action::Delete); } - if widgets::flat_button(ui, "Import…", 60.0).clicked() { + if widgets::flat_button(ui, tl!("Import…"), 60.0).clicked() { act = Some(Action::Import); } - if widgets::flat_button(ui, "Export…", 60.0).on_hover_text("Save the selected preset to a file").clicked() { + if widgets::flat_button(ui, tl!("Export…"), 60.0).on_hover_text(tl!("Save the selected preset to a file")).clicked() { act = Some(Action::Export); } }); @@ -126,7 +134,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(18.0); ui.vertical(|ui| { ui.set_width(280.0); - ui.label(egui::RichText::new(&preset.name).color(t.text_strong).strong()); + ui.label(egui::RichText::new(if builtin { tl!(&preset.name) } else { preset.name.as_str() }).color(t.text_strong).strong()); ui.add_space(8.0); egui::Grid::new("perspective-preset-details").num_columns(2).spacing([12.0, 4.0]).show(ui, |ui| { for (label, value) in details(preset) { @@ -137,7 +145,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); if builtin { ui.add_space(6.0); - ui.label(egui::RichText::new("Built-in presets don't change: New… starts an editable copy.").color(t.text_dim).size(11.5)); + ui.label(egui::RichText::new(tl!("Built-in presets don't change: New… starts an editable copy.")).color(t.text_dim).size(11.5)); } }); }); diff --git a/crates/ui-egui/src/dialogs/place.rs b/crates/ui-egui/src/dialogs/place.rs index 20878e727..3ddd71712 100644 --- a/crates/ui-egui/src/dialogs/place.rs +++ b/crates/ui-egui/src/dialogs/place.rs @@ -16,7 +16,7 @@ use crate::{VectorcraftApp, widgets}; pub const KIND: &str = "place"; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Place".into(), + heading: |_| tl!("Place").into(), body, confirm: crate::place::confirm, ok: Some("Place"), @@ -36,7 +36,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let files: Vec = d.fields.get("files").and_then(Value::as_array).cloned().unwrap_or_default(); let info = d.fields.get("__info").cloned().unwrap_or_default(); - widgets::subheader(ui, if files.len() == 1 { "File" } else { "Files" }); + widgets::subheader(ui, if files.len() == 1 { tl!("File") } else { tl!("Files") }); widgets::list_box(ui, |ui| { egui::ScrollArea::vertical().max_height(180.0).auto_shrink([false, true]).show(ui, |ui| { ui.set_width(ui.available_width()); @@ -63,7 +63,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); if files.len() > 1 { ui.add_space(4.0); - widgets::dim_label(ui, "Click to place each file at 100%, or drag to size it; arrow keys switch files, Esc skips one."); + widgets::dim_label(ui, tl!("Click to place each file at 100%, or drag to size it; arrow keys switch files, Esc skips one.")); } ui.add_space(10.0); // The options row. @@ -72,11 +72,11 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { for (key, label, tip) in OPTIONS { let on = d.bool(key); let enabled = key != "replace" || d.bool("__replace"); - let resp = ui.scope(|ui| widgets::check(ui, label, on && enabled, enabled)); + let resp = ui.scope(|ui| widgets::check(ui, tl!(label), on && enabled, enabled)); if resp.inner { d.fields.insert(key.into(), json!(!on)); } - resp.response.on_hover_text(tip); + resp.response.on_hover_text(tl!(tip)); } }); false diff --git a/crates/ui-egui/src/dialogs/placement_options.rs b/crates/ui-egui/src/dialogs/placement_options.rs index 3d5092e22..55849558f 100644 --- a/crates/ui-egui/src/dialogs/placement_options.rs +++ b/crates/ui-egui/src/dialogs/placement_options.rs @@ -27,7 +27,7 @@ const ALIGN: [&str; 9] = ["topLeft", "top", "topRight", "left", "center", "right const LABEL: f32 = 90.0; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Placement Options".into(), body, confirm, min_width: 400.0, max_width: Some(420.0), ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Placement Options").into(), body, confirm, min_width: 400.0, max_width: Some(420.0), ..DialogSpec::FORM }; /// Open the dialog on the placement options of images `ids` (default: the selected ones). pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result { @@ -48,12 +48,12 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { } fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { - form::choice(ui, d, "preserve", "Preserve:", (LABEL, 240.0), &PRESERVE); + form::choice(ui, d, "preserve", tl!("Preserve:"), (LABEL, 240.0), &PRESERVE); ui.add_space(8.0); // Bounds stretches the art into the old bounds: nothing to align or clip. let free = d.str("preserve") != "bounds"; ui.add_enabled_ui(free, |ui| { - widgets::label_row(ui, "Alignment:", LABEL, |ui| { + widgets::label_row(ui, tl!("Alignment:"), LABEL, |ui| { let current = ALIGN.iter().position(|a| *a == d.str("align")).unwrap_or(4); if let Some(i) = widgets::reference_point(ui, current) && let Some(a) = ALIGN.get(i) @@ -64,12 +64,12 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(8.0); widgets::label_row(ui, "", LABEL, |ui| { let clip = d.bool("clip"); - if widgets::check(ui, "Clip to Bounding Box", clip, free) { + if widgets::check(ui, tl!("Clip to Bounding Box"), clip, free) { d.fields.insert("clip".into(), json!(!clip)); } }); }); ui.add_space(6.0); - widgets::dim_label(ui, "Applies when the linked file is relinked or updated."); + widgets::dim_label(ui, tl!("Applies when the linked file is relinked or updated.")); false } diff --git a/crates/ui-egui/src/dialogs/plugin.rs b/crates/ui-egui/src/dialogs/plugin.rs index 6f40219bb..bd158ec24 100644 --- a/crates/ui-egui/src/dialogs/plugin.rs +++ b/crates/ui-egui/src/dialogs/plugin.rs @@ -24,7 +24,7 @@ const CMD: &str = "plugin.run"; pub const NO_FILTERS: &str = "No filter plug-ins installed"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |d| d.str("__label"), body, confirm, preview: true, max_width: Some(420.0), ..DialogSpec::FORM }; + DialogSpec { heading: |d| tl!(&d.str("__label")).to_string(), body, confirm, preview: true, max_width: Some(420.0), ..DialogSpec::FORM }; /// A menu label for plug-in `name`: with an ellipsis when it opens a dialog. fn label(name: &str, has_params: bool) -> &'static str { diff --git a/crates/ui-egui/src/dialogs/png_options.rs b/crates/ui-egui/src/dialogs/png_options.rs index ce53fed61..6e9520567 100644 --- a/crates/ui-egui/src/dialogs/png_options.rs +++ b/crates/ui-egui/src/dialogs/png_options.rs @@ -124,15 +124,15 @@ fn export_size(doc: &vectorcraft_doc::Document, p: &Value) -> Option<(f64, f64)> } fn heading(d: &Dialog) -> String { - format!("{} Options", format_of(d).map_or("Export", |f| f.label)) + crate::i18n::fmt(tl!("{format} Options"), &[("format", format_of(d).map_or(tl!("Export"), |f| f.label))]) } fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let id = format_of(d).map_or("png", |f| f.id); - let label = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(text).color(t.text_dim)); + let label = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(tl!(text)).color(t.text_dim)); egui::Grid::new("raster-options").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| { - label(ui, "Resolution:"); + label(ui, tl!("Resolution:")); let ppi = d.f64("ppi", 72.0); let preset = RESOLUTIONS.iter().position(|p| *p == ppi).filter(|_| !d.bool("__otherPpi")); ui.horizontal(|ui| { @@ -155,7 +155,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { if let Some([w, h]) = d.fields.get("__size").and_then(Value::as_array).map(|a| [0, 1].map(|i| a.get(i).and_then(Value::as_f64).unwrap_or(0.0))) { - label(ui, "Size:"); + label(ui, tl!("Size:")); let (pw, ph) = vectorcraft_render::region_pixels(vectorcraft_geom::Rect::new(0.0, 0.0, w, h), d.f64("ppi", 72.0) / 72.0); ui.label(egui::RichText::new(format!("{pw} × {ph} px")).color(t.text)); ui.end_row(); @@ -169,8 +169,8 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { /// choose (JPEG quality) and what writes other files (image maps). pub(super) fn option_rows(ui: &mut egui::Ui, d: &mut Dialog, id: &str, screens: bool) { let t = Tokens::get(ui.ctx()); - let label = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(text).color(t.text_dim)); - label(ui, "Background Color:"); + let label = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(tl!(text)).color(t.text_dim)); + label(ui, tl!("Background Color:")); let bg = d.str("background"); let choice = BACKGROUNDS.iter().position(|v| bg.eq_ignore_ascii_case(v)).unwrap_or(3); // JPEG has no transparency, nor have some TIFF, BMP and Targa options: their list starts at @@ -187,7 +187,7 @@ pub(super) fn option_rows(ui: &mut egui::Ui, d: &mut Dialog, id: &str, screens: }); ui.end_row(); - label(ui, "Anti-aliasing:"); + label(ui, tl!("Anti-aliasing:")); let aa = AntiAlias::from_id(&d.str("antiAlias")).unwrap_or_default(); if let Some(i) = widgets::dropdown(ui, "ro-aa", aa.label(), &AntiAlias::ALL.map(AntiAlias::label), 150.0) { d.fields.insert("antiAlias".into(), json!(AntiAlias::ALL[i].id())); @@ -197,14 +197,14 @@ pub(super) fn option_rows(ui: &mut egui::Ui, d: &mut Dialog, id: &str, screens: match id { "png" => { ui.label(""); - form::check(ui, d, "interlaced", "Interlaced"); + form::check(ui, d, "interlaced", tl!("Interlaced")); ui.end_row(); } "jpg" => jpeg_rows(ui, d, &label, screens), "gif" | "png8" => palette_rows(ui, d, &label), "webp" => { ui.label(""); - label(ui, "Lossless (lossy WebP isn't available yet)"); + label(ui, tl!("Lossless (lossy WebP isn't available yet)")); ui.end_row(); } "psd" => psd_options::rows(ui, d, &label), @@ -215,7 +215,7 @@ pub(super) fn option_rows(ui: &mut egui::Ui, d: &mut Dialog, id: &str, screens: /// The JPEG Options rows: colour model, quality, method and scans, profile and image map (not /// quality and image map for Export for Screens). fn jpeg_rows(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, &str) -> egui::Response, screens: bool) { - label(ui, "Color Model:"); + label(ui, tl!("Color Model:")); choice(ui, d, "colorModel", &ColorModel::ALL.map(ColorModel::id), &ColorModel::ALL.map(ColorModel::label)); ui.end_row(); @@ -224,19 +224,19 @@ fn jpeg_rows(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, &s } method_row(ui, d, label); if !screens { - label(ui, "Image Map:"); + label(ui, tl!("Image Map:")); choice(ui, d, "imageMap", &IMAGE_MAPS, &IMAGE_MAP_LABELS); ui.end_row(); } ui.label(""); - form::check(ui, d, "embedIcc", "Embed ICC Profile"); + form::check(ui, d, "embedIcc", tl!("Embed ICC Profile")); ui.end_row(); } /// JPEG quality on the dialog's 0–10 scale (stored ×10). fn quality_row(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, &str) -> egui::Response) { - label(ui, "Quality:"); + label(ui, tl!("Quality:")); let mut q = (d.f64("quality", 90.0) / 10.0).round().clamp(0.0, 10.0) as u8; ui.horizontal(|ui| { if ui.add(egui::Slider::new(&mut q, 0..=10)).changed() { @@ -249,11 +249,11 @@ fn quality_row(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, /// The JPEG method, and its scans when progressive. fn method_row(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, &str) -> egui::Response) { - label(ui, "Method:"); + label(ui, tl!("Method:")); ui.horizontal(|ui| { choice(ui, d, "method", &Method::ALL.map(Method::id), &Method::ALL.map(Method::label)); if Method::from_id(&d.str("method")) == Some(Method::Progressive) { - label(ui, "Scans:"); + label(ui, tl!("Scans:")); let mut scans = d.f64("scans", 3.0).round().clamp(*jpeg::SCANS.start() as f64, *jpeg::SCANS.end() as f64) as u8; if ui.add(egui::DragValue::new(&mut scans).range(jpeg::SCANS)).changed() { d.fields.insert("scans".into(), json!(scans)); @@ -266,11 +266,11 @@ fn method_row(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, & /// The PNG-8 and GIF Options rows: the palette (reduction, colours, dither and amount), /// transparency and matte, interlacing. fn palette_rows(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, &str) -> egui::Response) { - label(ui, "Color Reduction:"); + label(ui, tl!("Color Reduction:")); choice(ui, d, "reduction", &Reduction::ALL.map(Reduction::id), &Reduction::ALL.map(Reduction::label)); ui.end_row(); - label(ui, "Colors:"); + label(ui, tl!("Colors:")); let mut n = d.f64("colors", 256.0).round().clamp(2.0, 256.0) as u16; ui.horizontal(|ui| { if let Some(i) = widgets::dropdown(ui, "ro-colors", &n.to_string(), &COLOR_COUNTS, 70.0) { @@ -283,7 +283,7 @@ fn palette_rows(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, }); ui.end_row(); - label(ui, "Dither:"); + label(ui, tl!("Dither:")); ui.horizontal(|ui| { choice(ui, d, "dither", &Dither::ALL.map(Dither::id), &Dither::ALL.map(Dither::label)); if Dither::from_id(&d.str("dither")).is_some_and(|x| x != Dither::None) { @@ -295,7 +295,7 @@ fn palette_rows(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, }); ui.end_row(); - label(ui, "Matte:"); + label(ui, tl!("Matte:")); let matte = d.str("matte"); let at = MATTES.iter().position(|v| matte.eq_ignore_ascii_case(v)).unwrap_or(3); ui.horizontal(|ui| { @@ -310,8 +310,8 @@ fn palette_rows(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, ui.label(""); ui.horizontal(|ui| { - form::check(ui, d, "transparency", "Transparency"); - form::check(ui, d, "interlaced", "Interlaced"); + form::check(ui, d, "transparency", tl!("Transparency")); + form::check(ui, d, "interlaced", tl!("Interlaced")); }); ui.end_row(); } diff --git a/crates/ui-egui/src/dialogs/print.rs b/crates/ui-egui/src/dialogs/print.rs index 58ad7297d..808d70878 100644 --- a/crates/ui-egui/src/dialogs/print.rs +++ b/crates/ui-egui/src/dialogs/print.rs @@ -36,13 +36,13 @@ use vectorcraft_engine::cmd::printpresets::DEFAULT_PRESET; pub(super) const KIND: &str = "print"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Print".into(), body, confirm, ok: Some("Print"), discard: Some("Done"), ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Print").into(), body, confirm, ok: Some("Print"), discard: Some("Done"), ..DialogSpec::FORM }; /// The dialog kind of the preset editor (New / Edit in Edit → Print Presets). pub(super) const PRESET_KIND: &str = "printPreset"; pub(super) const PRESET_SPEC: DialogSpec = DialogSpec { - heading: |d| if d.str(EDITING).is_empty() { "New Print Preset" } else { "Edit Print Preset" }.into(), + heading: |d| if d.str(EDITING).is_empty() { tl!("New Print Preset") } else { tl!("Edit Print Preset") }.into(), body, confirm: confirm_preset, ok: Some("Save Preset"), @@ -218,7 +218,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { app.status(e); } if d.kind == PRESET_KIND { - row_with(ui, "Preset name:", TOP_LABEL_WIDTH, |ui| { + row_with(ui, tl!("Preset name:"), TOP_LABEL_WIDTH, |ui| { form::text_edit(ui, d, "name", 288.0); }); } else { @@ -237,7 +237,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(14.0); ui.vertical(|ui| { ui.set_width(500.0); - ui.label(egui::RichText::new(section).font(theme::semibold(14.0)).color(t.text_strong)); + ui.label(egui::RichText::new(tl!(section)).font(theme::semibold(14.0)).color(t.text_strong)); egui::ScrollArea::vertical().id_salt(("print", section)).max_height(360.0).auto_shrink([false, false]).show(ui, |ui| match section { "Marks and Bleed" => super::save_pdf::marks_and_bleeds(app, ui, d), "Output" => output(ui, d, &preview), @@ -263,11 +263,11 @@ fn preset_rows(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let mut picked = None; let asking = d.fields.contains_key(SAVE_AS); let mut ask = false; - row_with(ui, "Print Preset:", TOP_LABEL_WIDTH, |ui| { + row_with(ui, tl!("Print Preset:"), TOP_LABEL_WIDTH, |ui| { picked = widgets::dropdown(ui, "print-preset", if same { &name } else { CUSTOM }, &names, 300.0).and_then(|i| names.get(i)).map(|n| n.to_string()); - let save = ui.add_enabled_ui(!asking, |ui| widgets::flat_button(ui, "Save Preset…", 96.0)).inner; - ask = save.on_hover_text("Save these settings as a print preset").clicked(); + let save = ui.add_enabled_ui(!asking, |ui| widgets::flat_button(ui, tl!("Save Preset…"), 96.0)).inner; + ask = save.on_hover_text(tl!("Save these settings as a print preset")).clicked(); }); if let Some(n) = picked { // Picking the preset shown again brings its settings back. @@ -281,14 +281,14 @@ fn preset_rows(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { d.fields.insert(SAVE_AS.into(), json!(app.session.new_print_preset_name())); } if d.fields.contains_key(SAVE_AS) { - row_with(ui, "Save as preset:", TOP_LABEL_WIDTH, |ui| { + row_with(ui, tl!("Save as preset:"), TOP_LABEL_WIDTH, |ui| { form::text_edit(ui, d, SAVE_AS, 200.0); - if widgets::flat_button(ui, "Save", 52.0).clicked() + if widgets::flat_button(ui, tl!("Save"), 52.0).clicked() && let Err(e) = save_preset(app, d) { app.status(e); } - if widgets::flat_button(ui, "Cancel", 60.0).clicked() { + if widgets::flat_button(ui, tl!("Cancel"), 60.0).clicked() { d.fields.remove(SAVE_AS); } }); @@ -331,7 +331,7 @@ fn printer_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { } else { printer.as_str() }; - row_with(ui, "Printer:", TOP_LABEL_WIDTH, |ui| { + row_with(ui, tl!("Printer:"), TOP_LABEL_WIDTH, |ui| { let options: Vec<&str> = labels.iter().map(String::as_str).collect(); match widgets::dropdown(ui, "print-printer", current, &options, 300.0).and_then(|i| entries.get(i)) { Some(Destination::Printer(name)) => { @@ -348,8 +348,8 @@ fn printer_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { None => {} } let to_file = d.bool("toFile"); - let tip = if setup { "The system's settings of this printer" } else { "No printer settings to open here" }; - if ui.add_enabled_ui(setup && !to_file, |ui| widgets::flat_button(ui, "Setup…", 72.0)).inner.on_hover_text(tip).clicked() + let tip = if setup { tl!("The system's settings of this printer") } else { tl!("No printer settings to open here") }; + if ui.add_enabled_ui(setup && !to_file, |ui| widgets::flat_button(ui, tl!("Setup…"), 72.0)).inner.on_hover_text(tip).clicked() && let Err(e) = app.run("print.printerSetup", json!({ "printer": d.str("printer") })) { app.status(e); @@ -395,9 +395,9 @@ fn page_preview(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, pv: let sheet = pv.as_ref().ok().and_then(|v| current_sheet(d, v)); let Some((index, count, sheet)) = sheet else { let text = match pv { - _ if app.session.active().is_none() => "Open a document to preview its pages", - Ok(_) => "Nothing to print", - Err(_) => "Can't print these settings: see the Summary", + _ if app.session.active().is_none() => tl!("Open a document to preview its pages"), + Ok(_) => tl!("Nothing to print"), + Err(_) => tl!("Can't print these settings: see the Summary"), }; let font = egui::FontId::proportional(11.0); let galley = ui.painter().layout(text.into(), font, t.text_dim, rect.width() - 16.0); @@ -462,26 +462,26 @@ fn page_preview(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, pv: clip.rect_stroke(r, 0.0, egui::Stroke::new(1.0, t.accent), egui::StrokeKind::Middle); } let resp = if tiling { resp } else { resp.on_hover_cursor(egui::CursorIcon::Grab) }; - resp.on_hover_text(if tiling { "The page as it prints" } else { "Drag the art to move it on the paper" }); + resp.on_hover_text(if tiling { tl!("The page as it prints") } else { tl!("Drag the art to move it on the paper") }); // ◀ n of N ▶, then what the page is. ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; - if widgets::icon_button_enabled(ui, "chevron-left", "Previous page", false, index > 0, 20.0).clicked() { + if widgets::icon_button_enabled(ui, "chevron-left", tl!("Previous page"), false, index > 0, 20.0).clicked() { d.fields.insert("__sheet".into(), json!(index - 1)); } - let text = format!("{} of {count}", index + 1); + let text = crate::i18n::fmt(tl!("{page} of {count}"), &[("page", &(index + 1).to_string()), ("count", &count.to_string())]); ui.add_sized([PREVIEW.x - 44.0, 20.0], egui::Label::new(egui::RichText::new(text).size(11.5).color(t.text))); - if widgets::icon_button_enabled(ui, "chevron-right", "Next page", false, index + 1 < count, 20.0).clicked() { + if widgets::icon_button_enabled(ui, "chevron-right", tl!("Next page"), false, index + 1 < count, 20.0).clicked() { d.fields.insert("__sheet".into(), json!(index + 1)); } }); let mut what = vec![]; match sheet["artboard"].as_u64() { - Some(a) => what.push(format!("Artboard {}", a + 1)), - None => what.push("All the art".into()), + Some(a) => what.push(crate::i18n::fmt(tl!("Artboard {number}"), &[("number", &(a + 1).to_string())])), + None => what.push(tl!("All the art").into()), } if let Some(tile) = sheet["tile"].as_u64() { - what.push(format!("tile {tile}")); + what.push(crate::i18n::fmt(tl!("tile {number}"), &[("number", &tile.to_string())])); } if let Some(ink) = sheet["ink"].as_str() { what.push(ink.to_string()); @@ -563,17 +563,17 @@ fn whole(ui: &mut egui::Ui, d: &mut Dialog, path: &str, range: std::ops::RangeIn fn general(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let unit = app.session.general_unit(); - row(ui, "Copies:", |ui| { + row(ui, tl!("Copies:"), |ui| { whole(ui, d, "copies", 1.0..=f64::from(vectorcraft_pdf::MAX_COPIES), true); ui.add_space(12.0); let copies = get(d, "copies").as_u64().unwrap_or(1); - flag(ui, d, "collate", "Collate", copies > 1); - flag(ui, d, "reverse", "Reverse Order", true); + flag(ui, d, "collate", tl!("Collate"), copies > 1); + flag(ui, d, "reverse", tl!("Reverse Order"), true); }); - heading(ui, "Artboards"); + heading(ui, tl!("Artboards")); let artboards = choice::(d, "artboards").unwrap_or(PrintArtboards::All); ui.horizontal(|ui| { - for (a, label) in [(PrintArtboards::All, "All"), (PrintArtboards::Range, "Range:")] { + for (a, label) in [(PrintArtboards::All, tl!("All")), (PrintArtboards::Range, tl!("Range:"))] { if widgets::radio(ui, label, artboards == a, true) { set(d, "artboards", json!(a.id())); } @@ -584,43 +584,43 @@ fn general(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { set(d, "range", json!(range)); } }); - if widgets::radio(ui, "Ignore Artboards (all the art as one page)", artboards == PrintArtboards::Ignore, true) { + if widgets::radio(ui, tl!("Ignore Artboards (all the art as one page)"), artboards == PrintArtboards::Ignore, true) { set(d, "artboards", json!(PrintArtboards::Ignore.id())); } - flag(ui, d, "skipBlank", "Skip Blank Artboards", artboards != PrintArtboards::Ignore); - heading(ui, "Media"); - row(ui, "Size:", |ui| { + flag(ui, d, "skipBlank", tl!("Skip Blank Artboards"), artboards != PrintArtboards::Ignore); + heading(ui, tl!("Media")); + row(ui, tl!("Size:"), |ui| { pick::(ui, d, "media", 160.0, |_| true); }); // A paper size shows its size; Custom takes one. let media = choice::(d, "media").unwrap_or_default(); - row(ui, "Width:", |ui| match media.size() { + row(ui, tl!("Width:"), |ui| match media.size() { Some((w, h)) => { ui.add_enabled_ui(false, |ui| widgets::num_field(ui, "print-media-w", Some(w), unit, 80.0)); - ui.label("Height:"); + ui.label(tl!("Height:")); ui.add_enabled_ui(false, |ui| widgets::num_field(ui, "print-media-h", Some(h), unit, 80.0)); } None => { length(ui, d, "width", unit, true); - ui.label("Height:"); + ui.label(tl!("Height:")); length(ui, d, "height", unit, true); } }); - heading(ui, "Orientation"); - flag(ui, d, "autoRotate", "Auto-Rotate (turn the paper to each artboard)", true); + heading(ui, tl!("Orientation")); + flag(ui, d, "autoRotate", tl!("Auto-Rotate (turn the paper to each artboard)"), true); let auto = get(d, "autoRotate").as_bool() == Some(true); - row(ui, "Orientation:", |ui| { + row(ui, tl!("Orientation:"), |ui| { ui.add_enabled_ui(!auto, |ui| pick::(ui, d, "orientation", 160.0, |_| true)); }); - flag(ui, d, "transverse", "Transverse (the page a quarter turn on the paper)", true); - heading(ui, "Options"); - row(ui, "Print Layers:", |ui| { + flag(ui, d, "transverse", tl!("Transverse (the page a quarter turn on the paper)"), true); + heading(ui, tl!("Options")); + row(ui, tl!("Print Layers:"), |ui| { pick::(ui, d, "printLayers", 220.0, |_| true); }); let scaling = choice::(d, "scaling").unwrap_or(PrintScaling::None); let tiles = scaling.tiles(); let placed = get(d, PLACED).as_bool() == Some(true); - row(ui, "Placement:", |ui| { + row(ui, tl!("Placement:"), |ui| { ui.add_enabled_ui(!tiles && !placed, |ui| { let origin = choice::(d, "placement.origin").unwrap_or_default(); let current = Origin::ALL.iter().position(|o| *o == origin).unwrap_or(4); @@ -635,50 +635,50 @@ fn general(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { }); if placed { row(ui, "", |ui| { - note(ui, "The Print Tiling tool placed the pages."); - if widgets::flat_button(ui, "Reset", 56.0).on_hover_text("Place the pages with the placement again").clicked() { + note(ui, tl!("The Print Tiling tool placed the pages.")); + if widgets::flat_button(ui, tl!("Reset"), 56.0).on_hover_text(tl!("Place the pages with the placement again")).clicked() { set(d, PLACED, json!(false)); } }); } - row(ui, "Scaling:", |ui| { + row(ui, tl!("Scaling:"), |ui| { pick::(ui, d, "scaling", 180.0, |_| true); }); - row(ui, "Scale:", |ui| { + row(ui, tl!("Scale:"), |ui| { let on = scaling == PrintScaling::Custom || tiles; - ui.label("W:"); + ui.label(tl!("W:")); number(ui, d, "scale.width", "%", on); - ui.label("H:"); + ui.label(tl!("H:")); number(ui, d, "scale.height", "%", on); }); - row(ui, "Overlap:", |ui| length(ui, d, "overlap", unit, tiles)); - row(ui, "Tile Range:", |ui| text(ui, d, "tileRange", tiles)); + row(ui, tl!("Overlap:"), |ui| length(ui, d, "overlap", unit, tiles)); + row(ui, tl!("Tile Range:"), |ui| text(ui, d, "tileRange", tiles)); if tiles { - note(ui, "Tiles are numbered across, then down; leave the range empty for every tile."); + note(ui, tl!("Tiles are numbered across, then down; leave the range empty for every tile.")); } } fn output(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result) { - row(ui, "Mode:", |ui| { + row(ui, tl!("Mode:"), |ui| { pick::(ui, d, "output.mode", 160.0, |_| true); }); - row(ui, "Emulsion:", |ui| { + row(ui, tl!("Emulsion:"), |ui| { pick::(ui, d, "output.emulsion", 160.0, |_| true); }); - row(ui, "Image:", |ui| { + row(ui, tl!("Image:"), |ui| { pick::(ui, d, "output.image", 160.0, |_| true); }); let separations = choice::(d, "output.mode") == Some(OutputMode::Separations); - flag(ui, d, "output.spotsToProcess", "Convert All Spot Colors to Process", separations); - heading(ui, "Document Ink Options"); + flag(ui, d, "output.spotsToProcess", tl!("Convert All Spot Colors to Process"), separations); + heading(ui, tl!("Document Ink Options")); let inks = pv.as_ref().ok().and_then(|v| v["inks"].as_array()).map(Vec::as_slice).unwrap_or_default(); if !separations || inks.is_empty() { - note(ui, "Separations list the document's inks here: one page per ink that prints."); + note(ui, tl!("Separations list the document's inks here: one page per ink that prints.")); return; } let t = Tokens::get(ui.ctx()); egui::Grid::new("print-inks").num_columns(4).spacing([12.0, 4.0]).show(ui, |ui| { - for h in ["Print", "Ink", "Frequency", "Angle"] { + for h in [tl!("Print"), tl!("Ink"), tl!("Frequency"), tl!("Angle")] { ui.label(egui::RichText::new(h).size(11.5).color(t.text_dim)); } ui.end_row(); @@ -688,7 +688,7 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result) { if widgets::check(ui, "", on, true) { set_ink(d, name, "print", json!(!on)); } - let spot = if ink["spot"] == true { " (spot)" } else { "" }; + let spot = if ink["spot"] == true { format!(" ({})", tl!("spot")) } else { String::new() }; ui.label(egui::RichText::new(format!("{name}{spot}")).color(t.text)); for (k, suffix) in [("frequency", " lpi"), ("angle", "°")] { let v = ink[k].as_f64().unwrap_or(0.0); @@ -699,7 +699,7 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result) { ui.end_row(); } }); - note(ui, "Frequencies and angles are the output device's to apply."); + note(ui, tl!("Frequencies and angles are the output device's to apply.")); } /// Set option `key` of ink `name` in `output.inks` (the ink gets its own entry the first time). @@ -717,15 +717,15 @@ fn set_ink(d: &mut Dialog, name: &str, key: &str, value: Value) { } fn graphics(ui: &mut egui::Ui, d: &mut Dialog) { - heading(ui, "Paths"); - flag(ui, d, "graphics.autoFlatness", "Automatic flatness", true); + heading(ui, tl!("Paths")); + flag(ui, d, "graphics.autoFlatness", tl!("Automatic flatness"), true); let auto = get(d, "graphics.autoFlatness").as_bool() == Some(true); - row(ui, "Flatness:", |ui| { + row(ui, tl!("Flatness:"), |ui| { number(ui, d, "graphics.flatness", "", !auto); - ui.label(egui::RichText::new("0.2 (quality) to 100 (speed)").size(11.5).color(Tokens::get(ui.ctx()).text_dim)); + ui.label(egui::RichText::new(tl!("0.2 (quality) to 100 (speed)")).size(11.5).color(Tokens::get(ui.ctx()).text_dim)); }); - heading(ui, "Fonts"); - row(ui, "Download:", |ui| { + heading(ui, tl!("Fonts")); + row(ui, tl!("Download:"), |ui| { pick::(ui, d, "graphics.fonts", 160.0, |_| true); }); } @@ -733,37 +733,39 @@ fn graphics(ui: &mut egui::Ui, d: &mut Dialog) { fn color(ui: &mut egui::Ui, d: &mut Dialog) { let profiles = vectorcraft_color::cms::profiles(); let names: Vec<&str> = std::iter::once(SAME_AS_SOURCE).chain(profiles.iter().map(|p| p.name.as_str())).collect(); - row(ui, "Printer profile:", |ui| super::save_pdf::profile_pick(ui, d, "color.profile", &names, true)); - note(ui, "Composite colours are converted to it; separations separate with it when it is a CMYK profile."); - row(ui, "Rendering intent:", |ui| { + row(ui, tl!("Printer profile:"), |ui| super::save_pdf::profile_pick(ui, d, "color.profile", &names, true)); + note(ui, tl!("Composite colours are converted to it; separations separate with it when it is a CMYK profile.")); + row(ui, tl!("Rendering intent:"), |ui| { let current = serde_json::from_value::(get(d, "color.intent").clone()).unwrap_or_default(); let labels: Vec<&str> = Intent::ALL.iter().map(Intent::label).collect(); if let Some(i) = widgets::dropdown(ui, "color.intent", current.label(), &labels, 200.0).and_then(|i| Intent::ALL.get(i)) { set(d, "color.intent", serde_json::to_value(i).unwrap_or_default()); } }); - note(ui, "How colours outside the press's gamut are separated."); - flag(ui, d, "color.preserveNumbers", "Preserve CMYK Numbers", true); - note(ui, "CMYK colours print with their own values; off, they go through the colour settings too."); + note(ui, tl!("How colours outside the press's gamut are separated.")); + flag(ui, d, "color.preserveNumbers", tl!("Preserve CMYK Numbers"), true); + note(ui, tl!("CMYK colours print with their own values; off, they go through the colour settings too.")); } fn advanced(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { - heading(ui, "Printer"); - row(ui, "Unprintable margin:", |ui| length(ui, d, "margin", app.session.general_unit(), true)); - note(ui, "The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed."); + heading(ui, tl!("Printer")); + row(ui, tl!("Unprintable margin:"), |ui| length(ui, d, "margin", app.session.general_unit(), true)); + note(ui, tl!("The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed.")); let composite = choice::(d, "output.mode") != Some(OutputMode::Separations); - flag(ui, d, "advanced.printAsBitmap", "Print as Bitmap", composite); - note(ui, "Each page prints as one image, at the document's raster effects resolution."); - heading(ui, "Overprint and Transparency Flattener Options"); - row(ui, "Overprints:", |ui| { + flag(ui, d, "advanced.printAsBitmap", tl!("Print as Bitmap"), composite); + note(ui, tl!("Each page prints as one image, at the document's raster effects resolution.")); + heading(ui, tl!("Overprint and Transparency Flattener Options")); + row(ui, tl!("Overprints:"), |ui| { ui.add_enabled_ui(composite, |ui| pick::(ui, d, "advanced.overprints", 160.0, |_| true)); }); let presets = app.session.flattener_presets(); let names: Vec<&str> = std::iter::once(KEEP_TRANSPARENCY).chain(presets.iter().map(|p| p.name.as_str())).collect(); - row(ui, "Preset:", |ui| super::save_pdf::profile_pick(ui, d, "advanced.flattenerPreset", &names, true)); + row(ui, tl!("Preset:"), |ui| super::save_pdf::profile_pick(ui, d, "advanced.flattenerPreset", &names, true)); note( ui, - "Simulate prints overprints as Overprint Preview shows them; separations always keep them. A preset flattens transparency before printing (PostScript files always flatten it).", + tl!( + "Simulate prints overprints as Overprint Preview shows them; separations always keep them. A preset flattens transparency before printing (PostScript files always flatten it)." + ), ); } @@ -827,28 +829,34 @@ fn summary(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result, has_do let t = Tokens::get(ui.ctx()); match pv { Ok(v) => { - heading(ui, "Pages"); + heading(ui, tl!("Pages")); let per_copy = v["sheets"].as_array().map_or(0, Vec::len); - ui.label(egui::RichText::new(format!("{} in all, {per_copy} per copy", v["pages"])).color(t.text)); + ui.label( + egui::RichText::new(crate::i18n::fmt( + tl!("{pages} in all, {count} per copy"), + &[("pages", &v["pages"].to_string()), ("count", &per_copy.to_string())], + )) + .color(t.text), + ); } // A preset edited without a document has no pages to count. Err(_) if !has_doc => {} Err(e) => { - heading(ui, "Error"); + heading(ui, tl!("Error")); ui.label(egui::RichText::new(format!("⚠ {e}")).color(t.text)); } } - heading(ui, "Options"); + heading(ui, tl!("Options")); let mut changed = vec![]; vectorcraft_engine::cmd::fileio::pdf::changed("", &settings(d), &DEFAULTS, &mut changed); changed.sort_by_key(|c| section_of(c["option"].as_str().unwrap_or_default()).0); labelled(&mut changed); if changed.is_empty() { - note(ui, "Every option is at its default."); + note(ui, tl!("Every option is at its default.")); } super::save_pdf::option_rows(ui, &changed, option_label); if let Ok(v) = pv { - heading(ui, "Warnings"); + heading(ui, tl!("Warnings")); super::save_pdf::warning_rows(ui, v["warnings"].as_array().map(Vec::as_slice).unwrap_or_default()); } } diff --git a/crates/ui-egui/src/dialogs/print_presets.rs b/crates/ui-egui/src/dialogs/print_presets.rs index 763a071a0..1d1da4898 100644 --- a/crates/ui-egui/src/dialogs/print_presets.rs +++ b/crates/ui-egui/src/dialogs/print_presets.rs @@ -20,7 +20,7 @@ use crate::{VectorcraftApp, widgets}; pub const KIND: &str = "printPresets"; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Print Presets".into(), + heading: |_| tl!("Print Presets").into(), body, confirm: |app, _| { app.ui.dialog = None; @@ -58,13 +58,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.horizontal_top(|ui| { ui.vertical(|ui| { ui.set_width(280.0); - widgets::dim_label(ui, "Presets:"); + widgets::dim_label(ui, tl!("Presets:")); widgets::list_box(ui, |ui| { egui::ScrollArea::vertical().id_salt("print-presets").min_scrolled_height(240.0).max_height(240.0).show(ui, |ui| { ui.set_width(ui.available_width()); ui.set_min_height(240.0); for (k, n) in std::iter::once(DEFAULT_PRESET).chain(saved.iter().map(|p| p.name.as_str())).enumerate() { - if ui.selectable_label(k == i, n).clicked() { + if ui.selectable_label(k == i, if k == 0 { tl!(n) } else { n }).clicked() { act = Some(Action::Select(n.to_string())); } } @@ -73,19 +73,19 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(6.0); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 4.0; - if widgets::flat_button(ui, "New…", 48.0).on_hover_text("A new preset starting from the selected one").clicked() { + if widgets::flat_button(ui, tl!("New…"), 48.0).on_hover_text(tl!("A new preset starting from the selected one")).clicked() { act = Some(Action::New); } - if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Edit…", 48.0)).inner.clicked() { + if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, tl!("Edit…"), 48.0)).inner.clicked() { act = Some(Action::Edit); } - if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Delete", 52.0)).inner.clicked() { + if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, tl!("Delete"), 52.0)).inner.clicked() { act = Some(Action::Delete); } - if widgets::flat_button(ui, "Import…", 60.0).clicked() { + if widgets::flat_button(ui, tl!("Import…"), 60.0).clicked() { act = Some(Action::Import); } - if widgets::flat_button(ui, "Export…", 60.0).on_hover_text("Save the selected preset to a file").clicked() { + if widgets::flat_button(ui, tl!("Export…"), 60.0).on_hover_text(tl!("Save the selected preset to a file")).clicked() { act = Some(Action::Export); } }); @@ -95,19 +95,19 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.set_width(300.0); ui.label(egui::RichText::new(&name).color(t.text_strong).strong()); ui.add_space(8.0); - widgets::dim_label(ui, &format!("Differences from {DEFAULT_PRESET}:")); + widgets::dim_label(ui, &crate::i18n::fmt(tl!("Differences from {preset}:"), &[("preset", DEFAULT_PRESET)])); let settings = i.checked_sub(1).and_then(|k| saved.get(k)).map(|p| &p.settings); let mut changed = settings.map(|s| changes(s, &Default::default())).unwrap_or_default(); changed.sort_by_key(|c| section_of(c["option"].as_str().unwrap_or_default()).0); egui::ScrollArea::vertical().id_salt("print-preset-details").max_height(200.0).show(ui, |ui| { if changed.is_empty() { - ui.label(egui::RichText::new("None.").color(t.text_dim).size(11.5)); + ui.label(egui::RichText::new(tl!("None.")).color(t.text_dim).size(11.5)); } option_rows(ui, &changed, option_label); }); if builtin { ui.add_space(6.0); - ui.label(egui::RichText::new("[Default] is protected: New… starts an editable copy.").color(t.text_dim).size(11.5)); + ui.label(egui::RichText::new(tl!("[Default] is protected: New… starts an editable copy.")).color(t.text_dim).size(11.5)); } }); }); diff --git a/crates/ui-egui/src/dialogs/psd_options.rs b/crates/ui-egui/src/dialogs/psd_options.rs index 17c2f4c97..df31d80bb 100644 --- a/crates/ui-egui/src/dialogs/psd_options.rs +++ b/crates/ui-egui/src/dialogs/psd_options.rs @@ -25,18 +25,18 @@ pub(super) fn defaults(cmyk: bool, o: &mut Map) { /// The grid rows of PSD's own options. pub(super) fn rows(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui::Ui, &str) -> egui::Response) { - label(ui, "Color Model:"); + label(ui, tl!("Color Model:")); choice(ui, d, "colorModel", &ColorModel::ALL.map(ColorModel::id), &ColorModel::ALL.map(ColorModel::label)); ui.end_row(); - label(ui, "Options:"); + label(ui, tl!("Options:")); ui.vertical(|ui| { let mut layers = d.bool("layers"); - if ui.radio_value(&mut layers, false, "Flat Image").changed() | ui.radio_value(&mut layers, true, "Write Layers").changed() { + if ui.radio_value(&mut layers, false, tl!("Flat Image")).changed() | ui.radio_value(&mut layers, true, tl!("Write Layers")).changed() { d.fields.insert("layers".into(), json!(layers)); } ui.add_enabled_ui(layers, |ui| { - for (key, text) in [("maxEditability", "Maximum Editability"), ("hiddenLayers", "Include Hidden Layers")] { + for (key, text) in [("maxEditability", tl!("Maximum Editability")), ("hiddenLayers", tl!("Include Hidden Layers"))] { ui.horizontal(|ui| { ui.add_space(22.0); form::check(ui, d, key, text); @@ -47,6 +47,6 @@ pub(super) fn rows(ui: &mut egui::Ui, d: &mut Dialog, label: &dyn Fn(&mut egui:: ui.end_row(); ui.label(""); - form::check(ui, d, "embedIcc", "Embed ICC Profile"); + form::check(ui, d, "embedIcc", tl!("Embed ICC Profile")); ui.end_row(); } diff --git a/crates/ui-egui/src/dialogs/raster_effects.rs b/crates/ui-egui/src/dialogs/raster_effects.rs index 7ef9e2ea7..b24a293ca 100644 --- a/crates/ui-egui/src/dialogs/raster_effects.rs +++ b/crates/ui-egui/src/dialogs/raster_effects.rs @@ -17,7 +17,7 @@ const CMD: &str = "document.rasterEffectsSettings"; const RESOLUTIONS: [(f64, &str); 3] = [(72.0, "Screen (72 ppi)"), (150.0, "Medium (150 ppi)"), (300.0, "High (300 ppi)")]; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Document Raster Effects Settings".into(), + heading: |_| tl!("Document Raster Effects Settings").into(), body, confirm, min_width: 440.0, @@ -44,9 +44,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { // Resolution: a preset, or Other with its own value. let ppi = d.f64("resolution", 72.0); let preset = RESOLUTIONS.iter().position(|(p, _)| (*p - ppi).abs() < 1e-9); - let mut labels: Vec<&str> = RESOLUTIONS.iter().map(|(_, l)| *l).collect(); - labels.push("Other"); - widgets::label_row(ui, "Resolution:", LABEL, |ui| { + let mut labels: Vec<&str> = RESOLUTIONS.iter().map(|(_, l)| tl!(l)).collect(); + labels.push(tl!("Other")); + widgets::label_row(ui, tl!("Resolution:"), LABEL, |ui| { if let Some(i) = widgets::dropdown(ui, "raster-resolution", labels[preset.unwrap_or(RESOLUTIONS.len())], &labels, 170.0) { // Other keeps the current value to edit. if let Some((p, _)) = RESOLUTIONS.get(i) { @@ -64,11 +64,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(4.0); let own = if app.session.active().is_some_and(|s| s.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk) { ("cmyk", "CMYK") } else { ("rgb", "RGB") }; - choice(ui, d, "colorModel", "Color Model:", &[own, ("grayscale", "Grayscale"), ("bitmap", "Bitmap")]); + choice(ui, d, "colorModel", tl!("Color Model:"), &[own, ("grayscale", tl!("Grayscale")), ("bitmap", tl!("Bitmap"))]); ui.add_space(6.0); - widgets::label_row(ui, "Background:", LABEL, |ui| { + widgets::label_row(ui, tl!("Background:"), LABEL, |ui| { let cur = d.str("background"); - for (id, label) in [("white", "White"), ("transparent", "Transparent")] { + for (id, label) in [("white", tl!("White")), ("transparent", tl!("Transparent"))] { if widgets::radio(ui, label, cur == id, true) { d.fields.insert("background".into(), json!(id)); } @@ -76,21 +76,21 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } }); ui.add_space(10.0); - widgets::subheader(ui, "Options"); - check(ui, d, "antiAlias", "Anti-alias"); - check(ui, d, "clippingMask", "Create Clipping Mask"); + widgets::subheader(ui, tl!("Options")); + check(ui, d, "antiAlias", tl!("Anti-alias")); + check(ui, d, "clippingMask", tl!("Create Clipping Mask")); let unit = app.session.general_unit(); let t = crate::theme::Tokens::get(ui.ctx()); ui.horizontal(|ui| { - ui.label(egui::RichText::new("Add").color(t.text)); + ui.label(egui::RichText::new(tl!("Add")).color(t.text)); if let Some(v) = widgets::num_field(ui, "raster-add-around", Some(d.f64("addAround", 0.0)), unit, 80.0) { d.fields.insert("addAround".into(), json!(v)); } - ui.label(egui::RichText::new("Around Object").color(t.text)); + ui.label(egui::RichText::new(tl!("Around Object")).color(t.text)); }); - check(ui, d, "preserveSpotColors", "Preserve Spot Colors"); + check(ui, d, "preserveSpotColors", tl!("Preserve Spot Colors")); ui.add_space(8.0); - super::form::caption(ui, "Changing these settings changes how raster effects look when exported, flattened or expanded."); + super::form::caption(ui, tl!("Changing these settings changes how raster effects look when exported, flattened or expanded.")); false } diff --git a/crates/ui-egui/src/dialogs/recolor.rs b/crates/ui-egui/src/dialogs/recolor.rs index bd2605871..e58e296f3 100644 --- a/crates/ui-egui/src/dialogs/recolor.rs +++ b/crates/ui-egui/src/dialogs/recolor.rs @@ -36,8 +36,15 @@ use crate::{VectorcraftApp, widgets}; /// The dialog kind of Recolor Artwork. pub const KIND: &str = "recolor"; -pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Recolor Artwork".into(), body, confirm, preview: true, min_width: 540.0, max_width: Some(560.0), ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Recolor Artwork").into(), + body, + confirm, + preview: true, + min_width: 540.0, + max_width: Some(560.0), + ..DialogSpec::FORM +}; const CMD: &str = "recolor.apply"; /// Width of the current colours of a row. @@ -203,7 +210,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); ui.horizontal(|ui| { let tab = d.str("tab"); - for (id, label) in [("assign", "Assign"), ("edit", "Edit")] { + for (id, label) in [("assign", tl!("Assign")), ("edit", tl!("Edit"))] { if ui.selectable_label(tab == id || (id == "assign" && tab != "edit"), egui::RichText::new(label).font(theme::semibold(12.5))).clicked() { d.fields.insert("tab".into(), json!(id)); } @@ -212,7 +219,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { if let Some(i) = widgets::dropdown(ui, "recolor-preset", PRESETS[preset_of(d)], &PRESETS, 130.0) { set_preset(app, d, i); } - ui.label(egui::RichText::new("Preset:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Preset:")).color(t.text_dim)); }); }); widgets::divider(ui); @@ -228,7 +235,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { if d.fields.get("group").is_some_and(Value::is_string) || d.bool("__newGroup") { ui.add_space(6.0); ui.horizontal(|ui| { - ui.label(egui::RichText::new("Color Group:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Color Group:")).color(t.text_dim)); form::text(ui, d, "groupName", 200.0); }); } @@ -289,16 +296,16 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut // Reduction and the preserve rules apply to art (a colour group alone has a row per colour). if d.bool("__art") { ui.horizontal(|ui| { - ui.label(egui::RichText::new("Colors:").color(t.text_dim)); - let labels: Vec = std::iter::once("Auto".to_string()).chain((1..=total).map(|n| n.to_string())).collect(); + ui.label(egui::RichText::new(tl!("Colors:")).color(t.text_dim)); + let labels: Vec = std::iter::once(tl!("Auto").to_string()).chain((1..=total).map(|n| n.to_string())).collect(); let refs: Vec<&str> = labels.iter().map(String::as_str).collect(); - let cur = d.fields.get("colors").and_then(Value::as_u64).map_or("Auto".to_string(), |n| n.to_string()); + let cur = d.fields.get("colors").and_then(Value::as_u64).map_or(tl!("Auto").to_string(), |n| n.to_string()); if let Some(i) = widgets::dropdown(ui, "recolor-count", &cur, &refs, 70.0) { d.fields.insert("colors".into(), if i == 0 { Value::Null } else { json!(i) }); } ui.add_space(18.0); - ui.label(egui::RichText::new("Preserve:").color(t.text_dim)); - for (k, label) in [("preserveWhite", "White"), ("preserveBlack", "Black"), ("preserveGrays", "Grays")] { + ui.label(egui::RichText::new(tl!("Preserve:")).color(t.text_dim)); + for (k, label) in [("preserveWhite", tl!("White")), ("preserveBlack", tl!("Black")), ("preserveGrays", tl!("Grays"))] { let on = d.bool(k); if widgets::check(ui, label, on, true) { d.fields.insert(k.into(), json!(!on)); @@ -309,9 +316,14 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut } ui.horizontal(|ui| { ui.add_space(6.0); - ui.add_sized([CURRENT_W, 16.0], egui::Label::new(egui::RichText::new(format!("Current Colors ({total})")).color(t.text_dim))); + ui.add_sized( + [CURRENT_W, 16.0], + egui::Label::new( + egui::RichText::new(crate::i18n::fmt(tl!("Current Colors ({total})"), &[("total", &total.to_string())])).color(t.text_dim), + ), + ); ui.add_space(60.0); - ui.label(egui::RichText::new("New").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("New")).color(t.text_dim)); }); let mut sel = selection(d); let mut clicked: Option = None; @@ -321,7 +333,7 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut ui.set_min_height(TABLE_H); if rows.is_empty() { ui.add_space(8.0); - widgets::dim_label(ui, "No colors to recolor (preserved colors are left out)."); + widgets::dim_label(ui, tl!("No colors to recolor (preserved colors are left out).")); } for (i, row) in rows.iter_mut().enumerate() { let bg = ui.painter().add(egui::Shape::Noop); @@ -340,7 +352,7 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut } let (ar, aresp) = ui.allocate_exact_size(vec2(44.0, 20.0), Sense::click()); arrow(ui, ar, !off, aresp.hovered()); - if aresp.on_hover_text(if off { "Recolor this row" } else { "Keep this row's colors (exclude)" }).clicked() { + if aresp.on_hover_text(if off { tl!("Recolor this row") } else { tl!("Keep this row's colors (exclude)") }).clicked() { row["exclude"] = json!(!off); changed = true; } @@ -379,20 +391,20 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut ui.add_space(4.0); ui.horizontal(|ui| { let can_merge = sel.len() > 1; - if widgets::icon_button_enabled(ui, "combine", "Merge colors into one row", false, can_merge, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "combine", tl!("Merge colors into one row"), false, can_merge, 24.0).clicked() { merge(rows, &sel); d.fields.insert("__sel".into(), json!([sel.iter().min()])); changed = true; } let can_split = sel.iter().any(|i| rows.get(*i).is_some_and(|r| r["from"].as_array().is_some_and(|a| a.len() > 1))); - if widgets::icon_button_enabled(ui, "ungroup", "Separate colors into rows", false, can_split, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "ungroup", tl!("Separate colors into rows"), false, can_split, 24.0).clicked() { separate(rows, &sel); d.fields.insert("__sel".into(), json!([])); changed = true; } ui.add_space(12.0); for (icon, tip, sb) in - [("arrow-left-right", "Randomly change color order", false), ("sparkles", "Randomly change saturation and brightness", true)] + [("arrow-left-right", tl!("Randomly change color order"), false), ("sparkles", tl!("Randomly change saturation and brightness"), true)] { if widgets::icon_button_enabled(ui, icon, tip, false, rows.len() > 1 || sb, 24.0).clicked() { let seed = d.f64("__seed", 0.0) as u64 + 1; @@ -407,26 +419,26 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut } } } - if widgets::icon_button(ui, "rotate-ccw", "Reset the rows to the artwork's colors", false, 24.0).clicked() { + if widgets::icon_button(ui, "rotate-ccw", tl!("Reset the rows to the artwork's colors"), false, 24.0).clicked() { d.fields.remove("__reduced"); } }); ui.add_space(8.0); egui::Grid::new("recolor-options").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| { - ui.label(egui::RichText::new("Method:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Method:")).color(t.text_dim)); let m = Method::parse(&d.str("method")).unwrap_or_default(); let labels: Vec<&str> = Method::ALL.iter().map(|m| m.label()).collect(); if let Some(i) = widgets::dropdown(ui, "recolor-method", m.label(), &labels, 180.0) { d.fields.insert("method".into(), json!(Method::ALL[i].id())); } ui.end_row(); - ui.label(egui::RichText::new("Limit to Library:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Limit to Library:")).color(t.text_dim)); // None, the document's swatches, then the colour libraries. let libs = colour_libraries(app); let keys: Vec<&str> = ["", swatchlib::DOCUMENT_SWATCHES].into_iter().chain(libs.iter().map(|l| l.id.as_str())).collect(); - let labels: Vec = keys.iter().map(|k| limit_name(app, k).unwrap_or_else(|| "None".into())).collect(); + let labels: Vec = keys.iter().map(|k| limit_name(app, k).unwrap_or_else(|| tl!("None").into())).collect(); let cur = d.str("limitTo"); - let name = keys.iter().position(|k| *k == cur).map_or("None", |i| labels[i].as_str()); + let name = keys.iter().position(|k| *k == cur).map_or(tl!("None"), |i| labels[i].as_str()); let labels: Vec<&str> = labels.iter().map(String::as_str).collect(); if let Some(i) = widgets::dropdown(ui, "recolor-library", name, &labels, 180.0) { d.fields.insert("limitTo".into(), json!(keys[i])); @@ -513,7 +525,7 @@ fn edit_tab(ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]) -> bool { let active = active_rows(rows); let base = selection(d).into_iter().find(|i| active.contains(i)).or(active.first().copied()); ui.horizontal(|ui| { - ui.label(egui::RichText::new("Harmony Rules:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Harmony Rules:")).color(t.text_dim)); let rule = Harmony::parse(&d.str("rule")).unwrap_or(Harmony::Complementary); let labels: Vec<&str> = Harmony::ALL.iter().map(|h| h.label()).collect(); // Choosing a rule gives the new colours its colours, from the base colour. @@ -525,7 +537,7 @@ fn edit_tab(ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]) -> bool { } } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let tip = if linked { "Unlink harmony colors" } else { "Link harmony colors" }; + let tip = if linked { tl!("Unlink harmony colors") } else { tl!("Link harmony colors") }; if widgets::icon_button(ui, if linked { "link" } else { "link-2-off" }, tip, linked, 24.0).clicked() { d.fields.insert("linked".into(), json!(!linked)); } diff --git a/crates/ui-egui/src/dialogs/recovery.rs b/crates/ui-egui/src/dialogs/recovery.rs index 59a8fa10a..54688248c 100644 --- a/crates/ui-egui/src/dialogs/recovery.rs +++ b/crates/ui-egui/src/dialogs/recovery.rs @@ -8,10 +8,10 @@ use super::DialogSpec; use crate::theme::Tokens; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Recover Documents".into(), + heading: |_| tl!("Recover Documents").into(), body: |_, ui, d| { let t = Tokens::get(ui.ctx()); - ui.label(egui::RichText::new("VectorCraft didn't quit normally last time. These documents had unsaved changes:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("VectorCraft didn't quit normally last time. These documents had unsaved changes:")).color(t.text_dim)); ui.add_space(8.0); let copies = d.fields.get("copies").and_then(Value::as_array); egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| { @@ -25,7 +25,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec { } }); ui.add_space(8.0); - ui.label(egui::RichText::new("Restored documents open unsaved: save them to keep them.").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Restored documents open unsaved: save them to keep them.")).color(t.text_dim)); false }, confirm: crate::recovery::confirm, diff --git a/crates/ui-egui/src/dialogs/saturate.rs b/crates/ui-egui/src/dialogs/saturate.rs index 37f47230d..2492c2e85 100644 --- a/crates/ui-egui/src/dialogs/saturate.rs +++ b/crates/ui-egui/src/dialogs/saturate.rs @@ -16,7 +16,7 @@ pub const KIND: &str = "saturate"; const CMD: &str = "edit.colors.saturate"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Saturate".into(), body, confirm, preview: true, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Saturate").into(), body, confirm, preview: true, ..DialogSpec::FORM }; pub fn open(app: &mut VectorcraftApp) { app.ui.dialog = Some(Dialog::new(KIND, json!({"intensity": 0, "preview": true}))); @@ -28,7 +28,7 @@ fn params(d: &Dialog) -> Value { fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { // The rail runs from grey to a fully saturated colour. - form::slider(ui, d, "intensity", "Intensity:", -100.0..=100.0, "%", &|x| c32(&Color::from_hsb(12.0, x, 0.85))); + form::slider(ui, d, "intensity", tl!("Intensity:"), -100.0..=100.0, "%", &|x| c32(&Color::from_hsb(12.0, x, 0.85))); let p = params(d); form::preview(app, ui, d, "Saturate", CMD, p); false diff --git a/crates/ui-egui/src/dialogs/save_changes.rs b/crates/ui-egui/src/dialogs/save_changes.rs index 7f50e027b..c959b96a2 100644 --- a/crates/ui-egui/src/dialogs/save_changes.rs +++ b/crates/ui-egui/src/dialogs/save_changes.rs @@ -4,10 +4,10 @@ use super::DialogSpec; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |d| format!("Do you want to save the changes you made to “{}”?", d.str("name")), + heading: |d| crate::i18n::fmt(tl!("Do you want to save the changes you made to “{name}”?"), &[("name", &d.str("name"))]), body: |_, ui, _| { let t = crate::theme::Tokens::get(ui.ctx()); - ui.label(egui::RichText::new("Your changes will be lost if you don't save them.").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Your changes will be lost if you don't save them.")).color(t.text_dim)); false }, confirm: |app, _| crate::unsaved::confirm(app), diff --git a/crates/ui-egui/src/dialogs/save_for_web.rs b/crates/ui-egui/src/dialogs/save_for_web.rs index 6b9dccbfa..44404ddba 100644 --- a/crates/ui-egui/src/dialogs/save_for_web.rs +++ b/crates/ui-egui/src/dialogs/save_for_web.rs @@ -33,7 +33,7 @@ use crate::{VectorcraftApp, io, widgets}; pub const KIND: &str = "saveForWeb"; pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| "Save for Web".into(), + heading: |_| tl!("Save for Web").into(), body, confirm, ok: Some("Save…"), @@ -409,8 +409,8 @@ fn preview_area(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, set d.fields.insert("__kbps".into(), json!(s)); } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let b = ui.add_enabled(!io::is_web(app), |ui: &mut egui::Ui| widgets::secondary_button(ui, "Preview in Browser")); - browser = b.on_disabled_hover_text("Needs the desktop app").clicked(); + let b = ui.add_enabled(!io::is_web(app), |ui: &mut egui::Ui| widgets::secondary_button(ui, tl!("Preview in Browser"))); + browser = b.on_disabled_hover_text(tl!("Needs the desktop app")).clicked(); }); }); browser @@ -484,15 +484,16 @@ fn draw_pane( match (settings, shot.bytes) { (Some(s), Some(bytes)) => { let secs = download_seconds(bytes, kbps); - let time = if secs < 1.0 { "< 1 sec".into() } else { format!("{} sec", secs.ceil()) }; + let time = + if secs < 1.0 { tl!("< 1 sec").into() } else { crate::i18n::fmt(tl!("{secs} sec"), &[("secs", &secs.ceil().to_string())]) }; let detail = match s.format { - WebFormat::Jpg => format!("Quality {}", s.quality), + WebFormat::Jpg => crate::i18n::fmt(tl!("Quality {quality}"), &[("quality", &s.quality.to_string())]), WebFormat::Png24 => String::new(), - _ => format!("{} colors", shot.colors.len()), + _ => crate::i18n::tn(shot.colors.len() as u64, "{n} color", "{n} colors"), }; (s.format.label().to_string(), format!("{} {time} @ {}", size_label(bytes), speed_label(kbps)), detail) } - _ => ("Original".into(), format!("{} × {} px", shot.size[0], shot.size[1]), String::new()), + _ => (tl!("Original").into(), format!("{} × {} px", shot.size[0], shot.size[1]), String::new()), } } }; @@ -519,36 +520,36 @@ fn side(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, s: Option<& let choice = |ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, options: &[(&str, &str)]| { form::choice(ui, d, key, label, (LABEL_W, FIELD_W), options); }; - choice(ui, d, "format", "Format:", &pairs(&WebFormat::ALL, WebFormat::id, WebFormat::label)); + choice(ui, d, "format", tl!("Format:"), &pairs(&WebFormat::ALL, WebFormat::id, WebFormat::label)); let format = s.map_or(WebFormat::Gif, |s| s.format); match format { WebFormat::Gif | WebFormat::Png8 => { - choice(ui, d, "reduction", "Reduction:", &pairs(&Reduction::ALL, Reduction::id, Reduction::label)); + choice(ui, d, "reduction", tl!("Reduction:"), &pairs(&Reduction::ALL, Reduction::id, Reduction::label)); colors_row(ui, d); - choice(ui, d, "dither", "Dither:", &pairs(&Dither::ALL, Dither::id, Dither::label)); + choice(ui, d, "dither", tl!("Dither:"), &pairs(&Dither::ALL, Dither::id, Dither::label)); if Dither::from_id(&d.str("dither")).is_some_and(|x| x != Dither::None) { - slider_row(ui, d, "ditherAmount", "Amount:", "%"); + slider_row(ui, d, "ditherAmount", tl!("Amount:"), "%"); } - check_pair(ui, d, ("transparency", "Transparency"), ("interlaced", "Interlaced")); + check_pair(ui, d, ("transparency", tl!("Transparency")), ("interlaced", tl!("Interlaced"))); matte_row(ui, d); - slider_row(ui, d, "webSnap", "Web Snap:", "%"); + slider_row(ui, d, "webSnap", tl!("Web Snap:"), "%"); if format == WebFormat::Gif { - slider_row(ui, d, "lossy", "Lossy:", "%"); + slider_row(ui, d, "lossy", tl!("Lossy:"), "%"); } } WebFormat::Jpg => { quality_row(ui, d); - check_pair(ui, d, ("progressive", "Progressive"), ("optimized", "Optimized")); - widgets::label_row(ui, "", LABEL_W, |ui| form::check(ui, d, "embedProfile", "ICC Profile")); + check_pair(ui, d, ("progressive", tl!("Progressive")), ("optimized", tl!("Optimized"))); + widgets::label_row(ui, "", LABEL_W, |ui| form::check(ui, d, "embedProfile", tl!("ICC Profile"))); matte_row(ui, d); } WebFormat::Png24 => { - check_pair(ui, d, ("transparency", "Transparency"), ("interlaced", "Interlaced")); + check_pair(ui, d, ("transparency", tl!("Transparency")), ("interlaced", tl!("Interlaced"))); matte_row(ui, d); } } - widgets::label_row(ui, "", LABEL_W, |ui| form::check(ui, d, "convertToSrgb", "Convert to sRGB")); - choice(ui, d, "metadata", "Metadata:", &pairs(&WebMetadata::ALL, WebMetadata::key, WebMetadata::label)); + widgets::label_row(ui, "", LABEL_W, |ui| form::check(ui, d, "convertToSrgb", tl!("Convert to sRGB"))); + choice(ui, d, "metadata", tl!("Metadata:"), &pairs(&WebMetadata::ALL, WebMetadata::key, WebMetadata::label)); if format.palette() { widgets::divider(ui); color_table(ui, d, s); @@ -557,9 +558,9 @@ fn side(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, s: Option<& image_size(app, ui, d, s); widgets::divider(ui); if d.bool("__hasSlices") { - choice(ui, d, "slices", "Slices:", &pairs(&SliceScope::ALL, SliceScope::key, SliceScope::label)); + choice(ui, d, "slices", tl!("Slices:"), &pairs(&SliceScope::ALL, SliceScope::key, SliceScope::label)); } - choice(ui, d, "output", "Output:", &pairs(&WebOutput::ALL, WebOutput::key, WebOutput::label)); + choice(ui, d, "output", tl!("Output:"), &pairs(&WebOutput::ALL, WebOutput::key, WebOutput::label)); }); } @@ -570,22 +571,23 @@ fn presets_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, s: O let current = s.and_then(|s| presets.iter().rposition(|p| p.settings == *s)); let names: Vec<&str> = presets.iter().map(|p| p.name.as_str()).collect(); let mut run: Option<(&str, Value)> = None; - widgets::label_row(ui, "Preset:", LABEL_W, |ui| { - let shown = current.and_then(|i| names.get(i).copied()).unwrap_or("[Unnamed]"); + widgets::label_row(ui, tl!("Preset:"), LABEL_W, |ui| { + let shown = current.and_then(|i| names.get(i).copied()).unwrap_or(tl!("[Unnamed]")); if let Some(i) = widgets::dropdown(ui, "sfw-preset", shown, &names, FIELD_W - 60.0) && let Some(p) = presets.get(i) { d.fields.extend(settings_fields(&p.settings)); } let editing = d.fields.contains_key("__presetName"); - if widgets::icon_button(ui, "save", "Save the settings as a preset", editing, 24.0).clicked() { + if widgets::icon_button(ui, "save", tl!("Save the settings as a preset"), editing, 24.0).clicked() { match editing { true => _ = d.fields.remove("__presetName"), false => _ = d.fields.insert("__presetName".into(), json!("")), } } let user = current.filter(|i| *i + saved >= presets.len()); - let del = ui.add_enabled(user.is_some(), |ui: &mut egui::Ui| widgets::icon_button(ui, "trash-2", "Delete this saved preset", false, 24.0)); + let del = + ui.add_enabled(user.is_some(), |ui: &mut egui::Ui| widgets::icon_button(ui, "trash-2", tl!("Delete this saved preset"), false, 24.0)); if del.clicked() && let Some(name) = user.and_then(|i| names.get(i)) { @@ -593,10 +595,10 @@ fn presets_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, s: O } }); if d.fields.contains_key("__presetName") { - widgets::label_row(ui, "Name:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Name:"), LABEL_W, |ui| { form::text(ui, d, "__presetName", FIELD_W - 70.0); let name = d.str("__presetName"); - if ui.add_enabled(!name.trim().is_empty(), |ui: &mut egui::Ui| widgets::flat_button(ui, "Save", 52.0)).clicked() { + if ui.add_enabled(!name.trim().is_empty(), |ui: &mut egui::Ui| widgets::flat_button(ui, tl!("Save"), 52.0)).clicked() { let mut p = params(d); p["name"] = json!(name.trim()); run = Some(("webExport.presets.save", p)); @@ -635,7 +637,7 @@ fn slider_row(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, suffix: /// The palette size: the usual counts, or any 2–256. fn colors_row(ui: &mut egui::Ui, d: &mut Dialog) { - widgets::label_row(ui, "Colors:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Colors:"), LABEL_W, |ui| { let mut n = d.f64("colors", 128.0).round().clamp(2.0, 256.0) as u16; if let Some(c) = widgets::dropdown(ui, "sfw-colors", &n.to_string(), &COLOR_COUNTS, 80.0).and_then(|i| COLOR_COUNTS.get(i)) { n = c.parse().unwrap_or(n); @@ -651,7 +653,7 @@ fn colors_row(ui: &mut egui::Ui, d: &mut Dialog) { fn quality_row(ui: &mut egui::Ui, d: &mut Dialog) { let q = d.f64("quality", 60.0).round().clamp(0.0, 100.0) as u8; let step = QUALITIES.iter().rev().find(|(_, v)| q >= *v).map_or("Low", |(l, _)| *l); - widgets::label_row(ui, "Quality:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Quality:"), LABEL_W, |ui| { let labels = QUALITIES.map(|(l, _)| l); if let Some((_, v)) = widgets::dropdown(ui, "sfw-quality", step, &labels, FIELD_W).and_then(|i| QUALITIES.get(i)) { d.fields.insert("quality".into(), json!(v)); @@ -664,7 +666,7 @@ fn quality_row(ui: &mut egui::Ui, d: &mut Dialog) { fn matte_row(ui: &mut egui::Ui, d: &mut Dialog) { let matte = d.str("matte"); let at = MATTES.iter().position(|v| matte.eq_ignore_ascii_case(v)).unwrap_or(3); - widgets::label_row(ui, "Matte:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Matte:"), LABEL_W, |ui| { if let Some(i) = widgets::dropdown(ui, "sfw-matte", MATTE_LABELS[at], &MATTE_LABELS, 120.0) { d.fields.insert("matte".into(), json!(MATTES.get(i).copied().unwrap_or("#808080"))); } @@ -683,11 +685,11 @@ fn color_table(ui: &mut egui::Ui, d: &mut Dialog, s: Option<&WebSettings>) { let key = s.map(|s| s.to_json().to_string()); let shot = cache.and_then(|c| c.shots.into_iter().find(|(k, _)| Some(k) == key.as_ref())).and_then(|(_, s)| s.ok()); let Some(shot) = shot else { - widgets::subheader(ui, "Color Table"); - widgets::dim_label(ui, "Shown with the optimised preview."); + widgets::subheader(ui, tl!("Color Table")); + widgets::dim_label(ui, tl!("Shown with the optimised preview.")); return; }; - widgets::subheader(ui, &format!("Color Table ({} colors)", shot.colors.len())); + widgets::subheader(ui, &crate::i18n::tn(shot.colors.len() as u64, "Color Table ({n} color)", "Color Table ({n} colors)")); let selected = d.str("__color"); // (colour shown, source, transparent, locked, web-safe, mapped by hand) let rows: Vec<(TableColor, bool)> = shot @@ -744,9 +746,11 @@ fn color_table(ui: &mut egui::Ui, d: &mut Dialog, s: Option<&WebSettings>) { let on = |list: &[String]| list.iter().any(|h| h.eq_ignore_ascii_case(&selected)); ui.horizontal(|ui| { ui.add_enabled_ui(!selected.is_empty(), |ui| { - for (list, on_label, off_label) in - [("transparent", "Restore", "Transparent"), ("webShift", "Unshift", "Web Shift"), ("locked", "Unlock", "Lock")] - { + for (list, on_label, off_label) in [ + ("transparent", tl!("Restore"), tl!("Transparent")), + ("webShift", tl!("Unshift"), tl!("Web Shift")), + ("locked", tl!("Unlock"), tl!("Lock")), + ] { let current = match list { "transparent" => &table.transparent, "webShift" => &table.web_shift, @@ -760,7 +764,7 @@ fn color_table(ui: &mut egui::Ui, d: &mut Dialog, s: Option<&WebSettings>) { }); }); let sort = table.sort; - widgets::label_row(ui, "Order:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Order:"), LABEL_W, |ui| { let labels = ColorSort::ALL.map(ColorSort::label); if let Some(i) = widgets::dropdown(ui, "sfw-sort", sort.label(), &labels, FIELD_W) && let Some(o) = ColorSort::ALL.get(i) @@ -792,7 +796,7 @@ pub(super) fn set_table(d: &mut Dialog, key: &str, value: Value) { /// Image Size: width and height in pixels (proportional) or a percentage, anti-aliasing, Clip to /// Artboard and which artboard. fn image_size(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, s: Option<&WebSettings>) { - widgets::subheader(ui, "Image Size"); + widgets::subheader(ui, tl!("Image Size")); let Some(st) = app.session.active() else { return }; let region = s.and_then(|s| webexport::region(&st.doc, s).ok()); let (w, h) = region.map_or((0, 0), |(r, scale)| vectorcraft_render::region_pixels(r, scale)); @@ -802,28 +806,28 @@ fn image_size(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, s: Option } d.fields.insert(key.into(), v); }; - widgets::label_row(ui, "W:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("W:"), LABEL_W, |ui| { if let Some(v) = widgets::plain_field(ui, "sfw-w", f64::from(w), " px", 0, 80.0) { set(d, "width", json!(v.round().max(1.0))); } - ui.label("H:"); + ui.label(tl!("H:")); if let Some(v) = widgets::plain_field(ui, "sfw-h", f64::from(h), " px", 0, 80.0) { set(d, "height", json!(v.round().max(1.0))); } }); let percent = region.map_or(100.0, |(_, scale)| (scale * 100.0 * 100.0).round() / 100.0); - widgets::label_row(ui, "Percent:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Percent:"), LABEL_W, |ui| { if let Some(v) = widgets::plain_field(ui, "sfw-pct", percent, "%", 2, 80.0) { set(d, "percent", json!(v)); } }); - form::choice(ui, d, "antiAlias", "Anti-aliasing:", (LABEL_W, FIELD_W), &pairs(&AntiAlias::ALL, AntiAlias::id, AntiAlias::label)); - widgets::label_row(ui, "", LABEL_W, |ui| form::check(ui, d, "clipToArtboard", "Clip to Artboard")); + form::choice(ui, d, "antiAlias", tl!("Anti-aliasing:"), (LABEL_W, FIELD_W), &pairs(&AntiAlias::ALL, AntiAlias::id, AntiAlias::label)); + widgets::label_row(ui, "", LABEL_W, |ui| form::check(ui, d, "clipToArtboard", tl!("Clip to Artboard"))); let boards = &st.doc.artboards; if boards.len() > 1 && d.bool("clipToArtboard") { let names: Vec<&str> = boards.iter().map(|a| a.name.as_str()).collect(); let at = s.and_then(|s| s.artboard).unwrap_or(0).min(names.len() - 1); - widgets::label_row(ui, "Artboard:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Artboard:"), LABEL_W, |ui| { if let Some(i) = widgets::dropdown(ui, "sfw-artboard", names.get(at).copied().unwrap_or_default(), &names, FIELD_W) { d.fields.insert("artboard".into(), json!(i)); } diff --git a/crates/ui-egui/src/dialogs/save_options.rs b/crates/ui-egui/src/dialogs/save_options.rs index deae2061c..9ebdd1ab2 100644 --- a/crates/ui-egui/src/dialogs/save_options.rs +++ b/crates/ui-egui/src/dialogs/save_options.rs @@ -27,13 +27,13 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_wid fn heading(d: &Dialog) -> String { if d.bool("__pick") { return match SaveMode::of(&d.str("__action")) { - Some(SaveMode::Copy) => "Save a Copy", - Some(SaveMode::Template) => "Save as Template", - _ => "Save As", + Some(SaveMode::Copy) => tl!("Save a Copy"), + Some(SaveMode::Template) => tl!("Save as Template"), + _ => tl!("Save As"), } .into(); } - format!("{} Options", fileio::format(&d.str("format")).map_or("", |f| f.label)) + crate::i18n::fmt(tl!("{format} Options"), &[("format", fileio::format(&d.str("format")).map_or("", |f| f.label))]) } /// `f`'s options with the values a save would use (`file.formatOptions`: as last saved in that @@ -74,11 +74,11 @@ fn shown(f: &Format) -> impl Iterator { /// others from their names. fn label(o: &FormatOption) -> String { match o.name { - "separateArtboards" => "Save each artboard to a separate file".into(), - "includeLinked" => "Include Linked Files".into(), - "embedProfiles" => "Embed ICC Profiles".into(), - "pdfCompatible" => "Create PDF-Compatible File".into(), - "compress" => "Use Compression".into(), + "separateArtboards" => tl!("Save each artboard to a separate file").into(), + "includeLinked" => tl!("Include Linked Files").into(), + "embedProfiles" => tl!("Embed ICC Profiles").into(), + "pdfCompatible" => tl!("Create PDF-Compatible File").into(), + "compress" => tl!("Use Compression").into(), _ => form::humanize(o.name), } } @@ -96,7 +96,7 @@ fn artboard_range(ui: &mut egui::Ui, d: &mut Dialog) { ui.horizontal(|ui| { ui.add_space(22.0); let mut all = all_artboards(d); - if ui.radio_value(&mut all, true, "All").changed() | ui.radio_value(&mut all, false, "Range:").changed() { + if ui.radio_value(&mut all, true, tl!("All")).changed() | ui.radio_value(&mut all, false, tl!("Range:")).changed() { d.fields.insert("all".into(), json!(all)); } let mut range = d.str("range"); @@ -121,9 +121,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let mut switch = None; egui::Grid::new("save-options").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { if d.bool("__pick") { - form::field_w(ui, d, "path", "File Name:", 240.0); + form::field_w(ui, d, "path", tl!("File Name:"), 240.0); if SaveMode::of(&d.str("__action")) != Some(SaveMode::Template) { - ui.label(egui::RichText::new("Format:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Format:")).color(t.text_dim)); switch = widgets::dropdown(ui, "save-format", f.label, save_labels(), 254.0).and_then(|i| SAVE_FORMATS.get(i).copied()); ui.end_row(); } diff --git a/crates/ui-egui/src/dialogs/save_pdf.rs b/crates/ui-egui/src/dialogs/save_pdf.rs index b9d5ced52..1c8cb0118 100644 --- a/crates/ui-egui/src/dialogs/save_pdf.rs +++ b/crates/ui-egui/src/dialogs/save_pdf.rs @@ -27,13 +27,13 @@ use crate::{VectorcraftApp, io, widgets}; pub(super) const KIND: &str = "savePdf"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Save PDF".into(), body, confirm, ok: Some("Save PDF"), ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Save PDF").into(), body, confirm, ok: Some("Save PDF"), ..DialogSpec::FORM }; /// The dialog kind of the preset editor (New / Edit in Edit → PDF Presets). pub(super) const PRESET_KIND: &str = "pdfPreset"; pub(super) const PRESET_SPEC: DialogSpec = DialogSpec { - heading: |d| if d.str(EDITING).is_empty() { "New PDF Preset" } else { "Edit PDF Preset" }.into(), + heading: |d| if d.str(EDITING).is_empty() { tl!("New PDF Preset") } else { tl!("Edit PDF Preset") }.into(), body, confirm: confirm_preset, ok: Some("Save Preset"), @@ -304,20 +304,20 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let editor = d.kind == PRESET_KIND; if editor { - row_with(ui, "Preset name:", TOP_LABEL_WIDTH, |ui| { + row_with(ui, tl!("Preset name:"), TOP_LABEL_WIDTH, |ui| { form::text_edit(ui, d, "name", 288.0); }); } else { preset_rows(app, ui, d); } - row_with(ui, "Standard:", TOP_LABEL_WIDTH, |ui| { + row_with(ui, tl!("Standard:"), TOP_LABEL_WIDTH, |ui| { let picked = pick::(ui, d, "standard", 170.0, |_| true); let standard = choice::(d, "standard").unwrap_or_default(); if picked { standard_chosen(d, standard); } ui.add_space(16.0); - ui.label(egui::RichText::new("Compatibility:").color(t.text)); + ui.label(egui::RichText::new(tl!("Compatibility:")).color(t.text)); pick::(ui, d, "compatibility", 110.0, |c| standard.allows(c)); }); ui.add_space(10.0); @@ -327,7 +327,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(14.0); ui.vertical(|ui| { ui.set_width(500.0); - ui.label(egui::RichText::new(section).font(theme::semibold(14.0)).color(t.text_strong)); + ui.label(egui::RichText::new(tl!(section)).font(theme::semibold(14.0)).color(t.text_strong)); egui::ScrollArea::vertical().id_salt(("save-pdf", section)).max_height(360.0).auto_shrink([false, false]).show(ui, |ui| match section { "Compression" => compression(ui, d), "Marks and Bleeds" => marks_and_bleeds(app, ui, d), @@ -363,7 +363,7 @@ pub(super) fn section_list(ui: &mut egui::Ui, d: &mut Dialog, sections: &[&str], ui.spacing_mut().item_spacing.y = 1.0; for s in sections { let sel = *s == section; - let label = egui::RichText::new(*s).size(12.5).color(if sel { t.text_strong } else { t.text }); + let label = egui::RichText::new(tl!(*s)).size(12.5).color(if sel { t.text_strong } else { t.text }); if ui.add(egui::Button::selectable(sel, label).frame_when_inactive(false).min_size(egui::vec2(LIST_WIDTH - 12.0, 24.0))).clicked() { d.fields.insert("__section".into(), json!(s)); } @@ -391,7 +391,7 @@ fn standard_chosen(d: &mut Dialog, standard: Standard) { /// The Preset row (the presets, built-in and saved, and Save Preset…) and, while Save Preset… /// asks for a name, the row taking it. fn preset_rows(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { - row_with(ui, "Preset:", TOP_LABEL_WIDTH, |ui| { + row_with(ui, tl!("Preset:"), TOP_LABEL_WIDTH, |ui| { let presets = pdf::presets(&app.session); let names: Vec<&str> = presets.iter().map(String::as_str).collect(); let chosen = widgets::dropdown(ui, "pdf-preset", &d.str("preset"), &names, 300.0).and_then(|i| names.get(i)).map(|p| p.to_string()); @@ -400,23 +400,23 @@ fn preset_rows(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { } let asking = d.fields.contains_key(SAVE_AS); if ui - .add_enabled_ui(!asking, |ui| widgets::flat_button(ui, "Save Preset…", 96.0)) + .add_enabled_ui(!asking, |ui| widgets::flat_button(ui, tl!("Save Preset…"), 96.0)) .inner - .on_hover_text("Save these settings as a preset") + .on_hover_text(tl!("Save these settings as a preset")) .clicked() { d.fields.insert(SAVE_AS.into(), json!(app.session.new_pdf_preset_name())); } }); if d.fields.contains_key(SAVE_AS) { - row_with(ui, "Save as preset:", TOP_LABEL_WIDTH, |ui| { + row_with(ui, tl!("Save as preset:"), TOP_LABEL_WIDTH, |ui| { form::text_edit(ui, d, SAVE_AS, 200.0); - if widgets::flat_button(ui, "Save", 52.0).clicked() + if widgets::flat_button(ui, tl!("Save"), 52.0).clicked() && let Err(e) = save_preset(app, d) { app.status(e); } - if widgets::flat_button(ui, "Cancel", 60.0).clicked() { + if widgets::flat_button(ui, tl!("Cancel"), 60.0).clicked() { d.fields.remove(SAVE_AS); } }); @@ -436,27 +436,27 @@ fn apply_preset(app: &mut VectorcraftApp, d: &mut Dialog, name: &str) { /// General: the options, then the artboards (Save PDF) or the description (preset editor). fn general(ui: &mut egui::Ui, d: &mut Dialog, editor: bool) { - heading(ui, "Options"); + heading(ui, tl!("Options")); let standard = choice::(d, "standard").unwrap_or_default(); - flag(ui, d, "preserveEditing", "Preserve editing capabilities", standard == Standard::None); - flag(ui, d, "thumbnails", "Embed page thumbnails", true); - flag(ui, d, "fastWebView", "Optimize for fast web view", true); - flag(ui, d, "viewAfterSaving", "View PDF after saving", true); + flag(ui, d, "preserveEditing", tl!("Preserve editing capabilities"), standard == Standard::None); + flag(ui, d, "thumbnails", tl!("Embed page thumbnails"), true); + flag(ui, d, "fastWebView", tl!("Optimize for fast web view"), true); + flag(ui, d, "viewAfterSaving", tl!("View PDF after saving"), true); // PDF layers need PDF 1.5, and PDF/X-1a and PDF/X-3 have none. let layers = standard.allows_layers() && choice::(d, "compatibility").unwrap_or_default().has_layers(); - flag(ui, d, "createLayers", "Create PDF layers from top-level layers", layers); - flag(ui, d, "includeNonPrinting", "Include non-printing layers", true); + flag(ui, d, "createLayers", tl!("Create PDF layers from top-level layers"), layers); + flag(ui, d, "includeNonPrinting", tl!("Include non-printing layers"), true); if editor { - heading(ui, "Description"); + heading(ui, tl!("Description")); if let Some(text) = widgets::text_field(ui, "pdf-preset-description", Some(&d.str("description")), 460.0, 3) { d.fields.insert("description".into(), json!(text)); } return; } - heading(ui, "Artboards"); + heading(ui, tl!("Artboards")); let mut all = d.bool("__allArtboards"); ui.horizontal(|ui| { - let changed = ui.radio_value(&mut all, true, "All").changed() | ui.radio_value(&mut all, false, "Range:").changed(); + let changed = ui.radio_value(&mut all, true, tl!("All")).changed() | ui.radio_value(&mut all, false, tl!("Range:")).changed(); if changed { d.fields.insert("__allArtboards".into(), json!(all)); } @@ -473,19 +473,19 @@ fn image_rows(ui: &mut egui::Ui, d: &mut Dialog, key: &str, title: &str) { let on = get(d, &format!("{base}.downsample")) != Downsample::None.id(); ui.horizontal(|ui| { pick::(ui, d, &format!("{base}.downsample"), 150.0, |_| true); - ui.label("to"); + ui.label(tl!("to")); number(ui, d, &format!("{base}.ppi"), " ppi", on); - ui.label("above"); + ui.label(tl!("above")); number(ui, d, &format!("{base}.abovePpi"), " ppi", on); }); ui.horizontal(|ui| { - ui.label("Compression:"); + ui.label(tl!("Compression:")); if key == "mono" { pick::(ui, d, &format!("{base}.compression"), 150.0, |_| true); } else { pick::(ui, d, &format!("{base}.compression"), 120.0, |_| true); let lossy = get(d, &format!("{base}.compression")).as_str().is_some_and(|c| c != ImageCodec::None.id() && c != ImageCodec::Zip.id()); - ui.label("Quality:"); + ui.label(tl!("Quality:")); ui.add_enabled_ui(lossy, |ui| { pick::(ui, d, &format!("{base}.quality"), 110.0, |_| true); }); @@ -494,11 +494,11 @@ fn image_rows(ui: &mut egui::Ui, d: &mut Dialog, key: &str, title: &str) { } fn compression(ui: &mut egui::Ui, d: &mut Dialog) { - image_rows(ui, d, "color", "Color images"); - image_rows(ui, d, "gray", "Grayscale images"); - image_rows(ui, d, "mono", "Monochrome images"); + image_rows(ui, d, "color", tl!("Color images")); + image_rows(ui, d, "gray", tl!("Grayscale images")); + image_rows(ui, d, "mono", tl!("Monochrome images")); ui.add_space(8.0); - flag(ui, d, "compression.compressText", "Compress text and line art", true); + flag(ui, d, "compression.compressText", tl!("Compress text and line art"), true); } pub(super) fn marks_and_bleeds(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { @@ -509,9 +509,9 @@ pub(super) fn marks_and_bleeds(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut ("marks.colorBars", "Color bars"), ("marks.pageInfo", "Page information"), ]; - heading(ui, "Marks"); + heading(ui, tl!("Marks")); let all = MARKS.iter().all(|(p, _)| get(d, p).as_bool() == Some(true)); - if widgets::check(ui, "All printer's marks", all, true) { + if widgets::check(ui, tl!("All printer's marks"), all, true) { for (p, _) in MARKS { set(d, p, json!(!all)); } @@ -519,15 +519,15 @@ pub(super) fn marks_and_bleeds(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut egui::Grid::new("pdf-marks").num_columns(2).spacing([24.0, 4.0]).show(ui, |ui| { for pair in MARKS.chunks(2) { for (p, label) in pair { - flag(ui, d, p, label, true); + flag(ui, d, p, tl!(label), true); } ui.end_row(); } }); - row(ui, "Printer mark type:", |ui| { + row(ui, tl!("Printer mark type:"), |ui| { pick::(ui, d, "marks.kind", 130.0, |_| true); }); - row(ui, "Trim mark weight:", |ui| { + row(ui, tl!("Trim mark weight:"), |ui| { let w = get(d, "marks.weight").as_f64().unwrap_or(0.25); let current = WEIGHTS.iter().zip(WEIGHT_LABELS).find(|(x, _)| (*x - w).abs() < 1e-9).map_or_else(|| format!("{w} pt"), |(_, l)| l.to_string()); @@ -535,13 +535,16 @@ pub(super) fn marks_and_bleeds(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut set(d, "marks.weight", json!(x)); } }); - row(ui, "Offset:", |ui| length(ui, d, "marks.offset", unit, true)); - heading(ui, "Bleeds"); - flag(ui, d, "bleed.useDocument", "Use document bleed settings", true); + row(ui, tl!("Offset:"), |ui| length(ui, d, "marks.offset", unit, true)); + heading(ui, tl!("Bleeds")); + flag(ui, d, "bleed.useDocument", tl!("Use document bleed settings"), true); // With the document's bleed the fields show it (Document Setup's `[top, bottom, left, right]`). let document = (get(d, "bleed.useDocument").as_bool() == Some(true)).then(|| app.session.active().map_or([0.0; 4], |s| s.doc.setup.bleed)); egui::Grid::new("pdf-bleed").num_columns(4).spacing([10.0, 6.0]).show(ui, |ui| { - for pair in [[(0, "bleed.top", "Top:"), (1, "bleed.bottom", "Bottom:")], [(2, "bleed.left", "Left:"), (3, "bleed.right", "Right:")]] { + for pair in [ + [(0, "bleed.top", tl!("Top:")), (1, "bleed.bottom", tl!("Bottom:"))], + [(2, "bleed.left", tl!("Left:")), (3, "bleed.right", tl!("Right:"))], + ] { for (i, p, label) in pair { ui.label(label); match document { @@ -572,56 +575,56 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog) { let profiles = vectorcraft_color::cms::profiles(); let names = |blank: &'static str| -> Vec<&str> { std::iter::once(blank).chain(profiles.iter().map(|p| p.name.as_str())).collect() }; let standard = choice::(d, "standard").unwrap_or_default(); - heading(ui, "Color"); - row(ui, "Color conversion:", |ui| { + heading(ui, tl!("Color")); + row(ui, tl!("Color conversion:"), |ui| { pick::(ui, d, "output.conversion", 300.0, |_| true); }); // PDF/X-1a converts to CMYK even without a conversion. let converting = get(d, "output.conversion") != ColorConversion::None.id() || standard.cmyk_only(); - row(ui, "Destination:", |ui| profile_pick(ui, d, "output.destination", &names(DOCUMENT_PROFILE), converting)); - row(ui, "Profile inclusion:", |ui| { + row(ui, tl!("Destination:"), |ui| profile_pick(ui, d, "output.destination", &names(DOCUMENT_PROFILE), converting)); + row(ui, tl!("Profile inclusion:"), |ui| { // A standard decides whether colours are tagged. ui.add_enabled_ui(standard == Standard::None, |ui| pick::(ui, d, "output.profiles", 300.0, |_| true)); }); if standard.cmyk_only() { - note(ui, "PDF/X-1a files are CMYK: every colour is converted to the destination (blank: the output intent's CMYK profile), untagged."); + note(ui, tl!("PDF/X-1a files are CMYK: every colour is converted to the destination (blank: the output intent's CMYK profile), untagged.")); } else if standard != Standard::None { - note(ui, "Files of this standard tag their colours with ICC profiles."); + note(ui, tl!("Files of this standard tag their colours with ICC profiles.")); } - heading(ui, "Output Intent"); + heading(ui, tl!("Output Intent")); // PDF/A files carry their own output intent; PDF/X files always have one. let own = standard != Standard::PdfA2b; let blank = if standard.is_pdfx() { PDFX_INTENT } else { NO_PROFILE }; - row(ui, "Output intent profile:", |ui| profile_pick(ui, d, "output.outputIntent", &names(blank), own)); + row(ui, tl!("Output intent profile:"), |ui| profile_pick(ui, d, "output.outputIntent", &names(blank), own)); for (p, label) in [ - ("output.outputCondition", "Output condition:"), - ("output.outputConditionId", "Condition identifier:"), - ("output.registry", "Registry name:"), + ("output.outputCondition", tl!("Output condition:")), + ("output.outputConditionId", tl!("Condition identifier:")), + ("output.registry", tl!("Registry name:")), ] { row(ui, label, |ui| text(ui, d, p, own)); } - flag(ui, d, "output.trapped", "Mark as trapped", own); + flag(ui, d, "output.trapped", tl!("Mark as trapped"), own); if !own { - note(ui, "PDF/A files carry their own output intent."); + note(ui, tl!("PDF/A files carry their own output intent.")); } } fn advanced(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { - heading(ui, "Fonts"); + heading(ui, tl!("Fonts")); let outline = get(d, "advanced.outlineText").as_bool() == Some(true); - row(ui, "Subset fonts below:", |ui| { + row(ui, tl!("Subset fonts below:"), |ui| { number(ui, d, "advanced.fontSubsetPercent", "%", !outline); - ui.label("of characters used"); + ui.label(tl!("of characters used")); }); - flag(ui, d, "advanced.outlineText", "Convert text to outlines", true); - heading(ui, "Overprint and Transparency Flattener"); - row(ui, "Overprint:", |ui| { + flag(ui, d, "advanced.outlineText", tl!("Convert text to outlines"), true); + heading(ui, tl!("Overprint and Transparency Flattener")); + row(ui, tl!("Overprint:"), |ui| { pick::(ui, d, "advanced.overprint", 130.0, |_| true); }); // PDF 1.3 files (PDF/X-1a and PDF/X-3 ones too) have no transparency: it is flattened. let flat = choice::(d, "compatibility") == Some(Compatibility::Pdf13) || choice::(d, "standard").is_some_and(Standard::flattens); - row(ui, "Flattener preset:", |ui| { + row(ui, tl!("Flattener preset:"), |ui| { let presets = app.session.flattener_presets(); let names: Vec<&str> = presets.iter().map(|p| p.name.as_str()).collect(); let name = get(d, "flattenerPreset").as_str().map(str::trim).filter(|n| !n.is_empty()).unwrap_or(pdf::DEFAULT_FLATTENER); @@ -634,7 +637,7 @@ fn advanced(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { }); }); if !flat { - note(ui, "PDF 1.3 files have no transparency: the preset flattens it."); + note(ui, tl!("PDF 1.3 files have no transparency: the preset flattens it.")); } } @@ -665,24 +668,24 @@ fn password_field(ui: &mut egui::Ui, d: &mut Dialog, path: &str, enabled: bool) fn security(ui: &mut egui::Ui, d: &mut Dialog) { let plain = choice::(d, "standard").unwrap_or_default() == Standard::None; if !plain { - note(ui, "Files of a PDF standard can't be password-protected."); + note(ui, tl!("Files of a PDF standard can't be password-protected.")); } - heading(ui, "Document open password"); - let open = password_check(ui, d, OPEN_PASSWORD, "__requireOpenPassword", "Require a password to open the document", plain); - row(ui, "Open password:", |ui| password_field(ui, d, OPEN_PASSWORD, open)); - heading(ui, "Permissions"); - let restrict = password_check(ui, d, PERMISSIONS_PASSWORD, "__restrictPermissions", "Restrict printing, editing and other tasks", plain); - row(ui, "Permissions password:", |ui| password_field(ui, d, PERMISSIONS_PASSWORD, restrict)); + heading(ui, tl!("Document open password")); + let open = password_check(ui, d, OPEN_PASSWORD, "__requireOpenPassword", tl!("Require a password to open the document"), plain); + row(ui, tl!("Open password:"), |ui| password_field(ui, d, OPEN_PASSWORD, open)); + heading(ui, tl!("Permissions")); + let restrict = password_check(ui, d, PERMISSIONS_PASSWORD, "__restrictPermissions", tl!("Restrict printing, editing and other tasks"), plain); + row(ui, tl!("Permissions password:"), |ui| password_field(ui, d, PERMISSIONS_PASSWORD, restrict)); ui.add_enabled_ui(restrict, |ui| { - row(ui, "Printing allowed:", |ui| { + row(ui, tl!("Printing allowed:"), |ui| { pick::(ui, d, "security.printing", 260.0, |_| true); }); - row(ui, "Changes allowed:", |ui| { + row(ui, tl!("Changes allowed:"), |ui| { pick::(ui, d, "security.changes", 260.0, |_| true); }); }); - flag(ui, d, "security.copy", "Enable copying of text, images and other content", restrict); - let reader = "Enable text access for screen readers"; + flag(ui, d, "security.copy", tl!("Enable copying of text, images and other content"), restrict); + let reader = tl!("Enable text access for screen readers"); if get(d, "security.copy").as_bool() == Some(true) { // What can be copied can be read out. widgets::check(ui, reader, true, false); @@ -694,11 +697,11 @@ fn security(ui: &mut egui::Ui, d: &mut Dialog) { ui, d, "security.plaintextMetadata", - "Enable plaintext metadata", + tl!("Enable plaintext metadata"), (open || restrict) && !matches!(compatibility, Compatibility::Pdf13 | Compatibility::Pdf14), ); ui.add_space(6.0); - note(ui, &format!("Encryption: {} (set by Compatibility)", Encryption::for_compatibility(compatibility).label())); + note(ui, &crate::i18n::fmt(tl!("Encryption: {name} (set by Compatibility)"), &[("name", Encryption::for_compatibility(compatibility).label())])); } /// The section a changed option belongs to (for the Summary's order). @@ -745,20 +748,20 @@ fn summary(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let v = match summary_of(app, ui.ctx(), ¶ms(d)) { Ok(v) => v, Err(e) => { - heading(ui, "Error"); + heading(ui, tl!("Error")); ui.label(egui::RichText::new(format!("⚠ {e}")).color(t.text)); return; } }; - heading(ui, "Options"); + heading(ui, tl!("Options")); let mut changed: Vec<&Value> = v["changed"].as_array().map(|a| a.iter().collect()).unwrap_or_default(); changed.sort_by_key(|c| section_of(c["option"].as_str().unwrap_or_default())); if changed.is_empty() { - let base = if d.kind == PRESET_KIND { pdf::DEFAULT_PRESET } else { "the preset" }; - note(ui, &format!("Every option matches {base}.")); + let base = if d.kind == PRESET_KIND { pdf::DEFAULT_PRESET } else { tl!("the preset") }; + note(ui, &crate::i18n::fmt(tl!("Every option matches {base}."), &[("base", base)])); } option_rows(ui, changed, option_label); - heading(ui, "Warnings"); + heading(ui, tl!("Warnings")); warning_rows(ui, v["warnings"].as_array().map(Vec::as_slice).unwrap_or_default()); } @@ -767,7 +770,7 @@ pub(super) fn option_rows<'a>(ui: &mut egui::Ui, changed: impl IntoIterator if *b { "On" } else { "Off" }.to_string(), + Value::Bool(b) => if *b { tl!("On") } else { tl!("Off") }.to_string(), Value::String(s) => s.clone(), other => other.to_string(), }; @@ -779,7 +782,7 @@ pub(super) fn option_rows<'a>(ui: &mut egui::Ui, changed: impl IntoIterator) -> Result bool { grid(ui, |ui| { name_row(ui, d); - destination(ui, d, "Selected Styles Only"); + destination(ui, d, tl!("Selected Styles Only")); }); false } diff --git a/crates/ui-egui/src/dialogs/save_swatch_library.rs b/crates/ui-egui/src/dialogs/save_swatch_library.rs index 5d599e789..f6fca6af4 100644 --- a/crates/ui-egui/src/dialogs/save_swatch_library.rs +++ b/crates/ui-egui/src/dialogs/save_swatch_library.rs @@ -20,7 +20,8 @@ use crate::{VectorcraftApp, widgets}; /// The dialog kind of Save Swatch Library. pub const KIND: &str = "saveSwatchLibrary"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Save Swatch Library".into(), body, confirm, min_width: 360.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Save Swatch Library").into(), body, confirm, min_width: 360.0, ..DialogSpec::FORM }; /// Open Save Swatch Library for the document's swatches (`names`: the ones selected in the panel). pub fn open(app: &mut VectorcraftApp, names: Vec) -> Result { @@ -51,22 +52,22 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let labels = PaletteFormat::ALL.map(PaletteFormat::label); grid(ui, |ui| { name_row(ui, d); - label(ui, "Format:"); + label(ui, tl!("Format:")); if let Some(i) = widgets::dropdown(ui, "library-format", format_of(d).label(), &labels, 230.0) { set(d, "format", json!(PaletteFormat::ALL[i].id())); } ui.end_row(); - destination(ui, d, "Selected Swatches Only"); + destination(ui, d, tl!("Selected Swatches Only")); }); if format_of(d) == PaletteFormat::Gpl { - widgets::dim_label(ui, "GPL palettes keep solid colours only, as RGB."); + widgets::dim_label(ui, tl!("GPL palettes keep solid colours only, as RGB.")); } false } /// The Name row of a Save … Library dialog's [`grid`]. pub(super) fn name_row(ui: &mut egui::Ui, d: &mut Dialog) { - label(ui, "Name:"); + label(ui, tl!("Name:")); form::text(ui, d, "name", 220.0); ui.end_row(); } @@ -75,14 +76,14 @@ pub(super) fn name_row(ui: &mut egui::Ui, d: &mut Dialog) { /// one) or a file, and `only` (Selected Swatches Only) with the number of selected items. pub(super) fn destination(ui: &mut egui::Ui, d: &mut Dialog, only: &str) { let names = d.fields.get("names").and_then(Value::as_array).map_or(0, Vec::len); - label(ui, "Save To:"); + label(ui, tl!("Save To:")); let (user, has_user) = (d.bool("user"), d.bool("__user")); - if widgets::radio(ui, "User Defined Libraries", user, has_user) { + if widgets::radio(ui, tl!("User Defined Libraries"), user, has_user) { set(d, "user", json!(true)); } ui.end_row(); ui.label(""); - if widgets::radio(ui, "A File…", !user, true) { + if widgets::radio(ui, tl!("A File…"), !user, true) { set(d, "user", json!(false)); } ui.end_row(); diff --git a/crates/ui-egui/src/dialogs/shapes.rs b/crates/ui-egui/src/dialogs/shapes.rs index 75a8a4f75..aa57f9770 100644 --- a/crates/ui-egui/src/dialogs/shapes.rs +++ b/crates/ui-egui/src/dialogs/shapes.rs @@ -11,12 +11,12 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(&d.kind).int fn title(kind: &str) -> &'static str { match kind { - "rectangle" => "Rectangle", - "roundedRectangle" => "Rounded Rectangle", - "ellipse" => "Ellipse", - "polygon" => "Polygon", - "star" => "Star", - _ => "Line Segment Tool Options", + "rectangle" => tl!("Rectangle"), + "roundedRectangle" => tl!("Rounded Rectangle"), + "ellipse" => tl!("Ellipse"), + "polygon" => tl!("Polygon"), + "star" => tl!("Star"), + _ => tl!("Line Segment Tool Options"), } } diff --git a/crates/ui-egui/src/dialogs/slices.rs b/crates/ui-egui/src/dialogs/slices.rs index 524ca5b91..da45a477f 100644 --- a/crates/ui-egui/src/dialogs/slices.rs +++ b/crates/ui-egui/src/dialogs/slices.rs @@ -23,10 +23,10 @@ pub const OPTIONS: &str = "sliceOptions"; pub const DIVIDE: &str = "divideSlices"; pub(super) const OPTIONS_SPEC: DialogSpec = - DialogSpec { heading: |_| "Slice Options".into(), body: options_body, confirm: options_confirm, min_width: 380.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Slice Options").into(), body: options_body, confirm: options_confirm, min_width: 380.0, ..DialogSpec::FORM }; pub(super) const DIVIDE_SPEC: DialogSpec = - DialogSpec { heading: |_| "Divide Slices".into(), body: divide_body, confirm: divide_confirm, min_width: 320.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Divide Slices").into(), body: divide_body, confirm: divide_confirm, min_width: 320.0, ..DialogSpec::FORM }; /// Label column and field widths of the dialogs. const LABEL_W: f32 = 110.0; @@ -61,29 +61,31 @@ fn pairs(all: &[T], key: fn(T) -> &'static str, label: fn(T) -> &'stati fn options_body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let kinds: Vec<_> = SliceKind::ALL.iter().filter(|k| **k != SliceKind::HtmlText || d.bool("__html")).copied().collect(); - form::choice(ui, d, "kind", "Slice Type:", (LABEL_W, FIELD_W), &pairs(&kinds, SliceKind::key, SliceKind::label)); + form::choice(ui, d, "kind", tl!("Slice Type:"), (LABEL_W, FIELD_W), &pairs(&kinds, SliceKind::key, SliceKind::label)); ui.add_space(6.0); let kind = SliceKind::parse(&d.str("kind")).unwrap_or_default(); match kind { SliceKind::Image => { - for (key, label) in [("name", "Name:"), ("url", "URL:"), ("target", "Target:"), ("message", "Message:"), ("alt", "Alt:")] { + for (key, label) in + [("name", tl!("Name:")), ("url", tl!("URL:")), ("target", tl!("Target:")), ("message", tl!("Message:")), ("alt", tl!("Alt:"))] + { widgets::label_row(ui, label, LABEL_W, |ui| { form::text(ui, d, key, FIELD_W - 12.0); }); } } SliceKind::NoImage => { - widgets::dim_label(ui, "Text Displayed in Cell:"); + widgets::dim_label(ui, tl!("Text Displayed in Cell:")); form::text_area(ui, d, "text", LABEL_W + FIELD_W - 12.0, 4); } SliceKind::HtmlText => { - widgets::dim_label(ui, "The cell shows the type object's text as HTML."); + widgets::dim_label(ui, tl!("The cell shows the type object's text as HTML.")); } } if kind != SliceKind::Image { ui.add_space(6.0); - form::choice(ui, d, "hAlign", "Horizontal:", (LABEL_W, FIELD_W), &pairs(CellAlign::ALL, CellAlign::key, CellAlign::label)); - form::choice(ui, d, "vAlign", "Vertical:", (LABEL_W, FIELD_W), &pairs(CellVAlign::ALL, CellVAlign::key, CellVAlign::label)); + form::choice(ui, d, "hAlign", tl!("Horizontal:"), (LABEL_W, FIELD_W), &pairs(CellAlign::ALL, CellAlign::key, CellAlign::label)); + form::choice(ui, d, "vAlign", tl!("Vertical:"), (LABEL_W, FIELD_W), &pairs(CellVAlign::ALL, CellVAlign::key, CellVAlign::label)); } ui.add_space(6.0); background_row(ui, d); @@ -101,7 +103,7 @@ fn background_row(ui: &mut egui::Ui, d: &mut Dialog) { "#000000" => 3, _ => 4, }; - widgets::label_row(ui, "Background:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Background:"), LABEL_W, |ui| { if let Some(i) = widgets::dropdown(ui, "slice-background", NAMES[shown], &NAMES, 120.0) { let v = match i { 0 => "", @@ -152,16 +154,22 @@ fn divide_section(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, keys: d.fields.insert(mode.into(), json!("size")); } form::length(ui, d, size, app.session.general_unit(), 70.0); - widgets::dim_label(ui, "per slice"); + widgets::dim_label(ui, tl!("per slice")); }); }); }); } fn divide_body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { - divide_section(app, ui, d, ["divideRows", "rows", "rowMode", "rowHeight"], ["Divide Horizontally Into", "slices down, evenly spaced"]); + divide_section(app, ui, d, ["divideRows", "rows", "rowMode", "rowHeight"], [tl!("Divide Horizontally Into"), tl!("slices down, evenly spaced")]); ui.add_space(8.0); - divide_section(app, ui, d, ["divideColumns", "columns", "columnMode", "columnWidth"], ["Divide Vertically Into", "slices across, evenly spaced"]); + divide_section( + app, + ui, + d, + ["divideColumns", "columns", "columnMode", "columnWidth"], + [tl!("Divide Vertically Into"), tl!("slices across, evenly spaced")], + ); false } diff --git a/crates/ui-egui/src/dialogs/spot_colors.rs b/crates/ui-egui/src/dialogs/spot_colors.rs index d724ebed9..9ab6c2993 100644 --- a/crates/ui-egui/src/dialogs/spot_colors.rs +++ b/crates/ui-egui/src/dialogs/spot_colors.rs @@ -13,7 +13,7 @@ use crate::{VectorcraftApp, widgets}; /// The dialog kind of Spot Colors. pub const KIND: &str = "spotColors"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Spot Colors".into(), body, confirm, min_width: 360.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Spot Colors").into(), body, confirm, min_width: 360.0, ..DialogSpec::FORM }; /// The choices: (`useLab`, label, what it does). const CHOICES: [(bool, &str, &str); 2] = [ @@ -33,15 +33,15 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { grid(ui, |ui| { for (i, (value, text, help)) in CHOICES.into_iter().enumerate() { if i == 0 { - label(ui, "Lab Spot Colors:"); + label(ui, tl!("Lab Spot Colors:")); } else { ui.label(""); } ui.vertical(|ui| { - if widgets::radio(ui, text, use_lab == value, true) { + if widgets::radio(ui, tl!(text), use_lab == value, true) { d.fields.insert("useLab".into(), json!(value)); } - ui.indent(text, |ui| label(ui, help)); + ui.indent(text, |ui| label(ui, tl!(help))); }); ui.end_row(); } diff --git a/crates/ui-egui/src/dialogs/svg_options.rs b/crates/ui-egui/src/dialogs/svg_options.rs index 4a6e5d4bd..d88b0cbc8 100644 --- a/crates/ui-egui/src/dialogs/svg_options.rs +++ b/crates/ui-egui/src/dialogs/svg_options.rs @@ -12,8 +12,15 @@ use crate::state::Dialog; use crate::theme::{self, Tokens}; use crate::{VectorcraftApp, widgets}; -pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "SVG Options".into(), body, confirm, ok: Some("OK"), min_width: 380.0, max_width: Some(560.0), ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("SVG Options").into(), + body, + confirm, + ok: Some("OK"), + min_width: 380.0, + max_width: Some(560.0), + ..DialogSpec::FORM +}; /// `Dialog::kind` of this dialog. pub const KIND: &str = "svgOptions"; @@ -166,11 +173,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { if Mode::of(d) == Mode::Export { artboards(app, ui, d); } else { - check(ui, d, "preserveEditing", "Preserve Editing Capabilities", true); + check(ui, d, "preserveEditing", tl!("Preserve Editing Capabilities"), true); } ui.add_space(10.0); let show = d.bool("showCode"); - if widgets::secondary_button(ui, if show { "Hide Code" } else { "Show Code" }).clicked() { + if widgets::secondary_button(ui, if show { tl!("Hide Code") } else { tl!("Show Code") }).clicked() { d.fields.insert("showCode".into(), json!(!show)); } if show { @@ -188,47 +195,47 @@ pub(super) fn option_fields(ui: &mut egui::Ui, d: &mut Dialog, screens: bool) { // SVG Tiny has presentation attributes alone and no web fonts. let tiny = d.str("profile") == "tiny12"; egui::Grid::new("svg-options").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { - choice(ui, d, "profile", "SVG Profile:", &PROFILES, None); - choice(ui, d, "styling", "Styling:", &STYLING, tiny.then_some("presentation")); - choice(ui, d, "outlineText", "Font:", &FONTS, None); + choice(ui, d, "profile", tl!("SVG Profile:"), &PROFILES, None); + choice(ui, d, "styling", tl!("Styling:"), &STYLING, tiny.then_some("presentation")); + choice(ui, d, "outlineText", tl!("Font:"), &FONTS, None); if !screens { - choice(ui, d, "images", "Images:", &IMAGES, None); + choice(ui, d, "images", tl!("Images:"), &IMAGES, None); } - choice(ui, d, "objectIds", "Object IDs:", &OBJECT_IDS, None); - ui.label(egui::RichText::new("Decimal:").color(t.text_dim)); + choice(ui, d, "objectIds", tl!("Object IDs:"), &OBJECT_IDS, None); + ui.label(egui::RichText::new(tl!("Decimal:")).color(t.text_dim)); let decimals = d.f64("decimals", 3.0); if let Some(v) = widgets::spin_plain(ui, "svg-decimals", decimals, "", 0, 70.0, 1.0, 1.0, &[]) { let (lo, hi) = (*vectorcraft_svg::DECIMALS.start() as f64, *vectorcraft_svg::DECIMALS.end() as f64); d.fields.insert("decimals".into(), json!(v.round().clamp(lo, hi) as u8)); } ui.end_row(); - choice(ui, d, "encoding", "Encoding:", &ENCODINGS, None); + choice(ui, d, "encoding", tl!("Encoding:"), &ENCODINGS, None); }); ui.add_space(6.0); ui.horizontal(|ui| { - check(ui, d, "minify", "Minify", true); + check(ui, d, "minify", tl!("Minify"), true); ui.add_space(12.0); - check(ui, d, "responsive", "Responsive", true); + check(ui, d, "responsive", tl!("Responsive"), true); }); - check(ui, d, "embedFonts", "Embed Fonts (Glyphs Used)", !d.bool("outlineText") && !tiny); - check(ui, d, "fewerTspans", "Fewer Elements", !d.bool("outlineText")); - check(ui, d, "metadata", "Include Metadata", true); + check(ui, d, "embedFonts", tl!("Embed Fonts (Glyphs Used)"), !d.bool("outlineText") && !tiny); + check(ui, d, "fewerTspans", tl!("Fewer Elements"), !d.bool("outlineText")); + check(ui, d, "metadata", tl!("Include Metadata"), true); } /// Use Artboards: all of them or a range; off = the bounds of all art. fn artboards(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let count = app.session.active().map_or(0, |st| st.doc.artboards.len()); ui.add_space(4.0); - check(ui, d, "useArtboards", "Use Artboards", count > 0); + check(ui, d, "useArtboards", tl!("Use Artboards"), count > 0); let on = d.bool("useArtboards") && count > 0; ui.add_enabled_ui(on, |ui| { ui.horizontal(|ui| { ui.add_space(22.0); let all = d.bool("allArtboards"); - if ui.radio(all, "All").clicked() { + if ui.radio(all, tl!("All")).clicked() { d.fields.insert("allArtboards".into(), json!(true)); } - if ui.radio(!all, "Range:").clicked() { + if ui.radio(!all, tl!("Range:")).clicked() { d.fields.insert("allArtboards".into(), json!(false)); } let mut range = d.str("range"); diff --git a/crates/ui-egui/src/dialogs/swatch_conflict.rs b/crates/ui-egui/src/dialogs/swatch_conflict.rs index f7c353418..48e90e5d4 100644 --- a/crates/ui-egui/src/dialogs/swatch_conflict.rs +++ b/crates/ui-egui/src/dialogs/swatch_conflict.rs @@ -20,7 +20,7 @@ use crate::{VectorcraftApp, widgets}; pub const KIND: &str = "swatchConflict"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Swatch Conflict".into(), body, confirm, min_width: 380.0, max_width: Some(440.0), ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Swatch Conflict").into(), body, confirm, min_width: 380.0, max_width: Some(440.0), ..DialogSpec::FORM }; /// The choices: (`choice` value, label, what it does). const CHOICES: [(&str, &str, &str); 2] = [ @@ -67,33 +67,33 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let i = index(d); let (count, Some(c)) = (conflicts(d).len(), conflicts(d).get(i).cloned()) else { return true }; let name = c["name"].as_str().unwrap_or_default(); - ui.label(format!("This document already has a swatch named \u{201c}{name}\u{201d} with another color.")); + ui.label(crate::i18n::fmt(tl!("This document already has a swatch named “{name}” with another color."), &[("name", name)])); if count > 1 { - label(ui, &format!("Conflict {} of {count}", i + 1)); + label(ui, &crate::i18n::fmt(tl!("Conflict {n} of {count}"), &[("n", &(i + 1).to_string()), ("count", &count.to_string())])); } ui.add_space(10.0); ui.horizontal(|ui| { - chip(ui, c["document"].as_str(), "Document"); + chip(ui, c["document"].as_str(), tl!("Document")); ui.add_space(12.0); - chip(ui, c["clipboard"].as_str(), "Pasted"); + chip(ui, c["clipboard"].as_str(), tl!("Pasted")); }); ui.add_space(10.0); let choice = d.str("choice"); grid(ui, |ui| { for (k, (value, text, help)) in CHOICES.into_iter().enumerate() { - label(ui, if k == 0 { "Options:" } else { "" }); + label(ui, if k == 0 { tl!("Options:") } else { "" }); ui.vertical(|ui| { - if widgets::radio(ui, text, choice == value, true) { + if widgets::radio(ui, tl!(text), choice == value, true) { d.fields.insert("choice".into(), json!(value)); } - ui.indent(text, |ui| label(ui, help)); + ui.indent(text, |ui| label(ui, tl!(help))); }); ui.end_row(); } }); ui.add_space(6.0); let all = d.bool("applyToAll"); - if widgets::check(ui, "Apply to All", all, count > 1) { + if widgets::check(ui, tl!("Apply to All"), all, count > 1) { d.fields.insert("applyToAll".into(), json!(!all)); } false diff --git a/crates/ui-egui/src/dialogs/swatch_options.rs b/crates/ui-egui/src/dialogs/swatch_options.rs index d8696364d..79413bf6f 100644 --- a/crates/ui-egui/src/dialogs/swatch_options.rs +++ b/crates/ui-egui/src/dialogs/swatch_options.rs @@ -23,7 +23,7 @@ use crate::widgets; pub const KIND: &str = "swatchOptions"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Swatch Options".into(), body, confirm, preview: true, min_width: 340.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Swatch Options").into(), body, confirm, preview: true, min_width: 340.0, ..DialogSpec::FORM }; /// Colour modes, in menu order, with their `mode` field / `swatch.edit` ids. const MODES: [(Mode, &str); 6] = @@ -117,7 +117,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }; ui.add_space(8.0); let mut pv = d.bool("preview"); - let pv_changed = widgets::check(ui, "Preview", pv, true); + let pv_changed = widgets::check(ui, tl!("Preview"), pv, true); if pv_changed { pv = !pv; d.fields.insert("preview".into(), json!(pv)); @@ -151,7 +151,7 @@ pub(super) fn label(ui: &mut egui::Ui, s: &str) { /// The Swatch Name row of the grid. Returns true when it changed. pub(super) fn name_row(ui: &mut egui::Ui, d: &mut Dialog) -> bool { - label(ui, "Swatch Name:"); + label(ui, tl!("Swatch Name:")); let changed = form::text(ui, d, "name", 190.0); ui.end_row(); changed @@ -163,7 +163,7 @@ pub(super) fn editor(ui: &mut egui::Ui, d: &mut Dialog) -> bool { let mut changed = false; grid(ui, |ui| { changed |= name_row(ui, d); - label(ui, "Color Type:"); + label(ui, tl!("Color Type:")); let spot = d.bool("spot"); if let Some(i) = widgets::dropdown(ui, "swatch-type", TYPES[usize::from(spot)], &TYPES, 200.0) { d.fields.insert("spot".into(), json!(i == 1)); @@ -173,12 +173,12 @@ pub(super) fn editor(ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.label(""); // Spot colours are always global. let global = spot || d.bool("global"); - if widgets::check(ui, "Global", global, !spot) { + if widgets::check(ui, tl!("Global"), global, !spot) { d.fields.insert("global".into(), json!(!global)); changed = true; } ui.end_row(); - label(ui, "Color Mode:"); + label(ui, tl!("Color Mode:")); let labels = MODES.map(|m| m.0.label()); if let Some(i) = widgets::dropdown(ui, "swatch-mode", mode_of(d).label(), &labels, 200.0) { let m = MODES[i].0; diff --git a/crates/ui-egui/src/dialogs/text_export.rs b/crates/ui-egui/src/dialogs/text_export.rs index 748942c7f..6701fd65d 100644 --- a/crates/ui-egui/src/dialogs/text_export.rs +++ b/crates/ui-egui/src/dialogs/text_export.rs @@ -12,7 +12,7 @@ use crate::{VectorcraftApp, io}; pub(crate) const KIND: &str = "txtOptions"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Text Export Options".into(), body, confirm, ok: Some("Export"), min_width: 340.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Text Export Options").into(), body, confirm, ok: Some("Export"), min_width: 340.0, ..DialogSpec::FORM }; const ENCODINGS: [&str; 2] = ["utf8", "utf16"]; const ENCODING_LABELS: [&str; 2] = ["UTF-8", "UTF-16 (Unicode)"]; @@ -31,16 +31,16 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let label = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(text).color(t.text_dim)); egui::Grid::new("text-export").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| { - label(ui, "Encoding:"); + label(ui, tl!("Encoding:")); choice(ui, d, "encoding", &ENCODINGS, &ENCODING_LABELS); ui.end_row(); - label(ui, "Line Endings:"); + label(ui, tl!("Line Endings:")); choice(ui, d, "lineEndings", &LINE_ENDINGS, &LINE_ENDING_LABELS); ui.end_row(); }); ui.add_space(8.0); let selected = app.session.active().is_some_and(|st| !st.selection.objects.is_empty()); - ui.add_enabled_ui(selected, |ui| form::check(ui, d, "selectionOnly", "Selection Only")); + ui.add_enabled_ui(selected, |ui| form::check(ui, d, "selectionOnly", tl!("Selection Only"))); false } diff --git a/crates/ui-egui/src/dialogs/text_import.rs b/crates/ui-egui/src/dialogs/text_import.rs index e4f159b08..2ac09e349 100644 --- a/crates/ui-egui/src/dialogs/text_import.rs +++ b/crates/ui-egui/src/dialogs/text_import.rs @@ -15,7 +15,8 @@ use crate::{VectorcraftApp, widgets}; /// The dialog kind of Text Import Options. pub const KIND: &str = "textImport"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Text Import Options".into(), body, confirm, min_width: 340.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Text Import Options").into(), body, confirm, min_width: 340.0, ..DialogSpec::FORM }; /// The label column of the Encoding rows. const LABEL: f32 = 104.0; @@ -42,13 +43,13 @@ pub fn open(app: &mut VectorcraftApp, params: &Value) -> Value { } fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { - widgets::subheader(ui, "Encoding"); - form::choice(ui, d, "platform", "Platform:", (LABEL, 180.0), &[("windows", "Windows"), ("mac", "Mac")]); - form::choice(ui, d, "characterSet", "Character Set:", (LABEL, 180.0), &[("unicode", "Unicode"), ("ansi", "ANSI")]); + widgets::subheader(ui, tl!("Encoding")); + form::choice(ui, d, "platform", tl!("Platform:"), (LABEL, 180.0), &[("windows", "Windows"), ("mac", "Mac")]); + form::choice(ui, d, "characterSet", tl!("Character Set:"), (LABEL, 180.0), &[("unicode", "Unicode"), ("ansi", "ANSI")]); ui.add_space(10.0); - widgets::subheader(ui, "Line Breaks"); + widgets::subheader(ui, tl!("Line Breaks")); for (key, label) in - [("removeLineReturns", "Join the lines of each paragraph"), ("removeParagraphReturns", "Remove blank lines between paragraphs")] + [("removeLineReturns", tl!("Join the lines of each paragraph")), ("removeParagraphReturns", tl!("Remove blank lines between paragraphs"))] { let on = d.bool(key); if widgets::check(ui, label, on, true) { @@ -56,17 +57,17 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } } ui.add_space(10.0); - widgets::subheader(ui, "Spaces"); + widgets::subheader(ui, tl!("Spaces")); let on = d.bool("replaceSpaces"); ui.horizontal(|ui| { - if widgets::check(ui, "Replace", on, true) { + if widgets::check(ui, tl!("Replace"), on, true) { d.fields.insert("replaceSpaces".into(), json!(!on)); } ui.add_enabled_ui(on, |ui| { if let Some(n) = widgets::plain_field(ui, "text-import-spaces", d.f64("spaces", 3.0), "", 0, 44.0) { d.fields.insert("spaces".into(), json!(n.round().clamp(2.0, 100.0))); } - widgets::dim_label(ui, "or more spaces with a tab"); + widgets::dim_label(ui, tl!("or more spaces with a tab")); }); }); false diff --git a/crates/ui-egui/src/dialogs/tiff_bmp_tga.rs b/crates/ui-egui/src/dialogs/tiff_bmp_tga.rs index 24c6efa54..1349cd196 100644 --- a/crates/ui-egui/src/dialogs/tiff_bmp_tga.rs +++ b/crates/ui-egui/src/dialogs/tiff_bmp_tga.rs @@ -76,7 +76,7 @@ pub(super) fn rows(ui: &mut egui::Ui, d: &mut Dialog, id: &str, label: Label) { "tiff" => tiff_rows(ui, d, label), "bmp" => bmp_rows(ui, d, label), "tga" => { - label(ui, "Depth:"); + label(ui, tl!("Depth:")); depth_row(ui, d, "tga-depth", &tga::DEPTHS, &TGA_DEPTH_LABELS, |_| true); ui.end_row(); } @@ -85,51 +85,51 @@ pub(super) fn rows(ui: &mut egui::Ui, d: &mut Dialog, id: &str, label: Label) { } fn tiff_rows(ui: &mut egui::Ui, d: &mut Dialog, label: Label) { - label(ui, "Color Model:"); + label(ui, tl!("Color Model:")); choice(ui, d, "colorModel", &ColorModel::ALL.map(ColorModel::id), &ColorModel::ALL.map(ColorModel::label)); ui.end_row(); - label(ui, "Byte Order:"); + label(ui, tl!("Byte Order:")); choice(ui, d, "byteOrder", &ByteOrder::ALL.map(ByteOrder::id), &ByteOrder::ALL.map(ByteOrder::label)); ui.end_row(); ui.label(""); ui.horizontal(|ui| { - form::check(ui, d, "lzw", "LZW Compression"); - form::check(ui, d, "embedIcc", "Embed ICC Profile"); + form::check(ui, d, "lzw", tl!("LZW Compression")); + form::check(ui, d, "embedIcc", tl!("Embed ICC Profile")); }); ui.end_row(); } fn bmp_rows(ui: &mut egui::Ui, d: &mut Dialog, label: Label) { - label(ui, "Color Model:"); + label(ui, tl!("Color Model:")); choice(ui, d, "colorModel", &BMP_MODELS, &BMP_MODEL_LABELS); ui.end_row(); - label(ui, "File Format:"); + label(ui, tl!("File Format:")); choice(ui, d, "fileFormat", &LAYOUTS, &LAYOUT_LABELS); ui.end_row(); let os2 = d.str("fileFormat") == LAYOUTS[1]; - label(ui, "Depth:"); + label(ui, tl!("Depth:")); depth_row(ui, d, "bmp-depth", &bmp::DEPTHS, &BMP_DEPTH_LABELS, |depth| !os2 || bmp::OS2_DEPTHS.contains(&depth)); ui.end_row(); // The palette of a colour image at 4 or 8 bits (1 bit is black and white; greys are fixed). let depth = d.f64("depth", 24.0) as u8; if matches!(depth, 4 | 8) && d.str("colorModel") != BMP_MODELS[1] { - label(ui, "Color Reduction:"); + label(ui, tl!("Color Reduction:")); choice(ui, d, "reduction", &Reduction::ALL.map(Reduction::id), &Reduction::ALL.map(Reduction::label)); ui.end_row(); - label(ui, "Dither:"); + label(ui, tl!("Dither:")); choice(ui, d, "dither", &Dither::ALL.map(Dither::id), &Dither::ALL.map(Dither::label)); ui.end_row(); } ui.label(""); ui.horizontal(|ui| { - ui.add_enabled_ui(!os2 && matches!(depth, 4 | 8), |ui| form::check(ui, d, "rle", "Compress (RLE)")); - ui.add_enabled_ui(!os2 && !d.bool("rle"), |ui| form::check(ui, d, "flipRows", "Flip Row Order")); + ui.add_enabled_ui(!os2 && matches!(depth, 4 | 8), |ui| form::check(ui, d, "rle", tl!("Compress (RLE)"))); + ui.add_enabled_ui(!os2 && !d.bool("rle"), |ui| form::check(ui, d, "flipRows", tl!("Flip Row Order"))); }); ui.end_row(); normalize_bmp(d); diff --git a/crates/ui-egui/src/dialogs/tile_edge_color.rs b/crates/ui-egui/src/dialogs/tile_edge_color.rs index 3efcdfa22..785fafbcd 100644 --- a/crates/ui-egui/src/dialogs/tile_edge_color.rs +++ b/crates/ui-egui/src/dialogs/tile_edge_color.rs @@ -16,7 +16,7 @@ use crate::{VectorcraftApp, widgets}; /// The dialog kind of Tile Edge Color. pub const KIND: &str = "tileEdgeColor"; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Tile Edge Color".into(), body, confirm, min_width: 300.0, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Tile Edge Color").into(), body, confirm, min_width: 300.0, ..DialogSpec::FORM }; /// Open Tile Edge Color on the current preference. pub fn open(app: &mut VectorcraftApp) -> Result { @@ -38,7 +38,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let preset = LAYER_COLORS.iter().position(|(_, c)| *c == [r, g, b]); let names: Vec<&str> = LAYER_COLORS.iter().map(|(n, _)| *n).chain(["Custom"]).collect(); grid(ui, |ui| { - label(ui, "Color:"); + label(ui, tl!("Color:")); ui.horizontal(|ui| { if let Some((_, [r, g, b])) = widgets::dropdown(ui, "tile-edge", names[preset.unwrap_or(LAYER_COLORS.len())], &names, 140.0).and_then(|i| LAYER_COLORS.get(i)) diff --git a/crates/ui-egui/src/dialogs/transform.rs b/crates/ui-egui/src/dialogs/transform.rs index 4ef5fbafe..bbc81d2fb 100644 --- a/crates/ui-egui/src/dialogs/transform.rs +++ b/crates/ui-egui/src/dialogs/transform.rs @@ -11,11 +11,11 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(&d.kind).int fn title(kind: &str) -> &'static str { match kind { - "move" => "Move", - "rotate" => "Rotate", - "scale" => "Scale", - "reflect" => "Reflect", - _ => "Shear", + "move" => tl!("Move"), + "rotate" => tl!("Rotate"), + "scale" => tl!("Scale"), + "reflect" => tl!("Reflect"), + _ => tl!("Shear"), } } @@ -23,10 +23,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { form::grid(ui, d, app.session.general_unit()); ui.add_space(6.0); if d.kind == "scale" { - form::check(ui, d, "uniform", "Uniform"); + form::check(ui, d, "uniform", tl!("Uniform")); scale_options(app, ui, d); } - form::check(ui, d, "copy", "Copy (make a transformed copy)"); + form::check(ui, d, "copy", tl!("Copy (make a transformed copy)")); false } @@ -45,7 +45,7 @@ pub(super) fn scale_options(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dia for (field, _, label) in SCALE_OPTIONS { let on = scale_option(app, d, field); d.fields.insert(field.into(), Value::Bool(on)); - form::check(ui, d, field, label); + form::check(ui, d, field, tl!(label)); } } diff --git a/crates/ui-egui/src/dialogs/transform_each.rs b/crates/ui-egui/src/dialogs/transform_each.rs index d8609bc31..22c397544 100644 --- a/crates/ui-egui/src/dialogs/transform_each.rs +++ b/crates/ui-egui/src/dialogs/transform_each.rs @@ -20,7 +20,7 @@ pub const KIND: &str = "transformEach"; const CMD: &str = "object.transformEach"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Transform Each".into(), body, confirm, preview: true, min_width: 360.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Transform Each").into(), body, confirm, preview: true, min_width: 360.0, ..DialogSpec::FORM }; /// The fields a fresh dialog starts with. pub fn fields() -> Value { @@ -55,9 +55,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.vertical(|ui| { egui::Grid::new("te-grid").num_columns(2).spacing([10.0, 6.0]).show(ui, |ui| { for (head, rows) in [ - ("Scale", [("scaleH", "Horizontal:", "%"), ("scaleV", "Vertical:", "%")].as_slice()), - ("Move", &[("moveH", "Horizontal:", "pt"), ("moveV", "Vertical:", "pt")]), - ("Rotate", &[("rotate", "Angle:", "°")]), + (tl!("Scale"), [("scaleH", tl!("Horizontal:"), "%"), ("scaleV", tl!("Vertical:"), "%")].as_slice()), + (tl!("Move"), &[("moveH", tl!("Horizontal:"), "pt"), ("moveV", tl!("Vertical:"), "pt")]), + (tl!("Rotate"), &[("rotate", tl!("Angle:"), "°")]), ] { widgets::subheader(ui, head); ui.end_row(); @@ -69,20 +69,20 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); ui.add_space(16.0); ui.vertical(|ui| { - widgets::subheader(ui, "Options"); + widgets::subheader(ui, tl!("Options")); ui.add_space(4.0); scale_options(app, ui, d); - form::check(ui, d, "reflectX", "Reflect X"); - form::check(ui, d, "reflectY", "Reflect Y"); + form::check(ui, d, "reflectX", tl!("Reflect X")); + form::check(ui, d, "reflectY", tl!("Reflect Y")); ui.horizontal(|ui| { let cur = d.f64("reference", 4.0).clamp(0.0, 8.0) as usize; if let Some(i) = widgets::reference_point(ui, cur) { d.fields.insert("reference".into(), json!(i)); } - widgets::dim_label(ui, "Reference Point"); + widgets::dim_label(ui, tl!("Reference Point")); }); - form::check(ui, d, "random", "Random"); - form::check(ui, d, "copy", "Copy"); + form::check(ui, d, "random", tl!("Random")); + form::check(ui, d, "copy", tl!("Copy")); }); }); let mut p = params(d); diff --git a/crates/ui-egui/src/dialogs/width_point.rs b/crates/ui-egui/src/dialogs/width_point.rs index 35a4ebb22..247669a26 100644 --- a/crates/ui-egui/src/dialogs/width_point.rs +++ b/crates/ui-egui/src/dialogs/width_point.rs @@ -19,7 +19,7 @@ use crate::{VectorcraftApp, widgets}; pub const KIND: &str = "widthPoint"; pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| "Width Point Edit".into(), body, confirm, discard: Some("Delete"), min_width: 300.0, ..DialogSpec::FORM }; + DialogSpec { heading: |_| tl!("Width Point Edit").into(), body, confirm, discard: Some("Delete"), min_width: 300.0, ..DialogSpec::FORM }; /// Width point `index` of `id`: its place and side widths (points). fn point(app: &VectorcraftApp, id: u64, index: usize) -> Option<(f64, f64, f64)> { @@ -44,12 +44,12 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let (s1, s2, linked) = (d.f64("side1", 0.0), d.f64("side2", 0.0), d.bool("linked")); let mut sides = None; grid(ui, |ui| { - label(ui, "Side 1:"); + label(ui, tl!("Side 1:")); ui.horizontal(|ui| { if let Some(v) = widgets::num_field(ui, "wp-side1", Some(s1), unit, 90.0) { sides = Some((v, if linked { v } else { s2 })); } - if widgets::icon_button(ui, if linked { "link" } else { "link-2-off" }, "Keep both sides the same width", linked, 22.0).clicked() { + if widgets::icon_button(ui, if linked { "link" } else { "link-2-off" }, tl!("Keep both sides the same width"), linked, 22.0).clicked() { d.fields.insert("linked".into(), json!(!linked)); if !linked { sides = Some((s1, s1)); @@ -57,12 +57,12 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } }); ui.end_row(); - label(ui, "Side 2:"); + label(ui, tl!("Side 2:")); if let Some(v) = widgets::num_field(ui, "wp-side2", Some(s2), unit, 90.0) { sides = Some((if linked { v } else { s1 }, v)); } ui.end_row(); - label(ui, "Total Width:"); + label(ui, tl!("Total Width:")); if let Some(v) = widgets::num_field(ui, "wp-total", Some(s1 + s2), unit, 90.0) { // Both sides in proportion (equal halves from nothing). let k = if s1 + s2 > 1e-9 { v / (s1 + s2) } else { 0.0 }; @@ -76,7 +76,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } ui.add_space(4.0); ui.allocate_ui(vec2(ui.available_width(), 22.0), |ui| { - if widgets::check(ui, "Adjust Adjoining Width Points", d.bool("adjustAdjoining"), true) { + if widgets::check(ui, tl!("Adjust Adjoining Width Points"), d.bool("adjustAdjoining"), true) { d.fields.insert("adjustAdjoining".into(), json!(!d.bool("adjustAdjoining"))); } }); diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 33c705193..db7e35675 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -28,7 +28,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let mut x = strip.left(); for (tab, label) in [(DockTab::Properties, "Properties"), (DockTab::Layers, "Layers"), (DockTab::Libraries, "Libraries")] { let active = app.ui.dock_tab == tab; - let galley = ui.painter().layout_no_wrap(label.to_string(), theme::semibold(12.5), if active { t.text_strong } else { t.text_dim }); + let galley = + ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.5), if active { t.text_strong } else { t.text_dim }); let r = egui::Rect::from_min_size(egui::pos2(x, strip.top()), vec2(galley.size().x + 24.0, strip.height() - 1.0)); let resp = ui.interact(r, ui.id().with(("docktab", label)), Sense::click()); if active { @@ -99,10 +100,10 @@ pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.painter().rect_filled(strip, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.panel_darker); let tab = egui::Rect::from_min_size( strip.min, - vec2(ui.painter().layout_no_wrap(label.to_string(), theme::semibold(12.0), t.text).size().x + 24.0, 26.0), + vec2(ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.0), t.text).size().x + 24.0, 26.0), ); ui.painter().rect_filled(tab, CornerRadius { nw: 4, ne: 0, sw: 0, se: 0 }, t.panel); - ui.painter().text(tab.left_center() + vec2(12.0, 0.0), egui::Align2::LEFT_CENTER, *label, theme::semibold(12.0), t.text); + ui.painter().text(tab.left_center() + vec2(12.0, 0.0), egui::Align2::LEFT_CENTER, tl!(label), theme::semibold(12.0), t.text); let close = egui::Rect::from_center_size(strip.right_center() - vec2(13.0, 0.0), vec2(14.0, 14.0)); let cr = ui.interact(close, ui.id().with("close-panel"), Sense::click()); icons::paint(ui, "chevrons-right", close, if cr.hovered() { t.text } else { t.text_dim }); diff --git a/crates/ui-egui/src/find_font.rs b/crates/ui-egui/src/find_font.rs index 9bef09ff0..6d969f7ea 100644 --- a/crates/ui-egui/src/find_font.rs +++ b/crates/ui-egui/src/find_font.rs @@ -39,7 +39,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { egui::Area::new(egui::Id::new("modal-dim")).order(egui::Order::Middle).fixed_pos(egui::pos2(0.0, 0.0)).show(ctx, |ui| { ui.allocate_rect(ctx.content_rect(), egui::Sense::click()); }); - egui::Window::new("Find Font") + egui::Window::new(tl!("Find Font")) .id(egui::Id::new("dialog-find-font")) .order(egui::Order::Foreground) .collapsible(false) @@ -49,9 +49,9 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { .frame(egui::Frame::window(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(22))) .show(ctx, |ui: &mut Ui| { ui.set_width(380.0); - ui.label(egui::RichText::new("Find Font").font(theme::semibold(16.0)).color(t.text)); + ui.label(egui::RichText::new(tl!("Find Font")).font(theme::semibold(16.0)).color(t.text)); ui.add_space(10.0); - widgets::subheader(ui, &format!("Fonts in Document: {}", list.len())); + widgets::subheader(ui, &crate::i18n::fmt(tl!("Fonts in Document: {count}"), &[("count", &list.len().to_string())])); let sel = d.fields.get("selected").and_then(Value::as_u64).unwrap_or(0) as usize; egui::Frame::NONE.fill(t.input).stroke(egui::Stroke::new(1.0, t.input_border)).inner_margin(egui::Margin::same(4)).show(ui, |ui| { ui.set_min_height(120.0); @@ -68,7 +68,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { }); }); ui.add_space(10.0); - widgets::subheader(ui, "Replace With Font"); + widgets::subheader(ui, tl!("Replace With Font")); let fam = d.str("family"); ui.horizontal(|ui| { if let Some(f) = widgets::font_dropdown(ui, "ff-family", &fam, 220.0) { @@ -86,17 +86,17 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.add_space(14.0); ui.horizontal(|ui| { let has = sel < list.len(); - if ui.add_enabled(has, egui::Button::new("Find")).clicked() { + if ui.add_enabled(has, egui::Button::new(tl!("Find"))).clicked() { act = Some("find"); } - if ui.add_enabled(has, egui::Button::new("Change")).on_hover_text("In the selected objects").clicked() { + if ui.add_enabled(has, egui::Button::new(tl!("Change"))).on_hover_text(tl!("In the selected objects")).clicked() { act = Some("change"); } - if ui.add_enabled(has, egui::Button::new("Change All")).clicked() { + if ui.add_enabled(has, egui::Button::new(tl!("Change All"))).clicked() { act = Some("changeAll"); } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if widgets::primary_button(ui, "Done").clicked() { + if widgets::primary_button(ui, tl!("Done")).clicked() { close = true; } }); @@ -124,12 +124,16 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { /// standing in for another, characters the font lacks; and the colour to flag it with. fn font_note(f: &Value) -> (String, Option) { let lacking = f["missingGlyphs"].as_u64().unwrap_or(0); - let glyphs = if lacking > 0 { format!(" — {lacking} characters from another font") } else { String::new() }; + let glyphs = + if lacking > 0 { crate::i18n::fmt(tl!(" — {n} characters from another font"), &[("n", &lacking.to_string())]) } else { String::new() }; match f["status"].as_str() { - Some("missing") => (format!(" — missing{glyphs}"), Some(egui::Color32::from_rgb(230, 90, 90))), + Some("missing") => (format!("{}{glyphs}", tl!(" — missing")), Some(egui::Color32::from_rgb(230, 90, 90))), Some("substitute") => { let used = f["resolved"]["style"].as_str().unwrap_or(""); - (format!(" — substituted by {used}{glyphs}"), Some(egui::Color32::from_rgb(220, 160, 60))) + ( + format!("{}{glyphs}", crate::i18n::fmt(tl!(" — substituted by {style}"), &[("style", used)])), + Some(egui::Color32::from_rgb(220, 160, 60)), + ) } _ if lacking > 0 => (glyphs, Some(egui::Color32::from_rgb(220, 160, 60))), _ => (String::new(), None), diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs deleted file mode 100644 index 0ed267048..000000000 --- a/crates/ui-egui/src/i18n.rs +++ /dev/null @@ -1,313 +0,0 @@ -//! Interface translations. Command ids, document text and file names remain stable. -//! Untranslated labels fall back to English so coverage can grow incrementally. Every entry is a -//! label VectorCraft's menus show (tested), so labels from other apps don't creep in. - -mod czech; -use czech::CZECH; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Language { - #[default] - En, - Ja, - Cs, -} - -impl Language { - pub const ALL: [Self; 3] = [Self::En, Self::Ja, Self::Cs]; - - pub fn name(self) -> &'static str { - match self { - Self::En => "English", - Self::Ja => "日本語", - Self::Cs => "Čeština", - } - } - - pub fn parse(code: &str) -> Option { - match code { - "en" => Some(Self::En), - "ja" => Some(Self::Ja), - "cs" => Some(Self::Cs), - _ => None, - } - } - - pub fn tr(self, text: &str) -> &str { - let table = match self { - Self::En => return text, - Self::Ja => JAPANESE, - Self::Cs => CZECH, - }; - table.iter().find(|(english, _)| *english == text).map_or(text, |(_, translated)| translated) - } -} - -const JAPANESE: &[(&str, &str)] = &[ - ("Object", "オブジェクト"), - ("Effect", "効果"), - ("Settings…", "環境設定…"), - ("Type", "書式"), - ("Select", "選択"), - ("Window", "ウィンドウ"), - ("Language", "表示言語"), - ("Save As…", "別名で保存…"), - ("New…", "新規…"), - ("Horizontal", "横書き"), - ("Vertical", "縦書き"), - ("Type Orientation", "組み方向"), - ("Layers", "レイヤー"), - ("History", "履歴"), - ("Properties", "プロパティ"), - ("Color", "カラー"), - ("Tools", "ツール"), - ("Zoom In", "ズームイン"), - ("Zoom Out", "ズームアウト"), - ("Copy", "コピー"), - ("Cut", "切り取り"), - ("Paste", "貼り付け"), - ("Deselect", "選択を解除"), - ("Export", "書き出し"), - ("Export As…", "形式を指定して書き出し…"), - ("File", "ファイル"), - ("Edit", "編集"), - ("View", "表示"), - ("Help", "ヘルプ"), - ("Preferences…", "環境設定…"), - ("Open…", "開く…"), - ("Save", "保存"), - ("Revert", "保存済みの状態に戻す"), - ("Print…", "印刷…"), - ("Undo", "取り消し"), - ("Redo", "やり直し"), -]; - -#[cfg(test)] -mod tests { - use serde_json::{Value, json}; - - use super::*; - use crate::menus::Item; - - const TABLES: [(Language, &[(&str, &str)]); 2] = [(Language::Ja, JAPANESE), (Language::Cs, CZECH)]; - - /// Menu labels Czech shows as they are: the product name, language names, a format name, the - /// built-in workspace names and the perspective grid presets (names, shown untranslated - /// wherever else they appear). - const KEEP_AS_IS: &[&str] = &[ - "VectorCraft", - "English", - "日本語", - "Čeština", - "OpenType", - "Essentials", - "Essentials Classic", - "Automation", - "Layout", - "Painting", - "Printing and Proofing", - "Tracing", - "Typography", - "Web", - "[1P-Normal View]", - "[1P-Low View]", - "[1P-High View]", - "[2P-Normal View]", - "[2P-Low View]", - "[2P-High View]", - "[3P-Normal View]", - "[3P-Low View]", - ]; - - /// Every menu title, submenu, item and header label with its command and params (`""`/null - /// for the ones that run nothing). - fn menu_labels() -> Vec<(&'static str, &'static str, Value)> { - fn walk(items: &[Item], out: &mut Vec<(&'static str, &'static str, Value)>) { - for i in items { - match i { - Item::Cmd(l, id, p) => out.push((l, id, p.clone())), - Item::Todo(l, _) | Item::Header(l) => out.push((l, "", Value::Null)), - Item::Sub(l, children) => { - out.push((l, "", Value::Null)); - walk(children, out); - } - Item::Sep => {} - } - } - } - let mut labels = vec![]; - for (title, items) in crate::menus::menu_tree() { - labels.push((title, "", Value::Null)); - walk(&items, &mut labels); - } - labels - } - - /// The labels toggling items switch to ("Show Guides" once guides are hidden…), from a - /// document with every toggle flipped. - fn toggled_labels() -> Vec { - let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); - app.run("file.new", json!({})).unwrap(); - app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); - app.run("object.envelope.makeWithWarp", json!({})).unwrap(); - let toggles = [ - "view.outline", - "view.edges", - "view.cornerWidget", - "view.textThreads", - "type.hiddenCharacters", - "view.gradientAnnotator", - "view.artboards", - "view.rulers", - "view.boundingBox", - "view.transparencyGrid", - "view.guides", - "view.grid", - "view.guides.lock", - "view.slices.hide", - "view.printTiling", - "perspective.grid.show", - "perspective.grid.rulers", - "perspective.grid.lock", - "object.envelope.editContents", - ]; - let mut labels = vec![]; - for id in toggles { - let before = crate::menus::dynamic_label(&app, id, ""); - app.run(id, json!({})).unwrap(); - let after = crate::menus::dynamic_label(&app, id, ""); - assert_ne!(before, after, "{id} didn't toggle its label"); - labels.extend([before, after]); - } - labels - } - - #[test] - fn translations_are_unique_and_preserve_unknown_text() { - for (language, table) in TABLES { - for (i, (en, translated)) in table.iter().enumerate() { - assert!(!translated.is_empty()); - assert!(table.iter().take(i).all(|(other, _)| en != other), "`{en}` twice"); - assert_eq!(en.ends_with('…'), translated.ends_with('…'), "`{en}` and `{translated}` differ in their ellipsis"); - assert_eq!(Language::En.tr(en), *en); - assert_eq!(language.tr(en), *translated); - } - assert_eq!(language.tr("日本語の文書.pdf"), "日本語の文書.pdf"); - } - assert_eq!(Language::Ja.tr("File"), "ファイル"); - assert_eq!(Language::Cs.tr("File"), "Soubor"); - assert_eq!(Language::parse("xx"), None); - for language in Language::ALL { - let code = serde_json::to_value(language).unwrap(); - assert_eq!(Language::parse(code.as_str().unwrap()), Some(language)); - } - assert_eq!(serde_json::to_value(Language::Cs).unwrap(), json!("cs")); - assert_eq!(Language::Cs.name(), "Čeština"); - } - - #[test] - fn every_translated_label_is_a_menu_label() { - let mut labels: Vec = menu_labels().into_iter().map(|(l, ..)| l.to_string()).collect(); - // Object › Plug-ins' header leaves the menu while any test has a filter plug-in installed. - labels.push(crate::dialogs::plugin::NO_FILTERS.into()); - labels.extend(toggled_labels()); - for (language, table) in TABLES { - for (en, _) in table { - assert!(labels.iter().any(|l| l == en), "{language:?}: `{en}` isn't a VectorCraft menu label"); - } - } - } - - #[test] - fn czech_translates_every_menu_label() { - let translated = |l: &str| CZECH.iter().any(|(en, _)| *en == l); - let labels = menu_labels(); - // Font names and sizes, and installed plug-ins' own names, are not interface text. - let interface = |(label, id, p): &(&str, &str, Value)| { - let plugin = *id == "plugin.dialog" || p.get("effect").and_then(Value::as_str).is_some_and(|e| e.starts_with("plugin.")); - *id != "text.setStyle" && !plugin && !KEEP_AS_IS.contains(label) - }; - let mut missing: Vec = labels.iter().filter(|l| interface(l)).map(|(l, ..)| l.to_string()).collect(); - missing.extend(toggled_labels()); - missing.retain(|l| !translated(l)); - missing.dedup(); - assert!(missing.is_empty(), "untranslated Czech menu labels: {missing:?}"); - for keep in KEEP_AS_IS { - assert!(labels.iter().any(|(l, ..)| l == keep), "`{keep}` isn't a menu label"); - assert!(!translated(keep), "`{keep}` is both kept and translated"); - } - } - - #[test] - fn language_command_validates_and_persists_without_a_document() { - let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); - crate::menus::run_ui_command(&mut app, "app.language", &json!({"lang": "ja"})).unwrap().unwrap(); - assert_eq!(app.ui.language, Language::Ja); - assert_eq!(crate::menus::checked(&app, "app.language", &json!({"lang": "ja"})), Some(true)); - assert!(crate::menus::run_ui_command(&mut app, "app.language", &json!({"lang": "xx"})).unwrap().is_err()); - assert_eq!(app.ui.language, Language::Ja); - let saved = serde_json::to_string(&app.ui).unwrap(); - let restored: crate::state::UiState = serde_json::from_str(&saved).unwrap(); - assert_eq!(restored.language, Language::Ja); - assert_eq!(crate::menus::run_ui_command(&mut app, "app.language", &json!({"lang": "cs"})).unwrap(), Ok(json!("cs"))); - assert_eq!(app.ui.language, Language::Cs); - assert_eq!(crate::menus::checked(&app, "app.language", &json!({"lang": "cs"})), Some(true)); - assert_eq!(crate::menus::checked(&app, "app.language", &json!({"lang": "ja"})), Some(false)); - let restored: crate::state::UiState = serde_json::from_str(&serde_json::to_string(&app.ui).unwrap()).unwrap(); - assert_eq!(restored.language, Language::Cs); - } - - #[test] - fn the_language_menu_offers_every_language() { - let items: Vec<_> = menu_labels().into_iter().filter(|(_, id, _)| *id == "app.language").collect(); - assert_eq!(items.len(), Language::ALL.len()); - for language in Language::ALL { - let code = serde_json::to_value(language).unwrap(); - assert!(items.iter().any(|(l, _, p)| *l == language.name() && p["lang"] == code), "{language:?} isn't in the Language menu"); - } - } - - #[test] - fn japanese_glyphs_are_available_without_system_fonts() { - if !vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.is_japanese()) { - eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout to run it)"); - return; - } - let ctx = egui::Context::default(); - crate::theme::install_fonts(&ctx); - let mut output = ctx.run_ui(egui::RawInput::default(), |_| {}); - output.textures_delta.clear(); - ctx.fonts_mut(|fonts| { - let families: Vec<_> = fonts.definitions().families.keys().cloned().collect(); - for family in families { - let font = egui::FontId::new(13.0, family); - for ch in "日本語ファイル編集".chars() { - assert!(fonts.has_glyph(&font, ch), "missing {ch} in {font:?}"); - } - } - }); - } - - /// Czech letters (and the punctuation Czech text uses) come from each family's own first font, - /// not from a fallback further down the stack. (`has_glyph` can't tell: it counts characters of - /// the face that draws missing glyphs, the first one, as missing.) - #[test] - fn czech_glyphs_are_available_without_system_fonts() { - let ctx = egui::Context::default(); - crate::theme::install_fonts(&ctx); - let mut output = ctx.run_ui(egui::RawInput::default(), |_| {}); - output.textures_delta.clear(); - ctx.fonts_mut(|fonts| { - let families: Vec<_> = fonts.definitions().families.iter().map(|(f, stack)| (f.clone(), stack.first().cloned())).collect(); - for (family, first) in families { - let first = first.unwrap(); - let mut font = fonts.fonts.font(&family); - let chars = font.characters(); - for ch in "áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ„“‚‘…–".chars() { - assert!(chars.get(&ch).is_some_and(|fonts| fonts.contains(&first)), "{first} ({family:?}) has no {ch}"); - } - } - }); - } -} diff --git a/crates/ui-egui/src/i18n/catalog.rs b/crates/ui-egui/src/i18n/catalog.rs new file mode 100644 index 000000000..48d833a43 --- /dev/null +++ b/crates/ui-egui/src/i18n/catalog.rs @@ -0,0 +1,120 @@ +//! Parser and lookup tables for translation catalogs (`*.tsv`, format documented in `zh-hant.tsv`). + +use std::collections::HashMap; + +/// A parsed catalog. Values are owned for the life of the process (catalogs are built once). +#[derive(Debug, Default)] +pub struct Catalog { + /// context-free strings: English source → translation (the hot path, looked up every frame) + plain: HashMap, + /// strings with a disambiguating context, keyed `context \u{1} source` + contextual: HashMap, + /// command-id keyed strings + ids: HashMap, + /// plural messages: `one|other` → forms + plurals: HashMap>, +} + +/// A catalog entry as read from the file: (context, source, translation). +pub type Entry = (String, String, String); + +fn unescape(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut it = s.chars(); + while let Some(c) = it.next() { + if c != '\\' { + out.push(c); + continue; + } + match it.next() { + Some('n') => out.push('\n'), + Some('t') => out.push('\t'), + Some('\\') => out.push('\\'), + Some(o) => { + out.push('\\'); + out.push(o); + } + None => out.push('\\'), + } + } + out +} + +/// Read the entries of a catalog file. Malformed lines are returned as errors (and skipped), so +/// a bad translation never breaks the UI; the tests insist the bundled catalogs have none. +pub fn parse_entries(text: &str) -> (Vec, Vec) { + let mut entries = Vec::new(); + let mut errors = Vec::new(); + for (n, line) in text.lines().enumerate() { + if line.trim().is_empty() || line.starts_with('#') { + continue; + } + let mut cols = line.split('\t'); + match (cols.next(), cols.next(), cols.next(), cols.next()) { + (Some(ctx), Some(src), Some(tr), None) if !src.is_empty() && !tr.is_empty() => { + entries.push((unescape(ctx), unescape(src), unescape(tr))); + } + _ => errors.push(format!("line {}: expected `contextsourcetranslation`", n + 1)), + } + } + (entries, errors) +} + +impl Catalog { + pub fn parse(text: &str) -> Catalog { + let mut c = Catalog::default(); + for (ctx, src, tr) in parse_entries(text).0 { + match ctx.as_str() { + "" => { + c.plain.insert(src, tr); + } + "@id" => { + c.ids.insert(src, tr); + } + "@plural" => { + c.plurals.insert(src, tr.split('|').map(str::to_string).collect()); + } + _ => { + c.contextual.insert(format!("{ctx}\u{1}{src}"), tr); + } + } + } + c + } + + pub fn plain(&self, s: &str) -> Option<&str> { + self.plain.get(s).map(String::as_str) + } + + pub fn contextual(&self, ctx: &str, s: &str) -> Option<&str> { + self.contextual.get(&format!("{ctx}\u{1}{s}")).map(String::as_str) + } + + pub fn id(&self, id: &str) -> Option<&str> { + self.ids.get(id).map(String::as_str) + } + + /// The plural form `index` of the message whose English forms are `one|other`. + pub fn plural(&self, one: &str, other: &str, index: usize) -> Option<&str> { + let forms = self.plurals.get(&format!("{one}|{other}"))?; + forms.get(index.min(forms.len().saturating_sub(1))).map(String::as_str) + } +} + +/// `{name}` placeholders of a template, in order of appearance. +#[cfg(test)] +pub fn placeholders(s: &str) -> Vec<&str> { + let mut out = Vec::new(); + let mut rest = s; + while let Some(a) = rest.find('{') { + let after = &rest[a + 1..]; + match after.find('}') { + Some(b) => { + out.push(&after[..b]); + rest = &after[b + 1..]; + } + None => break, + } + } + out +} diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv new file mode 100644 index 000000000..28c35aac6 --- /dev/null +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -0,0 +1,571 @@ +# Czech (cs) catalog. Same format as zh-hant.tsv: context source translation +# (empty context = a plain UI string; `@id` = a command id; `@plural` = "one|few|other" forms). +# +# Clean-room: every entry is written from the meaning of VectorCraft's English label, following +# common Czech desktop-software conventions (commands in the infinitive, menus as nouns); no other +# application's Czech localisation was consulted or copied. +# +# Coverage: every menu label (tested: `czech_translates_every_menu_label`); panels and dialogs fall +# back to English. Left in English on purpose (`CZECH_KEEP_AS_IS` in the tests): the product name, +# language names, OpenType, the built-in workspace names and the bracketed perspective grid preset +# names. + About VectorCraft O aplikaci VectorCraft + Join Our Discord Připojit se k našemu Discordu + Settings… Nastavení… + Language Jazyk + UI Brightness Jas rozhraní + Dark Tmavý + Medium Dark Středně tmavý + Medium Light Středně světlý + Light Světlý + Quit VectorCraft Ukončit VectorCraft + File Soubor + New… Nový… + New from Template… Nový ze šablony… + Open… Otevřít… + Open Recent Files Otevřít nedávné soubory + Clear Recent Files Vymazat seznam nedávných souborů + Show in Folder Zobrazit ve složce + Close Zavřít + Close All Zavřít vše + Save Uložit + Save As… Uložit jako… + Save a Copy… Uložit kopii… + Save as Template… Uložit jako šablonu… + Save as PDF… Uložit jako PDF… + Save for Office Documents… Uložit pro kancelářské dokumenty… + Revert Vrátit k uložené verzi + Place… Umístit… + Export Export + Export for Screens… Exportovat pro obrazovky… + Export As… Exportovat jako… + Export As SVG… Exportovat jako SVG… + Export As PNG… Exportovat jako PNG… + Save for Web (Legacy)… Uložit pro web (starší)… + Export Selection… Exportovat výběr… + Package… Sbalit… + Scripts Skripty + Other Script… Jiný skript… + Document Setup… Nastavení dokumentu… + Document Color Mode Barevný režim dokumentu + CMYK Color Barvy CMYK + RGB Color Barvy RGB + File Info… Informace o souboru… + Print… Tisk… + Edit Úpravy + Undo Zpět + Redo Znovu + Cut Vyjmout + Copy Kopírovat + Paste Vložit + Paste in Front Vložit dopředu + Paste in Back Vložit dozadu + Paste in Place Vložit na místo + Paste on All Artboards Vložit na všechny pracovní plochy + Paste without Formatting Vložit bez formátování + Clear Smazat + Find and Replace… Najít a nahradit… + Find Next Najít další + Spelling Pravopis + Auto Spell Check Automatická kontrola pravopisu + Check Spelling… Zkontrolovat pravopis… + Edit Custom Dictionary… Upravit vlastní slovník… + Edit Colors Upravit barvy + Recolor Artwork… Přebarvit kresbu… + Recolor with Preset Přebarvit podle přednastavení + 1 Color Job… Zakázka s 1 barvou… + 2 Color Job… Zakázka se 2 barvami… + 3 Color Job… Zakázka se 3 barvami… + Color Library… Knihovna barev… + Adjust Color Balance… Upravit vyvážení barev… + Blend Front to Back Prolnout zepředu dozadu + Blend Horizontally Prolnout vodorovně + Blend Vertically Prolnout svisle + Convert to CMYK Převést na CMYK + Convert to Grayscale Převést na stupně šedi + Convert to RGB Převést na RGB + Invert Colors Invertovat barvy + Overprint Black… Přetisknout černou… + Saturate… Upravit sytost… + Edit Original Upravit originál + Transparency Flattener Presets… Přednastavení sloučení průhlednosti… + Print Presets… Přednastavení tisku… + PDF Presets… Přednastavení PDF… + Perspective Grid Presets… Přednastavení perspektivní mřížky… + Color Settings… Nastavení barev… + Assign Profile… Přiřadit profil… + Keyboard Shortcuts… Klávesové zkratky… + Preferences… Předvolby… + Object Objekt + Transform Transformace + Transform Again Transformovat znovu + Move… Přesunout… + Rotate… Otočit… + Reflect… Zrcadlit… + Scale… Změnit velikost… + Shear… Zkosit… + Transform Each… Transformovat jednotlivě… + Reset Bounding Box Obnovit ohraničovací rámeček + Arrange Uspořádat + Bring to Front Přenést do popředí + Bring Forward Přenést blíž + Send Backward Přesunout dál + Send to Back Přesunout do pozadí + Send to Current Layer Přesunout do aktuální vrstvy + Align Zarovnat + Horizontal Align Left Vodorovně zarovnat doleva + Horizontal Align Center Vodorovně zarovnat na střed + Horizontal Align Right Vodorovně zarovnat doprava + Vertical Align Top Svisle zarovnat nahoru + Vertical Align Center Svisle zarovnat na střed + Vertical Align Bottom Svisle zarovnat dolů + Group Seskupit + Ungroup Oddělit + Lock Zamknout + Selection Výběr + All Artwork Above Veškerou kresbu výše + Other Layers Ostatní vrstvy + Unlock All Odemknout vše + Hide Skrýt + Show All Zobrazit vše + Expand… Rozložit… + Expand Appearance Rozložit vzhled + Crop Image Oříznout obraz + Rasterize… Rastrovat… + Create Gradient Mesh… Vytvořit přechodovou síť… + Create Object Mosaic… Vytvořit mozaiku objektů… + Vector Halftone… Vektorový polotón… + Create Trim Marks Vytvořit značky ořezu + Flatten Transparency… Sloučit průhlednost… + Make Pixel Perfect Zarovnat na obrazové body + Slice Řez + Make Vytvořit + Release Uvolnit + Create from Guides Vytvořit z vodítek + Create from Selection Vytvořit z výběru + Duplicate Slice Duplikovat řez + Combine Slices Spojit řezy + Divide Slices… Rozdělit řezy… + Delete All Odstranit vše + Slice Options… Možnosti řezu… + Clip to Artboard Oříznout na pracovní plochu + Path Cesta + Join Spojit + Average… Zprůměrovat… + Outline Stroke Převést tah na obrys + Offset Path… Posunout cestu… + Reverse Path Direction Obrátit směr cesty + Simplify… Zjednodušit… + Add Anchor Points Přidat kotevní body + Remove Anchor Points Odstranit kotevní body + Divide Objects Below Rozdělit objekty pod + Split Into Grid… Rozdělit do mřížky… + Clean Up… Vyčistit… + Shape Tvar + Convert to Shape Převést na tvar + Expand Shape Rozložit tvar + Pattern Vzorek + Edit Pattern Upravit vzorek + Tile Edge Color… Barva okraje dlaždice… + Repeat Opakování + Radial Kruhové + Grid Mřížkové + Mirror Zrcadlové + Options… Možnosti… + Blend Prolnutí + Blend Options… Možnosti prolnutí… + Expand Rozložit + Replace Spine Nahradit páteř + Reverse Spine Obrátit páteř + Reverse Front to Back Obrátit pořadí zepředu dozadu + Envelope Distort Deformace obálkou + Make with Warp… Vytvořit pokřivením… + Reset with Warp… Obnovit pokřivením… + Make with Mesh… Vytvořit sítí… + Reset with Mesh… Obnovit sítí… + Make with Top Object Vytvořit horním objektem + Envelope Options… Možnosti obálky… + Edit Contents Upravit obsah + Edit Envelope Upravit obálku + Perspective Perspektiva + Attach to Active Plane Připojit k aktivní rovině + Release with Perspective Uvolnit s perspektivou + Move Plane to Match Object Posunout rovinu k objektu + Edit Text Upravit text + Live Paint Živá malba + Merge Sloučit + Gap Options… Možnosti mezer… + Image Trace Vektorizace obrazu + Make… Vytvořit… + Make and Expand… Vytvořit a rozložit… + Text Wrap Obtékání textu + Text Wrap Options… Možnosti obtékání textu… + Clipping Mask Ořezová maska + Edit Clipping Path Upravit ořezovou cestu + Compound Path Složená cesta + Artboards Pracovní plochy + Convert to Artboards Převést na pracovní plochy + Rearrange All Artboards… Přeuspořádat všechny pracovní plochy… + Fit to Artwork Bounds Přizpůsobit hranicím kresby + Fit to Selected Art Přizpůsobit vybrané kresbě + Graph Graf + Type… Typ… + Data… Údaje… + Design… Návrh… + Column… Sloupec… + Marker… Značka… + Collect for Export Shromáždit pro export + As Single Asset Jako jeden prvek + As Multiple Assets Jako více prvků + Plug-ins Zásuvné moduly + No filter plug-ins installed Nejsou nainstalovány žádné filtry + Install Plug-in… Nainstalovat zásuvný modul… + Reload Plug-ins Znovu načíst zásuvné moduly + Type Text + Font Písmo + Recent Fonts Nedávná písma + Size Velikost + Glyphs Glyfy + Insert Special Character Vložit speciální znak + Bullet Odrážka + Copyright Symbol Symbol copyrightu + Ellipsis Výpustka + Paragraph Symbol Symbol odstavce + Registered Trademark Symbol Symbol registrované ochranné známky + Section Symbol Symbol paragrafu + Trademark Symbol Symbol ochranné známky + Em Dash Dlouhá pomlčka + En Dash Krátká pomlčka + Discretionary Hyphen Volitelný spojovník + Nonbreaking Hyphen Nezlomitelný spojovník + Double Left Quotation Marks Levé dvojité uvozovky + Double Right Quotation Marks Pravé dvojité uvozovky + Single Left Quotation Mark Levá jednoduchá uvozovka + Single Right Quotation Mark Pravá jednoduchá uvozovka + Insert Whitespace Character Vložit znak mezery + Em Space Čtverčíková mezera + En Space Půlčtverčíková mezera + Hair Space Vlasová mezera + Sixth Space Šestinová mezera + Thin Space Úzká mezera + Nonbreaking Space Nezlomitelná mezera + Figure Space Číslicová mezera + Punctuation Space Interpunkční mezera + Third Space Třetinová mezera + Quarter Space Čtvrtinová mezera + Insert Break Character Vložit znak zalomení + Forced Line Break Vynucené zalomení řádku + Tab Tabulátor + Convert To Area Type Převést na text v oblasti + Convert To Point Type Převést na bodový text + Area Type Options… Možnosti textu v oblasti… + Type on a Path Text na cestě + Rainbow Duha + Skew Zkosení + 3D Ribbon 3D stuha + Stair Step Schody + Gravity Gravitace + Type on a Path Options… Možnosti textu na cestě… + Threaded Text Zřetězený text + Create Vytvořit + Release Selection Uvolnit výběr + Remove Threading Odstranit zřetězení + Fit Headline Přizpůsobit titulek + Find Font… Najít písmo… + Change Case Změnit velikost písmen + UPPERCASE VELKÁ PÍSMENA + lowercase malá písmena + Title Case Velká Počáteční Písmena + Sentence case Velké první písmeno věty + Smart Punctuation… Inteligentní interpunkce… + Create Outlines Převést na křivky + Optical Margin Alignment Optické zarovnání okrajů + Fill with Placeholder Text Vyplnit zástupným textem + Show Hidden Characters Zobrazit skryté znaky + Hide Hidden Characters Skrýt skryté znaky + Type Orientation Orientace textu + Horizontal Vodorovně + Vertical Svisle + Select Výběr + All Vybrat vše + All on Active Artboard Vše na aktivní pracovní ploše + Deselect Zrušit výběr + Reselect Znovu vybrat + Inverse Invertovat výběr + Next Object Above Další objekt nad + Next Object Below Další objekt pod + Same Stejné + Appearance Vzhled + Appearance Attribute Atribut vzhledu + Blending Mode Režim míchání + Fill & Stroke Výplň a tah + Fill Color Barva výplně + Opacity Krytí + Stroke Color Barva tahu + Stroke Weight Tloušťka tahu + Graphic Style Grafický styl + Symbol Instance Instance symbolu + Font Family Rodina písma + Font Family & Style Rodina a řez písma + Font Family, Style & Size Rodina, řez a velikost písma + Font Size Velikost písma + Text Fill Color Barva výplně textu + Text Stroke Color Barva tahu textu + All on Same Layers Vše ve stejných vrstvách + Direction Handles Směrové úchyty + Brush Strokes Tahy štětcem + Bristle Brush Strokes Tahy štětinovým štětcem + Clipping Masks Ořezové masky + Slices Řezy + Stray Points Osamocené body + Open Paths Otevřené cesty + All Text Objects Všechny textové objekty + Point Text Objects Bodové textové objekty + Area Text Objects Textové objekty v oblasti + Start Global Edit Zahájit globální úpravy + Save Selection… Uložit výběr… + Edit Selection… Upravit výběr… + Effect Efekt + Apply Last Effect Použít poslední efekt + Last Effect… Poslední efekt… + Document Raster Effects Settings… Nastavení rastrových efektů dokumentu… + Vector Effects Vektorové efekty + 3D and Materials 3D a materiály + Extrude & Bevel… Vysunutí a zkosení… + Revolve… Rotační těleso… + Inflate… Nafouknutí… + Materials… Materiály… + Color Adjustments Úpravy barev + Brightness/Contrast… Jas/kontrast… + Curves… Křivky… + Hue/Saturation… Odstín/sytost… + Levels… Úrovně… + Shift to Color… Posun k barvě… + Temperature/Tint… Teplota/nádech… + Rectangle… Obdélník… + Rounded Rectangle… Zaoblený obdélník… + Ellipse… Elipsa… + Crop Marks Ořezové značky + Distort & Transform Deformace a transformace + Free Distort… Volná deformace… + Pucker & Bloat… Smrštění a vyboulení… + Roughen… Zdrsnění… + Transform… Transformovat… + Tweak… Náhodné posunutí… + Twist… Zkroucení… + Zig Zag… Cikcak… + Pathfinder Operace s cestami + Add Sjednotit + Intersect Protnout + Exclude Vyloučit + Subtract Odečíst + Minus Back Odečíst zadní + Divide Rozdělit + Trim Zastřihnout + Crop Oříznout + Outline Obrys + Stylize Stylizace + Round Corners… Zaoblit rohy… + Scribble… Čmáranice… + Drop Shadow… Vržený stín… + Inner Glow… Vnitřní záře… + Outer Glow… Vnější záře… + Feather… Měkké okraje… + SVG Filters Filtry SVG + Apply SVG Filter… Použít filtr SVG… + Warp Pokřivení + Arc… Oblouk… + Arc Lower… Dolní oblouk… + Arc Upper… Horní oblouk… + Arch… Klenba… + Bulge… Vydutí… + Shell Lower… Dolní mušle… + Shell Upper… Horní mušle… + Flag… Vlajka… + Wave… Vlna… + Fish… Ryba… + Rise… Stoupání… + Fisheye… Rybí oko… + Squeeze… Stlačení… + Raster Effects Rastrové efekty + Blur Rozostření + Gaussian Blur… Gaussovské rozostření… + View Zobrazení + Preview Náhled + Overprint Preview Náhled přetisků + Pixel Preview Náhled obrazových bodů + Trim View Oříznuté zobrazení + Presentation Mode Režim prezentace + Screen Mode Režim obrazovky + Normal Screen Mode Normální režim obrazovky + Full Screen Mode with Menu Bar Celá obrazovka s pruhem nabídek + Full Screen Mode Celá obrazovka + Proof Setup Nastavení nátisku + Working CMYK Pracovní CMYK + Legacy Macintosh RGB (Gamma 1.8) Starší Macintosh RGB (gama 1,8) + Internet Standard RGB (sRGB) Standardní internetové RGB (sRGB) + Monitor RGB RGB monitoru + Color blindness – Protanopia-type Barvoslepost – protanopie + Color blindness – Deuteranopia-type Barvoslepost – deuteranopie + Customize… Vlastní… + Proof Colors Barvy nátisku + Zoom In Zvětšit + Zoom Out Zmenšit + Fit Artboard in Window Přizpůsobit pracovní plochu oknu + Fit All in Window Přizpůsobit vše oknu + Actual Size Skutečná velikost + Reset Rotate View Obnovit otočení zobrazení + Hide Edges Skrýt okraje + Show Edges Zobrazit okraje + Hide Artboards Skrýt pracovní plochy + Show Artboards Zobrazit pracovní plochy + Show Print Tiling Zobrazit dlaždice tisku + Hide Print Tiling Skrýt dlaždice tisku + Hide Slices Skrýt řezy + Show Slices Zobrazit řezy + Lock Slices Zamknout řezy + Rulers Pravítka + Show Rulers Zobrazit pravítka + Hide Rulers Skrýt pravítka + Change to Global Rulers Přepnout na globální pravítka + Show Video Rulers Zobrazit video pravítka + Hide Bounding Box Skrýt ohraničovací rámeček + Show Bounding Box Zobrazit ohraničovací rámeček + Show Transparency Grid Zobrazit mřížku průhlednosti + Hide Transparency Grid Skrýt mřížku průhlednosti + Hide Text Threads Skrýt zřetězení textu + Show Text Threads Zobrazit zřetězení textu + Hide Gradient Annotator Skrýt ovladač přechodu + Show Gradient Annotator Zobrazit ovladač přechodu + Hide Corner Widget Skrýt ovladač rohů + Show Corner Widget Zobrazit ovladač rohů + Guides Vodítka + Hide Guides Skrýt vodítka + Show Guides Zobrazit vodítka + Lock Guides Zamknout vodítka + Unlock Guides Odemknout vodítka + Make Guides Vytvořit vodítka + Release Guides Uvolnit vodítka + Clear Guides Vymazat vodítka + Smart Guides Chytrá vodítka + Perspective Grid Perspektivní mřížka + Show Grid Zobrazit mřížku + Hide Grid Skrýt mřížku + Snap to Grid Přichytávat k mřížce + Lock Grid Zamknout mřížku + Unlock Grid Odemknout mřížku + Lock Station Point Zamknout bod pozorovatele + Define Grid… Definovat mřížku… + One Point Perspective Jednobodová perspektiva + Two Point Perspective Dvoubodová perspektiva + Three Point Perspective Tříbodová perspektiva + Save Grid as Preset… Uložit mřížku jako přednastavení… + Snap to Pixel Přichytávat k obrazovým bodům + Snap to Point Přichytávat k bodům + Snap to Glyph Přichytávat ke glyfům + New View… Nové zobrazení… + Edit Views… Upravit zobrazení… + Saved View 1 Uložené zobrazení 1 + Saved View 2 Uložené zobrazení 2 + Saved View 3 Uložené zobrazení 3 + Saved View 4 Uložené zobrazení 4 + Saved View 5 Uložené zobrazení 5 + Saved View 6 Uložené zobrazení 6 + Saved View 7 Uložené zobrazení 7 + Saved View 8 Uložené zobrazení 8 + Saved View 9 Uložené zobrazení 9 + Saved View 10 Uložené zobrazení 10 + Window Okno + New Window Nové okno + Cascade Do kaskády + Tile Vedle sebe + Float in Window Uvolnit do samostatného okna + Consolidate All Windows Sloučit všechna okna + Workspace Pracovní prostor + Reset Essentials Obnovit Essentials + New Workspace… Nový pracovní prostor… + Manage Workspaces… Spravovat pracovní prostory… + Control Ovládací panel + Contextual Task Bar Kontextový panel úloh + Tools Nástroje + Toolbars Panely nástrojů + Advanced Rozšířený + Single / Double Column Jeden / dva sloupce + Actions Akce + Asset Export Export prvků + Attributes Atributy + Brushes Štětce + Color Barva + Color Guide Průvodce barvami + Color Themes Barevné motivy + CSS Properties Vlastnosti CSS + Document Info Informace o dokumentu + Flattener Preview Náhled sloučení + Gradient Přechod + Graphic Styles Grafické styly + History Historie + Info Informace + Layers Vrstvy + Libraries Knihovny + Links Propojení + Magic Wand Kouzelná hůlka + Navigator Navigátor + Pattern Options Možnosti vzorku + Properties Vlastnosti + Separations Preview Náhled separací + Stroke Tah + SVG Interactivity Interaktivita SVG + Swatches Vzorník + Symbols Symboly + Transparency Průhlednost + Character Znak + Character Styles Styly znaků + Paragraph Odstavec + Paragraph Styles Styly odstavců + Tabs Tabulátory + Variables Proměnné + Brush Libraries Knihovny štětců + Built-in Libraries Vestavěné knihovny + User Defined Uživatelské + Other Library… Jiná knihovna… + Graphic Style Libraries Knihovny grafických stylů + Shadows and Glows Stíny a záře + Outlines and Rules Obrysy a linky + Hand-Drawn Ručně kreslené + Gradient Finishes Přechodové povrchy + Shape Effects Efekty tvarů + Blends and Transparency Prolnutí a průhlednost + Save Graphic Style Library… Uložit knihovnu grafických stylů… + Swatch Libraries Knihovny vzorníků + Default Swatches Výchozí vzorník + Web Safe 216 Webově bezpečné 216 + Grays and Neutrals Šedé a neutrální + Earth Tones Zemité tóny + Skin Tone Ramps Škály odstínů pleti + Pastels Pastelové + Brights Zářivé + Metallic Gradients Kovové přechody + Perceptual Scales Percepční škály + Harmony Sets Harmonické sady + Gradients Přechody + Spectrum Gradients Spektrální přechody + Sky Gradients Přechody oblohy + Neutral Gradients Neutrální přechody + Duotone Gradients Dvoutónové přechody + Radial Glows Kruhové záře + Save Swatch Library… Uložit knihovnu vzorníku… + Symbol Libraries Knihovny symbolů + Help Nápověda + ArtCraft Website Web ArtCraft + VectorCraft on getartcraft.com VectorCraft na getartcraft.com + VectorCraft on GitHub VectorCraft na GitHubu + Search Commands… Hledat příkazy… + VectorCraft Help… Nápověda VectorCraft… + Automatic Automaticky + Isolate Selected Group Izolovat vybranou skupinu + Exit Isolation Mode Ukončit izolovaný režim + Make Clipping Mask Vytvořit ořezovou masku + Release Clipping Mask Uvolnit ořezovou masku + Make Compound Path Vytvořit složenou cestu + Release Compound Path Uvolnit složenou cestu + Select All Vybrat vše diff --git a/crates/ui-egui/src/i18n/czech.rs b/crates/ui-egui/src/i18n/czech.rs deleted file mode 100644 index 897532b4f..000000000 --- a/crates/ui-egui/src/i18n/czech.rs +++ /dev/null @@ -1,578 +0,0 @@ -//! Czech (Čeština) menu labels, in menu order. -//! -//! Clean-room: every entry is written from the meaning of VectorCraft's English label, following -//! common Czech desktop-software conventions (commands in the infinitive, menus as nouns); no -//! other application's Czech localisation was consulted or copied. -//! -//! Left in English on purpose (see `KEEP_AS_IS` in the tests): the product name, language names, -//! OpenType, the built-in workspace names (names the workspace switcher and saved preferences -//! show as-is) and the bracketed perspective grid preset names (preset names stored in documents). - -pub(super) const CZECH: &[(&str, &str)] = &[ - // VectorCraft - ("About VectorCraft", "O aplikaci VectorCraft"), - ("Join Our Discord", "Připojit se k našemu Discordu"), - ("Settings…", "Nastavení…"), - ("Language", "Jazyk"), - ("UI Brightness", "Jas rozhraní"), - ("Dark", "Tmavý"), - ("Medium Dark", "Středně tmavý"), - ("Medium Light", "Středně světlý"), - ("Light", "Světlý"), - ("Quit VectorCraft", "Ukončit VectorCraft"), - // File - ("File", "Soubor"), - ("New…", "Nový…"), - ("New from Template…", "Nový ze šablony…"), - ("Open…", "Otevřít…"), - ("Open Recent Files", "Otevřít nedávné soubory"), - ("Recent File", "Nedávný soubor"), - ("Clear Recent Files", "Vymazat seznam nedávných souborů"), - ("Show in Folder", "Zobrazit ve složce"), - ("Close", "Zavřít"), - ("Close All", "Zavřít vše"), - ("Save", "Uložit"), - ("Save As…", "Uložit jako…"), - ("Save a Copy…", "Uložit kopii…"), - ("Save as Template…", "Uložit jako šablonu…"), - ("Save as PDF…", "Uložit jako PDF…"), - ("Save for Office Documents…", "Uložit pro kancelářské dokumenty…"), - ("Revert", "Vrátit k uložené verzi"), - ("Place…", "Umístit…"), - ("Export", "Export"), - ("Export for Screens…", "Exportovat pro obrazovky…"), - ("Export As…", "Exportovat jako…"), - ("Export As SVG…", "Exportovat jako SVG…"), - ("Export As PNG…", "Exportovat jako PNG…"), - ("Save for Web (Legacy)…", "Uložit pro web (starší)…"), - ("Export Selection…", "Exportovat výběr…"), - ("Package…", "Sbalit…"), - ("Scripts", "Skripty"), - ("Other Script…", "Jiný skript…"), - ("Document Setup…", "Nastavení dokumentu…"), - ("Document Color Mode", "Barevný režim dokumentu"), - ("CMYK Color", "Barvy CMYK"), - ("RGB Color", "Barvy RGB"), - ("File Info…", "Informace o souboru…"), - ("Print…", "Tisk…"), - // Edit - ("Edit", "Úpravy"), - ("Undo", "Zpět"), - ("Redo", "Znovu"), - ("Cut", "Vyjmout"), - ("Copy", "Kopírovat"), - ("Paste", "Vložit"), - ("Paste in Front", "Vložit dopředu"), - ("Paste in Back", "Vložit dozadu"), - ("Paste in Place", "Vložit na místo"), - ("Paste on All Artboards", "Vložit na všechny pracovní plochy"), - ("Paste without Formatting", "Vložit bez formátování"), - ("Clear", "Smazat"), - ("Find and Replace…", "Najít a nahradit…"), - ("Find Next", "Najít další"), - ("Spelling", "Pravopis"), - ("Auto Spell Check", "Automatická kontrola pravopisu"), - ("Check Spelling…", "Zkontrolovat pravopis…"), - ("Edit Custom Dictionary…", "Upravit vlastní slovník…"), - ("Edit Colors", "Upravit barvy"), - ("Recolor Artwork…", "Přebarvit kresbu…"), - ("Recolor with Preset", "Přebarvit podle přednastavení"), - ("1 Color Job…", "Zakázka s 1 barvou…"), - ("2 Color Job…", "Zakázka se 2 barvami…"), - ("3 Color Job…", "Zakázka se 3 barvami…"), - ("Color Library…", "Knihovna barev…"), - ("Adjust Color Balance…", "Upravit vyvážení barev…"), - ("Blend Front to Back", "Prolnout zepředu dozadu"), - ("Blend Horizontally", "Prolnout vodorovně"), - ("Blend Vertically", "Prolnout svisle"), - ("Convert to CMYK", "Převést na CMYK"), - ("Convert to Grayscale", "Převést na stupně šedi"), - ("Convert to RGB", "Převést na RGB"), - ("Invert Colors", "Invertovat barvy"), - ("Overprint Black…", "Přetisknout černou…"), - ("Saturate…", "Upravit sytost…"), - ("Edit Original", "Upravit originál"), - ("Transparency Flattener Presets…", "Přednastavení sloučení průhlednosti…"), - ("Print Presets…", "Přednastavení tisku…"), - ("PDF Presets…", "Přednastavení PDF…"), - ("Perspective Grid Presets…", "Přednastavení perspektivní mřížky…"), - ("Color Settings…", "Nastavení barev…"), - ("Assign Profile…", "Přiřadit profil…"), - ("Keyboard Shortcuts…", "Klávesové zkratky…"), - ("Preferences…", "Předvolby…"), - // Object - ("Object", "Objekt"), - ("Transform", "Transformace"), - ("Transform Again", "Transformovat znovu"), - ("Move…", "Přesunout…"), - ("Rotate…", "Otočit…"), - ("Reflect…", "Zrcadlit…"), - ("Scale…", "Změnit velikost…"), - ("Shear…", "Zkosit…"), - ("Transform Each…", "Transformovat jednotlivě…"), - ("Reset Bounding Box", "Obnovit ohraničovací rámeček"), - ("Arrange", "Uspořádat"), - ("Bring to Front", "Přenést do popředí"), - ("Bring Forward", "Přenést blíž"), - ("Send Backward", "Přesunout dál"), - ("Send to Back", "Přesunout do pozadí"), - ("Send to Current Layer", "Přesunout do aktuální vrstvy"), - ("Align", "Zarovnat"), - ("Horizontal Align Left", "Vodorovně zarovnat doleva"), - ("Horizontal Align Center", "Vodorovně zarovnat na střed"), - ("Horizontal Align Right", "Vodorovně zarovnat doprava"), - ("Vertical Align Top", "Svisle zarovnat nahoru"), - ("Vertical Align Center", "Svisle zarovnat na střed"), - ("Vertical Align Bottom", "Svisle zarovnat dolů"), - ("Group", "Seskupit"), - ("Ungroup", "Oddělit"), - ("Lock", "Zamknout"), - ("Selection", "Výběr"), - ("All Artwork Above", "Veškerou kresbu výše"), - ("Other Layers", "Ostatní vrstvy"), - ("Unlock All", "Odemknout vše"), - ("Hide", "Skrýt"), - ("Show All", "Zobrazit vše"), - ("Expand…", "Rozložit…"), - ("Expand Appearance", "Rozložit vzhled"), - ("Crop Image", "Oříznout obraz"), - ("Rasterize…", "Rastrovat…"), - ("Create Gradient Mesh…", "Vytvořit přechodovou síť…"), - ("Create Object Mosaic…", "Vytvořit mozaiku objektů…"), - ("Vector Halftone…", "Vektorový polotón…"), - ("Create Trim Marks", "Vytvořit značky ořezu"), - ("Flatten Transparency…", "Sloučit průhlednost…"), - ("Make Pixel Perfect", "Zarovnat na obrazové body"), - ("Slice", "Řez"), - ("Make", "Vytvořit"), - ("Release", "Uvolnit"), - ("Create from Guides", "Vytvořit z vodítek"), - ("Create from Selection", "Vytvořit z výběru"), - ("Duplicate Slice", "Duplikovat řez"), - ("Combine Slices", "Spojit řezy"), - ("Divide Slices…", "Rozdělit řezy…"), - ("Delete All", "Odstranit vše"), - ("Slice Options…", "Možnosti řezu…"), - ("Clip to Artboard", "Oříznout na pracovní plochu"), - ("Path", "Cesta"), - ("Join", "Spojit"), - ("Average…", "Zprůměrovat…"), - ("Outline Stroke", "Převést tah na obrys"), - ("Offset Path…", "Posunout cestu…"), - ("Reverse Path Direction", "Obrátit směr cesty"), - ("Simplify…", "Zjednodušit…"), - ("Add Anchor Points", "Přidat kotevní body"), - ("Remove Anchor Points", "Odstranit kotevní body"), - ("Divide Objects Below", "Rozdělit objekty pod"), - ("Split Into Grid…", "Rozdělit do mřížky…"), - ("Clean Up…", "Vyčistit…"), - ("Shape", "Tvar"), - ("Convert to Shape", "Převést na tvar"), - ("Expand Shape", "Rozložit tvar"), - ("Pattern", "Vzorek"), - ("Edit Pattern", "Upravit vzorek"), - ("Tile Edge Color…", "Barva okraje dlaždice…"), - ("Repeat", "Opakování"), - ("Radial", "Kruhové"), - ("Grid", "Mřížkové"), - ("Mirror", "Zrcadlové"), - ("Options…", "Možnosti…"), - ("Blend", "Prolnutí"), - ("Blend Options…", "Možnosti prolnutí…"), - ("Expand", "Rozložit"), - ("Replace Spine", "Nahradit páteř"), - ("Reverse Spine", "Obrátit páteř"), - ("Reverse Front to Back", "Obrátit pořadí zepředu dozadu"), - ("Envelope Distort", "Deformace obálkou"), - ("Make with Warp…", "Vytvořit pokřivením…"), - ("Reset with Warp…", "Obnovit pokřivením…"), - ("Make with Mesh…", "Vytvořit sítí…"), - ("Reset with Mesh…", "Obnovit sítí…"), - ("Make with Top Object", "Vytvořit horním objektem"), - ("Envelope Options…", "Možnosti obálky…"), - ("Edit Contents", "Upravit obsah"), - ("Edit Envelope", "Upravit obálku"), - ("Perspective", "Perspektiva"), - ("Attach to Active Plane", "Připojit k aktivní rovině"), - ("Release with Perspective", "Uvolnit s perspektivou"), - ("Move Plane to Match Object", "Posunout rovinu k objektu"), - ("Edit Text", "Upravit text"), - ("Live Paint", "Živá malba"), - ("Merge", "Sloučit"), - ("Gap Options…", "Možnosti mezer…"), - ("Image Trace", "Vektorizace obrazu"), - ("Make…", "Vytvořit…"), - ("Make and Expand…", "Vytvořit a rozložit…"), - ("Text Wrap", "Obtékání textu"), - ("Text Wrap Options…", "Možnosti obtékání textu…"), - ("Clipping Mask", "Ořezová maska"), - ("Edit Clipping Path", "Upravit ořezovou cestu"), - ("Compound Path", "Složená cesta"), - ("Artboards", "Pracovní plochy"), - ("Convert to Artboards", "Převést na pracovní plochy"), - ("Rearrange All Artboards…", "Přeuspořádat všechny pracovní plochy…"), - ("Fit to Artwork Bounds", "Přizpůsobit hranicím kresby"), - ("Fit to Selected Art", "Přizpůsobit vybrané kresbě"), - ("Graph", "Graf"), - ("Type…", "Typ…"), - ("Data…", "Údaje…"), - ("Design…", "Návrh…"), - ("Column…", "Sloupec…"), - ("Marker…", "Značka…"), - ("Collect for Export", "Shromáždit pro export"), - ("As Single Asset", "Jako jeden prvek"), - ("As Multiple Assets", "Jako více prvků"), - ("Plug-ins", "Zásuvné moduly"), - ("No filter plug-ins installed", "Nejsou nainstalovány žádné filtry"), - ("Install Plug-in…", "Nainstalovat zásuvný modul…"), - ("Reload Plug-ins", "Znovu načíst zásuvné moduly"), - // Type - ("Type", "Text"), - ("Font", "Písmo"), - ("Recent Fonts", "Nedávná písma"), - ("Recent Font", "Nedávné písmo"), - ("Size", "Velikost"), - ("Glyphs", "Glyfy"), - ("Insert Special Character", "Vložit speciální znak"), - ("Bullet", "Odrážka"), - ("Copyright Symbol", "Symbol copyrightu"), - ("Ellipsis", "Výpustka"), - ("Paragraph Symbol", "Symbol odstavce"), - ("Registered Trademark Symbol", "Symbol registrované ochranné známky"), - ("Section Symbol", "Symbol paragrafu"), - ("Trademark Symbol", "Symbol ochranné známky"), - ("Em Dash", "Dlouhá pomlčka"), - ("En Dash", "Krátká pomlčka"), - ("Discretionary Hyphen", "Volitelný spojovník"), - ("Nonbreaking Hyphen", "Nezlomitelný spojovník"), - ("Double Left Quotation Marks", "Levé dvojité uvozovky"), - ("Double Right Quotation Marks", "Pravé dvojité uvozovky"), - ("Single Left Quotation Mark", "Levá jednoduchá uvozovka"), - ("Single Right Quotation Mark", "Pravá jednoduchá uvozovka"), - ("Insert Whitespace Character", "Vložit znak mezery"), - ("Em Space", "Čtverčíková mezera"), - ("En Space", "Půlčtverčíková mezera"), - ("Hair Space", "Vlasová mezera"), - ("Sixth Space", "Šestinová mezera"), - ("Thin Space", "Úzká mezera"), - ("Nonbreaking Space", "Nezlomitelná mezera"), - ("Figure Space", "Číslicová mezera"), - ("Punctuation Space", "Interpunkční mezera"), - ("Third Space", "Třetinová mezera"), - ("Quarter Space", "Čtvrtinová mezera"), - ("Insert Break Character", "Vložit znak zalomení"), - ("Forced Line Break", "Vynucené zalomení řádku"), - ("Tab", "Tabulátor"), - ("Convert To Area Type", "Převést na text v oblasti"), - ("Convert To Point Type", "Převést na bodový text"), - ("Area Type Options…", "Možnosti textu v oblasti…"), - ("Type on a Path", "Text na cestě"), - ("Rainbow", "Duha"), - ("Skew", "Zkosení"), - ("3D Ribbon", "3D stuha"), - ("Stair Step", "Schody"), - ("Gravity", "Gravitace"), - ("Type on a Path Options…", "Možnosti textu na cestě…"), - ("Threaded Text", "Zřetězený text"), - ("Create", "Vytvořit"), - ("Release Selection", "Uvolnit výběr"), - ("Remove Threading", "Odstranit zřetězení"), - ("Fit Headline", "Přizpůsobit titulek"), - ("Find Font…", "Najít písmo…"), - ("Change Case", "Změnit velikost písmen"), - ("UPPERCASE", "VELKÁ PÍSMENA"), - ("lowercase", "malá písmena"), - ("Title Case", "Velká Počáteční Písmena"), - ("Sentence case", "Velké první písmeno věty"), - ("Smart Punctuation…", "Inteligentní interpunkce…"), - ("Create Outlines", "Převést na křivky"), - ("Optical Margin Alignment", "Optické zarovnání okrajů"), - ("Fill with Placeholder Text", "Vyplnit zástupným textem"), - ("Show Hidden Characters", "Zobrazit skryté znaky"), - ("Hide Hidden Characters", "Skrýt skryté znaky"), - ("Type Orientation", "Orientace textu"), - ("Horizontal", "Vodorovně"), - ("Vertical", "Svisle"), - // Select - ("Select", "Výběr"), - ("All", "Vybrat vše"), - ("All on Active Artboard", "Vše na aktivní pracovní ploše"), - ("Deselect", "Zrušit výběr"), - ("Reselect", "Znovu vybrat"), - ("Inverse", "Invertovat výběr"), - ("Next Object Above", "Další objekt nad"), - ("Next Object Below", "Další objekt pod"), - ("Same", "Stejné"), - ("Appearance", "Vzhled"), - ("Appearance Attribute", "Atribut vzhledu"), - ("Blending Mode", "Režim míchání"), - ("Fill & Stroke", "Výplň a tah"), - ("Fill Color", "Barva výplně"), - ("Opacity", "Krytí"), - ("Stroke Color", "Barva tahu"), - ("Stroke Weight", "Tloušťka tahu"), - ("Graphic Style", "Grafický styl"), - ("Symbol Instance", "Instance symbolu"), - ("Font Family", "Rodina písma"), - ("Font Family & Style", "Rodina a řez písma"), - ("Font Family, Style & Size", "Rodina, řez a velikost písma"), - ("Font Size", "Velikost písma"), - ("Text Fill Color", "Barva výplně textu"), - ("Text Stroke Color", "Barva tahu textu"), - ("All on Same Layers", "Vše ve stejných vrstvách"), - ("Direction Handles", "Směrové úchyty"), - ("Brush Strokes", "Tahy štětcem"), - ("Bristle Brush Strokes", "Tahy štětinovým štětcem"), - ("Clipping Masks", "Ořezové masky"), - ("Slices", "Řezy"), - ("Stray Points", "Osamocené body"), - ("Open Paths", "Otevřené cesty"), - ("All Text Objects", "Všechny textové objekty"), - ("Point Text Objects", "Bodové textové objekty"), - ("Area Text Objects", "Textové objekty v oblasti"), - ("Start Global Edit", "Zahájit globální úpravy"), - ("Save Selection…", "Uložit výběr…"), - ("Edit Selection…", "Upravit výběr…"), - // Effect - ("Effect", "Efekt"), - ("Apply Last Effect", "Použít poslední efekt"), - ("Last Effect…", "Poslední efekt…"), - ("Document Raster Effects Settings…", "Nastavení rastrových efektů dokumentu…"), - ("Vector Effects", "Vektorové efekty"), - ("3D and Materials", "3D a materiály"), - ("Extrude & Bevel…", "Vysunutí a zkosení…"), - ("Revolve…", "Rotační těleso…"), - ("Inflate…", "Nafouknutí…"), - ("Materials…", "Materiály…"), - ("Color Adjustments", "Úpravy barev"), - ("Brightness/Contrast…", "Jas/kontrast…"), - ("Curves…", "Křivky…"), - ("Hue/Saturation…", "Odstín/sytost…"), - ("Levels…", "Úrovně…"), - ("Shift to Color…", "Posun k barvě…"), - ("Temperature/Tint…", "Teplota/nádech…"), - ("Rectangle…", "Obdélník…"), - ("Rounded Rectangle…", "Zaoblený obdélník…"), - ("Ellipse…", "Elipsa…"), - ("Crop Marks", "Ořezové značky"), - ("Distort & Transform", "Deformace a transformace"), - ("Free Distort…", "Volná deformace…"), - ("Pucker & Bloat…", "Smrštění a vyboulení…"), - ("Roughen…", "Zdrsnění…"), - ("Transform…", "Transformovat…"), - ("Tweak…", "Náhodné posunutí…"), - ("Twist…", "Zkroucení…"), - ("Zig Zag…", "Cikcak…"), - ("Pathfinder", "Operace s cestami"), - ("Add", "Sjednotit"), - ("Intersect", "Protnout"), - ("Exclude", "Vyloučit"), - ("Subtract", "Odečíst"), - ("Minus Back", "Odečíst zadní"), - ("Divide", "Rozdělit"), - ("Trim", "Zastřihnout"), - ("Crop", "Oříznout"), - ("Outline", "Obrys"), - ("Stylize", "Stylizace"), - ("Round Corners…", "Zaoblit rohy…"), - ("Scribble…", "Čmáranice…"), - ("Drop Shadow…", "Vržený stín…"), - ("Inner Glow…", "Vnitřní záře…"), - ("Outer Glow…", "Vnější záře…"), - ("Feather…", "Měkké okraje…"), - ("SVG Filters", "Filtry SVG"), - ("Apply SVG Filter…", "Použít filtr SVG…"), - ("Warp", "Pokřivení"), - ("Arc…", "Oblouk…"), - ("Arc Lower…", "Dolní oblouk…"), - ("Arc Upper…", "Horní oblouk…"), - ("Arch…", "Klenba…"), - ("Bulge…", "Vydutí…"), - ("Shell Lower…", "Dolní mušle…"), - ("Shell Upper…", "Horní mušle…"), - ("Flag…", "Vlajka…"), - ("Wave…", "Vlna…"), - ("Fish…", "Ryba…"), - ("Rise…", "Stoupání…"), - ("Fisheye…", "Rybí oko…"), - ("Squeeze…", "Stlačení…"), - ("Raster Effects", "Rastrové efekty"), - ("Blur", "Rozostření"), - ("Gaussian Blur…", "Gaussovské rozostření…"), - // View - ("View", "Zobrazení"), - ("Preview", "Náhled"), - ("Overprint Preview", "Náhled přetisků"), - ("Pixel Preview", "Náhled obrazových bodů"), - ("Trim View", "Oříznuté zobrazení"), - ("Presentation Mode", "Režim prezentace"), - ("Screen Mode", "Režim obrazovky"), - ("Normal Screen Mode", "Normální režim obrazovky"), - ("Full Screen Mode with Menu Bar", "Celá obrazovka s pruhem nabídek"), - ("Full Screen Mode", "Celá obrazovka"), - ("Proof Setup", "Nastavení nátisku"), - ("Working CMYK", "Pracovní CMYK"), - ("Legacy Macintosh RGB (Gamma 1.8)", "Starší Macintosh RGB (gama 1,8)"), - ("Internet Standard RGB (sRGB)", "Standardní internetové RGB (sRGB)"), - ("Monitor RGB", "RGB monitoru"), - ("Color blindness – Protanopia-type", "Barvoslepost – protanopie"), - ("Color blindness – Deuteranopia-type", "Barvoslepost – deuteranopie"), - ("Customize…", "Vlastní…"), - ("Proof Colors", "Barvy nátisku"), - ("Zoom In", "Zvětšit"), - ("Zoom Out", "Zmenšit"), - ("Fit Artboard in Window", "Přizpůsobit pracovní plochu oknu"), - ("Fit All in Window", "Přizpůsobit vše oknu"), - ("Actual Size", "Skutečná velikost"), - ("Reset Rotate View", "Obnovit otočení zobrazení"), - ("Hide Edges", "Skrýt okraje"), - ("Show Edges", "Zobrazit okraje"), - ("Hide Artboards", "Skrýt pracovní plochy"), - ("Show Artboards", "Zobrazit pracovní plochy"), - ("Show Print Tiling", "Zobrazit dlaždice tisku"), - ("Hide Print Tiling", "Skrýt dlaždice tisku"), - ("Hide Slices", "Skrýt řezy"), - ("Show Slices", "Zobrazit řezy"), - ("Lock Slices", "Zamknout řezy"), - ("Rulers", "Pravítka"), - ("Show Rulers", "Zobrazit pravítka"), - ("Hide Rulers", "Skrýt pravítka"), - ("Change to Global Rulers", "Přepnout na globální pravítka"), - ("Show Video Rulers", "Zobrazit video pravítka"), - ("Hide Bounding Box", "Skrýt ohraničovací rámeček"), - ("Show Bounding Box", "Zobrazit ohraničovací rámeček"), - ("Show Transparency Grid", "Zobrazit mřížku průhlednosti"), - ("Hide Transparency Grid", "Skrýt mřížku průhlednosti"), - ("Hide Text Threads", "Skrýt zřetězení textu"), - ("Show Text Threads", "Zobrazit zřetězení textu"), - ("Hide Gradient Annotator", "Skrýt ovladač přechodu"), - ("Show Gradient Annotator", "Zobrazit ovladač přechodu"), - ("Hide Corner Widget", "Skrýt ovladač rohů"), - ("Show Corner Widget", "Zobrazit ovladač rohů"), - ("Guides", "Vodítka"), - ("Hide Guides", "Skrýt vodítka"), - ("Show Guides", "Zobrazit vodítka"), - ("Lock Guides", "Zamknout vodítka"), - ("Unlock Guides", "Odemknout vodítka"), - ("Make Guides", "Vytvořit vodítka"), - ("Release Guides", "Uvolnit vodítka"), - ("Clear Guides", "Vymazat vodítka"), - ("Smart Guides", "Chytrá vodítka"), - ("Perspective Grid", "Perspektivní mřížka"), - ("Show Grid", "Zobrazit mřížku"), - ("Hide Grid", "Skrýt mřížku"), - ("Snap to Grid", "Přichytávat k mřížce"), - ("Lock Grid", "Zamknout mřížku"), - ("Unlock Grid", "Odemknout mřížku"), - ("Lock Station Point", "Zamknout bod pozorovatele"), - ("Define Grid…", "Definovat mřížku…"), - ("One Point Perspective", "Jednobodová perspektiva"), - ("User Preset", "Uživatelské přednastavení"), - ("Two Point Perspective", "Dvoubodová perspektiva"), - ("Three Point Perspective", "Tříbodová perspektiva"), - ("Save Grid as Preset…", "Uložit mřížku jako přednastavení…"), - ("Snap to Pixel", "Přichytávat k obrazovým bodům"), - ("Snap to Point", "Přichytávat k bodům"), - ("Snap to Glyph", "Přichytávat ke glyfům"), - ("New View…", "Nové zobrazení…"), - ("Edit Views…", "Upravit zobrazení…"), - ("Saved View 1", "Uložené zobrazení 1"), - ("Saved View 2", "Uložené zobrazení 2"), - ("Saved View 3", "Uložené zobrazení 3"), - ("Saved View 4", "Uložené zobrazení 4"), - ("Saved View 5", "Uložené zobrazení 5"), - ("Saved View 6", "Uložené zobrazení 6"), - ("Saved View 7", "Uložené zobrazení 7"), - ("Saved View 8", "Uložené zobrazení 8"), - ("Saved View 9", "Uložené zobrazení 9"), - ("Saved View 10", "Uložené zobrazení 10"), - // Window - ("Window", "Okno"), - ("New Window", "Nové okno"), - ("Cascade", "Do kaskády"), - ("Tile", "Vedle sebe"), - ("Float in Window", "Uvolnit do samostatného okna"), - ("Consolidate All Windows", "Sloučit všechna okna"), - ("Workspace", "Pracovní prostor"), - ("Reset Essentials", "Obnovit Essentials"), - ("New Workspace…", "Nový pracovní prostor…"), - ("Manage Workspaces…", "Spravovat pracovní prostory…"), - ("Control", "Ovládací panel"), - ("Contextual Task Bar", "Kontextový panel úloh"), - ("Tools", "Nástroje"), - ("Toolbars", "Panely nástrojů"), - ("Advanced", "Rozšířený"), - ("Single / Double Column", "Jeden / dva sloupce"), - ("Actions", "Akce"), - ("Asset Export", "Export prvků"), - ("Attributes", "Atributy"), - ("Brushes", "Štětce"), - ("Color", "Barva"), - ("Color Guide", "Průvodce barvami"), - ("Color Themes", "Barevné motivy"), - ("CSS Properties", "Vlastnosti CSS"), - ("Document Info", "Informace o dokumentu"), - ("Flattener Preview", "Náhled sloučení"), - ("Gradient", "Přechod"), - ("Graphic Styles", "Grafické styly"), - ("History", "Historie"), - ("Info", "Informace"), - ("Layers", "Vrstvy"), - ("Libraries", "Knihovny"), - ("Links", "Propojení"), - ("Magic Wand", "Kouzelná hůlka"), - ("Navigator", "Navigátor"), - ("Pattern Options", "Možnosti vzorku"), - ("Properties", "Vlastnosti"), - ("Separations Preview", "Náhled separací"), - ("Stroke", "Tah"), - ("SVG Interactivity", "Interaktivita SVG"), - ("Swatches", "Vzorník"), - ("Symbols", "Symboly"), - ("Transparency", "Průhlednost"), - ("Character", "Znak"), - ("Character Styles", "Styly znaků"), - ("Paragraph", "Odstavec"), - ("Paragraph Styles", "Styly odstavců"), - ("Tabs", "Tabulátory"), - ("Variables", "Proměnné"), - ("Brush Libraries", "Knihovny štětců"), - ("Built-in Libraries", "Vestavěné knihovny"), - ("User Defined", "Uživatelské"), - ("Other Library…", "Jiná knihovna…"), - ("Graphic Style Libraries", "Knihovny grafických stylů"), - ("Shadows and Glows", "Stíny a záře"), - ("Outlines and Rules", "Obrysy a linky"), - ("Hand-Drawn", "Ručně kreslené"), - ("Gradient Finishes", "Přechodové povrchy"), - ("Shape Effects", "Efekty tvarů"), - ("Blends and Transparency", "Prolnutí a průhlednost"), - ("User Library", "Uživatelská knihovna"), - ("Save Graphic Style Library…", "Uložit knihovnu grafických stylů…"), - ("Swatch Libraries", "Knihovny vzorníků"), - ("Default Swatches", "Výchozí vzorník"), - ("Web Safe 216", "Webově bezpečné 216"), - ("Grays and Neutrals", "Šedé a neutrální"), - ("Earth Tones", "Zemité tóny"), - ("Skin Tone Ramps", "Škály odstínů pleti"), - ("Pastels", "Pastelové"), - ("Brights", "Zářivé"), - ("Metallic Gradients", "Kovové přechody"), - ("Perceptual Scales", "Percepční škály"), - ("Harmony Sets", "Harmonické sady"), - ("Gradients", "Přechody"), - ("Spectrum Gradients", "Spektrální přechody"), - ("Sky Gradients", "Přechody oblohy"), - ("Neutral Gradients", "Neutrální přechody"), - ("Duotone Gradients", "Dvoutónové přechody"), - ("Radial Glows", "Kruhové záře"), - ("Save Swatch Library…", "Uložit knihovnu vzorníku…"), - ("Symbol Libraries", "Knihovny symbolů"), - // Help - ("Help", "Nápověda"), - ("ArtCraft Website", "Web ArtCraft"), - ("VectorCraft on getartcraft.com", "VectorCraft na getartcraft.com"), - ("VectorCraft on GitHub", "VectorCraft na GitHubu"), - ("Search Commands…", "Hledat příkazy…"), - ("VectorCraft Help…", "Nápověda VectorCraft…"), -]; diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv new file mode 100644 index 000000000..86fae124b --- /dev/null +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -0,0 +1,42 @@ +# Japanese (ja) catalog. Same format as zh-hant.tsv: context source translation +# (empty context = a plain UI string; `@id` = a command id; `@plural` = "one|other" forms). +# +# Coverage: the main menus. Untranslated text falls back to English, so rows can be added one at a +# time; set `complete_menus` in i18n/mod.rs once every menu string and `tl!` literal has a row. +# Every row's source must be a string the UI shows (menu label or `tl!` literal): tested. + Copy コピー + Color カラー + Cut 切り取り + Deselect 選択を解除 + Edit 編集 + Effect 効果 + Export 書き出し + Export As… 形式を指定して書き出し… + File ファイル + Help ヘルプ + History 履歴 + Horizontal 横書き + Language 表示言語 + Layers レイヤー + New… 新規… + Object オブジェクト + Open… 開く… + Paste 貼り付け + Preferences… 環境設定… + Print… 印刷… + Properties プロパティ + Redo やり直し + Revert 保存済みの状態に戻す + Save 保存 + Save As… 別名で保存… + Select 選択 + Settings… 環境設定… + Tools ツール + Type 書式 + Type Orientation 組み方向 + Undo 取り消し + Vertical 縦書き + View 表示 + Window ウィンドウ + Zoom In ズームイン + Zoom Out ズームアウト diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs new file mode 100644 index 000000000..ebacd8b9f --- /dev/null +++ b/crates/ui-egui/src/i18n/mod.rs @@ -0,0 +1,308 @@ +//! UI localisation. Strings in code stay English and are the default lookup keys; a per-language +//! catalog (`*.tsv`, see `zh-hant.tsv` for the format) maps them to display text at render time. +//! Command ids, menu paths used for logic, the control channel, the CLI and MCP never see +//! translated text, so agents and scripts are unaffected. A string without a translation is shown +//! in English. +//! +//! # Adding a language +//! 1. Add `xx.tsv` next to `zh-hant.tsv` (copy its header; translate from the *meaning* of the +//! English text, clean-room, see `zh-hant.tsv`). +//! 2. Add one row to [`LANGUAGES`] (code, native name, catalog, plural rule). +//! +//! That is all: the Preferences dropdown, the system-locale match and the catalog tests (parse, +//! placeholders, plural forms, menu coverage) pick it up from the registry. +//! +//! # Looking strings up +//! - [`t`] / the `tl!` macro: a plain string in the language the UI is drawn in. [`tr`] takes the +//! language explicitly; [`tr_ctx`] when one English word needs different translations. +//! - [`tr_id`]: a command-id keyed string with the English label as fallback (menu items), so a +//! translation survives rewording of the English text and can differ per command. +//! - [`trn`]: plural-aware (`{n}` is filled in). [`fmt`]: fill `{name}` placeholders after [`tr`]; +//! translators may reorder placeholders freely. + +mod catalog; + +use std::sync::OnceLock; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use catalog::Catalog; + +/// One supported UI language. +pub struct LangInfo { + /// BCP 47 code, lowercase (`ja`, `zh-hant`, `pt-br`). Also the `interfaceLanguage` value. + pub code: &'static str, + /// The language's name in itself, shown in the Preferences dropdown. + pub name: &'static str, + /// Catalog file contents (empty for the built-in English). + pub source: &'static str, + /// Plural form index for a count (English: 0 = one, 1 = other; Chinese: always 0). + pub plural: fn(u64) -> usize, + /// Must the catalog cover every menu string? (checked by the tests) + pub complete_menus: bool, + catalog: OnceLock, +} + +fn plural_one_other(n: u64) -> usize { + usize::from(n != 1) +} + +fn plural_none(_: u64) -> usize { + 0 +} + +/// Czech: 1 is `one`, 2–4 `few`, everything else (0, 5…) `other`. +fn plural_czech(n: u64) -> usize { + match n { + 1 => 0, + 2..=4 => 1, + _ => 2, + } +} + +/// The registry. English first: it is the fallback and the source language. +pub static LANGUAGES: [LangInfo; 4] = [ + LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() }, + // Japanese: the main menus so far (untranslated text stays English); `complete_menus` once the + // catalog covers every menu string and `tl!` literal. + LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, complete_menus: false, catalog: OnceLock::new() }, + // Czech: every menu label (`czech_translates_every_menu_label`); panels and dialogs not yet. + LangInfo { code: "cs", name: "Čeština", source: include_str!("cs.tsv"), plural: plural_czech, complete_menus: false, catalog: OnceLock::new() }, + // Traditional Chinese in the vocabulary used in Taiwan; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-*` + // locales all resolve here (see `candidates`). + LangInfo { + code: "zh-hant", + name: "繁體中文", + source: include_str!("zh-hant.tsv"), + plural: plural_none, + complete_menus: true, + catalog: OnceLock::new(), + }, +]; + +impl LangInfo { + fn catalog(&self) -> &Catalog { + self.catalog.get_or_init(|| Catalog::parse(self.source)) + } +} + +/// A language the UI can be shown in (a handle into [`LANGUAGES`]). +#[derive(Clone, Copy)] +pub struct Lang(&'static LangInfo); + +impl std::fmt::Debug for Lang { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Lang({})", self.0.code) + } +} + +impl PartialEq for Lang { + fn eq(&self, other: &Self) -> bool { + self.0.code == other.0.code + } +} + +impl Eq for Lang {} + +impl Lang { + pub const EN: Lang = Lang(&LANGUAGES[0]); + + pub fn code(self) -> &'static str { + self.0.code + } + + /// A language by its exact code. + pub fn from_code(code: &str) -> Option { + LANGUAGES.iter().find(|l| l.code.eq_ignore_ascii_case(code)).map(Lang) + } + + /// Resolve the `interfaceLanguage` preference: a language code, or `auto` (and anything + /// unknown, e.g. a code from a newer version) to follow the system locale. + pub fn from_pref(pref: &str) -> Lang { + Lang::from_code(pref).unwrap_or_else(system_lang) + } + + /// Every registered language. + pub fn all() -> impl Iterator { + LANGUAGES.iter().map(Lang) + } + + pub fn name(self) -> &'static str { + self.0.name + } + + /// Does this language's catalog claim to cover every menu string and `tl!` literal? + pub fn complete_menus(self) -> bool { + self.0.complete_menus + } + + fn catalog(self) -> &'static Catalog { + self.0.catalog() + } +} + +/// Candidate language codes for a locale tag, most specific first: `zh_TW.UTF-8` → +/// `zh-tw`, `zh-hant`, `zh`. +fn candidates(tag: &str) -> Vec { + let base = tag.split(['.', '@']).next().unwrap_or("").replace('_', "-").to_ascii_lowercase(); + let parts: Vec<&str> = base.split('-').filter(|p| !p.is_empty()).collect(); + let Some(&primary) = parts.first() else { return Vec::new() }; + let mut out = Vec::new(); + for n in (1..=parts.len()).rev() { + out.push(parts[..n].join("-")); + } + if primary == "zh" && !parts.iter().any(|p| matches!(*p, "hans" | "hant")) { + // Chinese by region when no script is given. + let script = if parts.iter().any(|p| matches!(*p, "tw" | "hk" | "mo")) { "zh-hant" } else { "zh-hans" }; + out.insert(out.len() - 1, script.to_string()); + } + out +} + +/// The registered language for a locale tag such as `ja_JP.UTF-8`, `ja-JP`, `zh-TW`; `None` if +/// the language isn't supported. `C`/`POSIX` mean English. +pub fn lang_from_tag(tag: &str) -> Option { + let cands = candidates(tag); + if matches!(cands.first().map(String::as_str), Some("c" | "posix")) { + return Some(Lang::EN); + } + cands.iter().find_map(|c| Lang::from_code(c)) +} + +/// The system language (cached). English when it can't be determined. +pub fn system_lang() -> Lang { + // Tests drive the UI by its English labels whatever the developer's locale is. + if cfg!(test) { + return Lang::EN; + } + static SYSTEM: OnceLock = OnceLock::new(); + *SYSTEM.get_or_init(detect_system_lang) +} + +#[cfg(not(target_arch = "wasm32"))] +fn detect_system_lang() -> Lang { + // `VECTORCRAFT_LOCALE` overrides everything (also handy for screenshots); then the POSIX + // variables. `LANGUAGE` is a colon-separated priority list. + for var in ["VECTORCRAFT_LOCALE", "LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"] { + let Some(v) = std::env::var(var).ok().filter(|v| !v.is_empty()) else { continue }; + if let Some(l) = v.split(':').find_map(lang_from_tag) { + return l; + } + } + // Apps started from the Finder don't inherit LANG: use the macOS preferred-languages list. + // The absolute path keeps a `defaults` earlier on PATH from running; any failure means English. + #[cfg(target_os = "macos")] + if let Ok(out) = std::process::Command::new("/usr/bin/defaults").args(["read", "-g", "AppleLanguages"]).output() + && out.status.success() + && let Some(l) = first_supported(&String::from_utf8_lossy(&out.stdout)) + { + return l; + } + // Windows sets no LANG: read the user locale (`HKCU\Control Panel\International` › + // `LocaleName`, e.g. `zh-TW`). + #[cfg(target_os = "windows")] + if let Some(l) = windows_locale().as_deref().and_then(lang_from_tag) { + return l; + } + Lang::EN +} + +/// The first supported language in a `defaults read` list like `(\n "ja-JP",\n "en-US"\n)`. +#[cfg_attr(not(target_os = "macos"), allow(dead_code))] +fn first_supported(list: &str) -> Option { + list.split(['(', ')', ',', '"', '\n']).map(str::trim).filter(|s| !s.is_empty()).find_map(lang_from_tag) +} + +/// The Windows user locale name (`reg query` so no extra dependency; a hidden console window). +#[cfg(target_os = "windows")] +fn windows_locale() -> Option { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + // reg.exe by its full path, so nothing named `reg` in the app's folder or on PATH runs instead. + let root = std::env::var_os("SystemRoot").map(std::path::PathBuf::from).unwrap_or_else(|| r"C:\Windows".into()); + let out = std::process::Command::new(root.join("System32").join("reg.exe")) + .args(["query", "HKCU\\Control Panel\\International", "/v", "LocaleName"]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .ok()?; + registry_locale(&String::from_utf8_lossy(&out.stdout)) +} + +/// The value in `reg query` output like ` LocaleName REG_SZ zh-TW`. +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +fn registry_locale(text: &str) -> Option { + text.lines().find(|l| l.contains("LocaleName")).and_then(|l| l.split_whitespace().last()).map(str::to_string) +} + +#[cfg(target_arch = "wasm32")] +fn detect_system_lang() -> Lang { + Lang::EN +} + +/// Index into [`LANGUAGES`] of the language the UI is drawn in this frame. +static CURRENT: AtomicUsize = AtomicUsize::new(0); + +/// Set the UI language for drawing (the shell calls this once per frame from the preference), so +/// widgets can translate without every call site carrying a language around. +pub fn set_current(lang: Lang) { + let i = LANGUAGES.iter().position(|l| l.code == lang.code()).unwrap_or(0); + CURRENT.store(i, Ordering::Relaxed); +} + +/// The language the UI is drawn in. +pub fn current() -> Lang { + Lang(LANGUAGES.get(CURRENT.load(Ordering::Relaxed)).unwrap_or(&LANGUAGES[0])) +} + +/// Does `lang` have a catalog entry for this plain string? (English never does: it is the source.) +pub fn has(lang: Lang, s: &str) -> bool { + lang.catalog().plain(s).is_some() +} + +/// Translate an English UI string into the current language ([`tr`] with [`current`]). +pub fn t(s: &str) -> &str { + tr(current(), s) +} + +/// Translate an English UI string; unknown strings come back unchanged. +pub fn tr(lang: Lang, s: &str) -> &str { + lang.catalog().plain(s).unwrap_or(s) +} + +/// Like [`tr`], for an English string that needs a disambiguating `context`. +pub fn tr_ctx<'a>(lang: Lang, context: &str, s: &'a str) -> &'a str { + lang.catalog().contextual(context, s).unwrap_or_else(|| tr(lang, s)) +} + +/// A string keyed by its command id, falling back to the translation of the English `label`. +pub fn tr_id<'a>(lang: Lang, id: &str, label: &'a str) -> &'a str { + lang.catalog().id(id).unwrap_or_else(|| tr(lang, label)) +} + +/// [`tr_id`] in the current language. +pub fn t_id<'a>(id: &str, label: &'a str) -> &'a str { + tr_id(current(), id, label) +} + +/// Fill `{name}` placeholders. Unknown placeholders are left as written. +pub fn fmt(template: &str, args: &[(&str, &str)]) -> String { + let mut out = template.to_string(); + for (k, v) in args { + out = out.replace(&format!("{{{k}}}"), v); + } + out +} + +/// A plural-aware message: `one`/`other` are the English forms (with `{n}` where the count goes). +pub fn trn(lang: Lang, n: u64, one: &str, other: &str) -> String { + let idx = (lang.0.plural)(n); + let text = lang.catalog().plural(one, other, idx).unwrap_or(if n == 1 { one } else { other }); + fmt(text, &[("n", &n.to_string())]) +} + +/// [`trn`] in the current language. +pub fn tn(n: u64, one: &str, other: &str) -> String { + trn(current(), n, one, other) +} + +#[cfg(test)] +mod tests; diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs new file mode 100644 index 000000000..2d030a03d --- /dev/null +++ b/crates/ui-egui/src/i18n/tests.rs @@ -0,0 +1,488 @@ +use super::catalog::{Catalog, parse_entries, placeholders}; +use super::*; + +const ZH: fn() -> Lang = || Lang::from_code("zh-hant").expect("zh-hant registered"); + +#[test] +fn tags_map_to_languages() { + assert_eq!(lang_from_tag("en_US.UTF-8"), Some(Lang::EN)); + assert_eq!(lang_from_tag("C"), Some(Lang::EN)); + assert_eq!(lang_from_tag("POSIX"), Some(Lang::EN)); + assert_eq!(lang_from_tag("fr_FR"), None); + assert_eq!(lang_from_tag("ja_JP.UTF-8"), Lang::from_code("ja")); + assert_eq!(lang_from_tag("ja"), Lang::from_code("ja")); + // Traditional Chinese: by region, by script, and with a region after the script. + assert_eq!(lang_from_tag("zh_TW.UTF-8"), Some(ZH())); + assert_eq!(lang_from_tag("zh-TW"), Some(ZH())); + assert_eq!(lang_from_tag("zh-HK"), Some(ZH())); + assert_eq!(lang_from_tag("zh_MO"), Some(ZH())); + assert_eq!(lang_from_tag("zh-Hant"), Some(ZH())); + assert_eq!(lang_from_tag("zh-Hant-TW"), Some(ZH())); + assert_eq!(lang_from_tag("zh-Hant-HK"), Some(ZH())); + assert_eq!(lang_from_tag("zh_TW.UTF-8@radical"), Some(ZH())); + // Simplified Chinese locales never pick up the Traditional catalog (they resolve to a + // `zh-hans` catalog once one is registered, and to English until then). + for tag in ["zh-CN", "zh_CN.UTF-8", "zh_SG", "zh-Hans", "zh-Hans-CN", "zh"] { + assert_ne!(lang_from_tag(tag), Some(ZH()), "{tag}"); + } + assert_eq!(lang_from_tag(""), None); + assert_eq!(lang_from_tag("_"), None); +} + +#[test] +fn candidates_walk_from_specific_to_general() { + assert_eq!(candidates("pt_BR.UTF-8"), ["pt-br", "pt"]); + assert_eq!(candidates("zh_TW"), ["zh-tw", "zh-hant", "zh"]); + assert_eq!(candidates("zh-CN"), ["zh-cn", "zh-hans", "zh"]); + assert_eq!(candidates("zh-Hant-HK"), ["zh-hant-hk", "zh-hant", "zh"]); +} + +#[test] +fn os_language_lists_are_parsed() { + assert_eq!(first_supported("(\n \"zh-Hant-TW\",\n \"en-US\"\n)\n"), Some(ZH())); + assert_eq!(first_supported("(\n \"fr-FR\",\n \"en-US\"\n)\n"), Some(Lang::EN)); + assert_eq!(first_supported("("), None); + // Windows: `reg query HKCU\Control Panel\International /v LocaleName`. + let reg = "\r\nHKEY_CURRENT_USER\\Control Panel\\International\r\n LocaleName REG_SZ zh-TW\r\n\r\n"; + assert_eq!(registry_locale(reg).as_deref(), Some("zh-TW")); + assert_eq!(registry_locale(reg).as_deref().and_then(lang_from_tag), Some(ZH())); + assert_eq!(registry_locale("ERROR: The system was unable to find the specified registry key or value."), None); +} + +#[test] +fn preferences_resolve_with_fallback() { + assert_eq!(Lang::from_pref("zh-hant"), ZH()); + assert_eq!(Lang::from_pref("ZH-Hant"), ZH()); + assert_eq!(Lang::from_pref("en"), Lang::EN); + // `auto` and unknown codes follow the system (English under test). + assert_eq!(Lang::from_pref("auto"), Lang::EN); + assert_eq!(Lang::from_pref("xx-unknown"), Lang::EN); +} + +#[test] +fn current_language_is_a_frame_setting() { + set_current(ZH()); + assert_eq!(current(), ZH()); + assert_eq!(t("Layers"), "圖層"); + set_current(Lang::EN); + assert_eq!(current(), Lang::EN); + assert_eq!(t("Layers"), "Layers"); +} + +#[test] +fn lookups_fall_back_to_english() { + assert_eq!(tr(Lang::EN, "Layers"), "Layers"); + assert_eq!(tr(ZH(), "Layers"), "圖層"); + assert_eq!(tr(ZH(), "no such label"), "no such label"); + assert_eq!(tr_id(ZH(), "no.such.id", "Layers"), "圖層"); + assert_eq!(tr_ctx(ZH(), "no such context", "Layers"), "圖層"); + assert!(has(ZH(), "Layers")); + assert!(!has(Lang::EN, "Layers")); +} + +#[test] +fn catalog_kinds_are_parsed_and_looked_up() { + let c = Catalog::parse("# c\n\tHello\t你好\n@id\tfile.save\t儲存\nmenu\tWindows\t視窗們\n@plural\t{n} file|{n} files\t{n} 個檔案\n\n"); + assert_eq!(c.plain("Hello"), Some("你好")); + assert_eq!(c.id("file.save"), Some("儲存")); + assert_eq!(c.contextual("menu", "Windows"), Some("視窗們")); + assert_eq!(c.contextual("other", "Windows"), None); + assert_eq!(c.plural("{n} file", "{n} files", 0), Some("{n} 個檔案")); + assert_eq!(c.plural("{n} file", "{n} files", 5), Some("{n} 個檔案"), "an index past the forms clamps"); +} + +#[test] +fn malformed_lines_are_reported_not_fatal() { + let (entries, errors) = parse_entries("\tok\t好\nno tabs here\n\tonly\n\ta\tb\tc\textra\n\t\tempty source\n"); + assert_eq!(entries.len(), 1); + assert_eq!(errors.len(), 4, "{errors:?}"); + assert_eq!(parse_entries("\ta\\tb\tx\\ny\\\\z\n").0[0], (String::new(), "a\tb".into(), "x\ny\\z".into())); +} + +#[test] +fn plurals_and_placeholders() { + assert_eq!(trn(Lang::EN, 1, "{n} item", "{n} items"), "1 item"); + assert_eq!(trn(Lang::EN, 0, "{n} item", "{n} items"), "0 items"); + assert_eq!(trn(Lang::EN, 7, "{n} item", "{n} items"), "7 items"); + assert_eq!(trn(ZH(), 1, "{n} item", "{n} items"), "1 個項目"); + assert_eq!(trn(ZH(), 7, "{n} item", "{n} items"), "7 個項目"); + assert_eq!(fmt("{b} before {a}", &[("a", "x"), ("b", "y"), ("c", "z")]), "y before x"); + assert_eq!(fmt("{missing}", &[]), "{missing}"); + assert_eq!(placeholders("a {x} b {y} {"), ["x", "y"]); +} + +/// Every bundled catalog is well-formed and consistent with its sources. +#[test] +fn bundled_catalogs_are_consistent() { + for l in &LANGUAGES { + assert!(l.code == l.code.to_ascii_lowercase() && !l.name.is_empty(), "{}", l.code); + let (entries, errors) = parse_entries(l.source); + assert!(errors.is_empty(), "{}: {errors:?}", l.code); + let mut seen = std::collections::HashSet::new(); + for (ctx, src, tr) in &entries { + assert!(seen.insert((ctx.clone(), src.clone())), "{}: duplicate {ctx:?} {src:?}", l.code); + if ctx == "@plural" { + let one_other: Vec<&str> = src.split('|').collect(); + assert_eq!(one_other.len(), 2, "{}: plural source must be `one|other`: {src:?}", l.code); + for form in tr.split('|') { + let mut want = placeholders(one_other[1]); + let mut got = placeholders(form); + want.sort_unstable(); + got.sort_unstable(); + assert_eq!(want, got, "{}: placeholders differ in {src:?}", l.code); + } + continue; + } + let mut want = placeholders(src); + let mut got = placeholders(tr); + want.sort_unstable(); + got.sort_unstable(); + assert_eq!(want, got, "{}: placeholders differ in {src:?}", l.code); + if ctx.is_empty() { + assert_eq!(src.ends_with('…'), tr.ends_with('…'), "{}: ellipsis mismatch: {src:?}", l.code); + } + if ctx == "@id" { + let known = + crate::menus::UI_COMMANDS.iter().any(|c| c.0 == src) || vectorcraft_engine::cmd::command_specs().iter().any(|c| c.id == src); + assert!(known, "{}: unknown command id {src:?}", l.code); + } + } + } +} + +/// A catalog that doesn't claim complete menus may still only hold strings the UI shows (menu +/// labels, command labels, `tl!` literals), so labels from other apps don't creep in. +#[test] +fn partial_catalogs_only_translate_strings_the_ui_shows() { + let known: std::collections::BTreeSet = crate::menus::menu_strings().into_iter().chain(tl_literals()).collect(); + for l in LANGUAGES.iter().filter(|l| !l.complete_menus && !l.source.is_empty()) { + let (entries, _) = parse_entries(l.source); + let unknown: Vec<_> = entries.iter().filter(|(ctx, src, _)| ctx.is_empty() && !known.contains(src)).map(|(_, src, _)| src).collect(); + assert!(unknown.is_empty(), "{}: not UI strings: {unknown:?}", l.code); + } +} + +/// VectorCraft › Language (`app.language`) sets the `interfaceLanguage` preference, which is what +/// persists; the checked item follows the preference, and a bad code is an error, not a change. +#[test] +fn language_command_sets_the_preference() { + use crate::menus::{checked, run_ui_command}; + let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + assert_eq!(checked(&app, "app.language", &serde_json::json!({"lang": "auto"})), Some(true)); + run_ui_command(&mut app, "app.language", &serde_json::json!({"lang": "ja"})).unwrap().unwrap(); + assert_eq!(app.session.prefs.interface_language, "ja"); + assert_eq!(app.ui_language(), Lang::from_code("ja").unwrap()); + assert_eq!(checked(&app, "app.language", &serde_json::json!({"lang": "ja"})), Some(true)); + assert_eq!(checked(&app, "app.language", &serde_json::json!({"lang": "auto"})), Some(false)); + assert_eq!(checked(&app, "app.language", &serde_json::json!({"lang": "zh-hant"})), Some(false)); + let err = run_ui_command(&mut app, "app.language", &serde_json::json!({"lang": "xx"})).unwrap().unwrap_err(); + assert!(err.contains("auto") && err.contains("zh-hant"), "{err}"); + assert_eq!(app.session.prefs.interface_language, "ja"); + run_ui_command(&mut app, "app.language", &serde_json::json!({"lang": "auto"})).unwrap().unwrap(); + assert_eq!(app.session.prefs.interface_language, "auto"); + assert_eq!(checked(&app, "app.language", &serde_json::json!({"lang": "auto"})), Some(true)); + // Every registered language is in the menu, by its own name. + let labels: Vec = + crate::menus::menu_entries(&app).into_iter().filter(|e| e.command.as_deref() == Some("app.language")).map(|e| e.label).collect(); + for l in Lang::all() { + assert!(labels.iter().any(|x| x == l.name()), "{} missing from {labels:?}", l.code()); + } +} + +/// Chinese and Japanese characters (CJK punctuation, kana, ideographs, full-width forms). +fn is_cjk(c: char) -> bool { + matches!(c as u32, 0x3000..=0x9FFF | 0xF900..=0xFAFF | 0xFF00..=0xFFEF | 0x20000..=0x2FFFF) +} + +/// The UI fonts draw every character the catalogs use, in every family the UI draws text with +/// (the web build has no system fonts to fall back to). Chinese and Japanese characters come from +/// the craft-fonts build input, which is optional: without it they are left to the installed +/// fonts, and with it the Japanese catalog is checked against BIZ UDPGothic (the Traditional +/// Chinese one waits for a Traditional Chinese UI font in craft-fonts). +#[test] +fn bundled_fonts_cover_the_catalogs() { + let japanese_ui_font = vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.is_japanese()); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut output = ctx.run_ui(egui::RawInput::default(), |_| {}); + output.textures_delta.clear(); + let mut missing = std::collections::BTreeMap::new(); + ctx.fonts_mut(|fonts| { + let families = [ + egui::FontFamily::Proportional, + egui::FontFamily::Name(crate::theme::FONT_UI.into()), + egui::FontFamily::Name(crate::theme::FONT_UI_SEMIBOLD.into()), + egui::FontFamily::Name(crate::theme::FONT_MONO.into()), + ]; + for l in LANGUAGES.iter().filter(|l| !l.source.is_empty()) { + let (entries, _) = parse_entries(l.source); + let cjk_checked = japanese_ui_font && l.code == "ja"; + let chars: std::collections::BTreeSet = + entries.iter().flat_map(|(_, _, tr)| tr.chars()).filter(|c| !c.is_whitespace() && (cjk_checked || !is_cjk(*c))).collect(); + for family in &families { + let font = egui::FontId::new(13.0, family.clone()); + let gone: String = chars.iter().filter(|c| !fonts.has_glyph(&font, **c)).collect(); + if !gone.is_empty() { + missing.insert(format!("{} in {family:?}", l.code), gone); + } + } + } + }); + assert!(missing.is_empty(), "characters without a glyph: {missing:#?}"); +} + +/// The Traditional Chinese catalog holds no Simplified-only characters (a translation written +/// in the wrong script would read as a typo to every user). +#[test] +fn zh_hant_has_no_simplified_characters() { + const SIMPLIFIED_ONLY: &str = "们这为来时间个说对会发现过还没动开关图层选设项编辑显视帮删预览导节线连择报错误处据库经体应该样点击确认标记录输进转换调约维护启闭锁义类网络页颜绘画宽长边缘缩镜复贴渐滤笔钢区饱参数变号单双属组齐径锚轮阴阳实际让从产东车门问闪并坚测试运术压缩叠饰够亚质纸张档缺省认识"; + let (entries, _) = parse_entries(ZH().0.source); + let mut bad = Vec::new(); + for (ctx, src, tr) in entries { + if let Some(c) = tr.chars().find(|c| SIMPLIFIED_ONLY.contains(*c)) { + bad.push(format!("{ctx} {src:?} → {tr:?} has {c:?}")); + } + } + assert!(bad.is_empty(), "simplified characters in zh-hant.tsv:\n{}", bad.join("\n")); +} + +/// Every menu string (top-level titles, submenu names, item labels, section headers, UI and +/// engine command labels and menu paths) has an entry in each language that claims complete menus. +#[test] +fn complete_languages_translate_every_menu_string() { + let strings = crate::menus::menu_strings(); + assert!(strings.len() > 500, "menu scan found only {} strings", strings.len()); + for l in LANGUAGES.iter().filter(|l| l.complete_menus) { + let cat = l.catalog(); + let missing: Vec<_> = strings.iter().filter(|s| cat.plain(s).is_none()).collect(); + assert!(missing.is_empty(), "{}: {} untranslated menu strings: {missing:#?}", l.code, missing.len()); + } +} + +/// Every `tl!("literal")` in the shell has an entry in each language that claims complete menus +/// (so a new label can't ship untranslated by accident). Literals that are deliberately shown as +/// they are (names, units) are listed in `KEEP_AS_IS`. +#[test] +fn every_tl_literal_is_translated() { + const KEEP_AS_IS: &[&str] = &[]; + let literals = tl_literals(); + assert!(literals.len() > 300, "scan found only {} literals", literals.len()); + for l in LANGUAGES.iter().filter(|l| l.complete_menus) { + let cat = l.catalog(); + let missing: Vec<_> = literals.iter().filter(|s| !KEEP_AS_IS.contains(&s.as_str()) && cat.plain(s).is_none()).collect(); + assert!(missing.is_empty(), "{}: {} untranslated tl! strings: {missing:#?}", l.code, missing.len()); + } +} + +/// Every `tl!("…")` literal in this crate's sources (test modules aside). +pub fn tl_literals() -> std::collections::BTreeSet { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut literals = std::collections::BTreeSet::new(); + let mut stack = vec![dir]; + while let Some(d) = stack.pop() { + for entry in std::fs::read_dir(&d).into_iter().flatten().flatten() { + let path = entry.path(); + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if path.is_dir() { + stack.push(path); + } else if name.ends_with(".rs") && name != "lib.rs" && !name.starts_with("tests") { + let text = std::fs::read_to_string(&path).unwrap_or_default(); + let code = text.split("#[cfg(test)]").next().unwrap_or(""); + let mut rest = code; + while let Some(at) = rest.find("tl!(\"") { + rest = &rest[at + 5..]; + let mut end = 0; + let bytes = rest.as_bytes(); + while end < bytes.len() && !(bytes[end] == b'"' && (end == 0 || bytes[end - 1] != b'\\')) { + end += 1; + } + let lit = rest.get(..end).unwrap_or("").replace("\\\"", "\""); + if rest.get(end + 1..end + 2) == Some(")") { + literals.insert(lit); + } + } + } + } + } + literals +} + +/// `VECTORCRAFT_I18N_DUMP= cargo test -p vectorcraft-ui-egui dump_source_strings` writes every +/// English source string a complete catalog has to cover, one per line, for translators. +#[test] +fn dump_source_strings() { + let Ok(path) = std::env::var("VECTORCRAFT_I18N_DUMP") else { return }; + let mut all = crate::menus::menu_strings(); + all.extend(tl_literals()); + let text: String = all.iter().map(|s| format!("{}\n", s.replace('\\', "\\\\").replace('\n', "\\n").replace('\t', "\\t"))).collect(); + std::fs::write(path, text).expect("write dump"); +} + +fn cs() -> Lang { + Lang::from_code("cs").expect("cs registered") +} + +/// Menu labels Czech shows as they are: the product name, a format name, the built-in workspace +/// names and the perspective grid presets (names, shown untranslated wherever else they appear). +/// Each language's own name in the Language menu is left alone too. +const CZECH_KEEP_AS_IS: &[&str] = &[ + "VectorCraft", + "OpenType", + "Essentials", + "Essentials Classic", + "Automation", + "Layout", + "Painting", + "Printing and Proofing", + "Tracing", + "Typography", + "Web", + "[1P-Normal View]", + "[1P-Low View]", + "[1P-High View]", + "[2P-Normal View]", + "[2P-Low View]", + "[2P-High View]", + "[3P-Normal View]", + "[3P-Low View]", +]; + +/// Every menu title, submenu, item and header label with its command and params (`""`/null for +/// the ones that run nothing). +fn menu_labels() -> Vec<(&'static str, &'static str, serde_json::Value)> { + use crate::menus::Item; + fn walk(items: &[Item], out: &mut Vec<(&'static str, &'static str, serde_json::Value)>) { + for i in items { + match i { + Item::Cmd(l, id, p) => out.push((l, id, p.clone())), + Item::Todo(l, _) | Item::Header(l) => out.push((l, "", serde_json::Value::Null)), + Item::Sub(l, children) => { + out.push((l, "", serde_json::Value::Null)); + walk(children, out); + } + Item::Sep => {} + } + } + } + let mut labels = vec![]; + for (title, items) in crate::menus::menu_tree() { + labels.push((title, "", serde_json::Value::Null)); + walk(&items, &mut labels); + } + labels +} + +/// The labels toggling items switch to ("Show Guides" once guides are hidden…), from a document +/// with every toggle flipped. +fn toggled_labels() -> Vec { + use serde_json::json; + let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); + app.run("object.envelope.makeWithWarp", json!({})).unwrap(); + let toggles = [ + "view.outline", + "view.edges", + "view.cornerWidget", + "view.textThreads", + "type.hiddenCharacters", + "view.gradientAnnotator", + "view.artboards", + "view.rulers", + "view.boundingBox", + "view.transparencyGrid", + "view.guides", + "view.grid", + "view.guides.lock", + "view.slices.hide", + "view.printTiling", + "perspective.grid.show", + "perspective.grid.rulers", + "perspective.grid.lock", + "object.envelope.editContents", + ]; + let mut labels = vec![]; + for id in toggles { + let before = crate::menus::dynamic_label(&app, id, ""); + app.run(id, json!({})).unwrap(); + let after = crate::menus::dynamic_label(&app, id, ""); + assert_ne!(before, after, "{id} didn't toggle its label"); + labels.extend([before, after]); + } + labels +} + +/// Czech covers every menu label, the Show/Hide pairs included (panels and dialogs not yet). +#[test] +fn czech_translates_every_menu_label() { + let labels = menu_labels(); + // What the menus show (recent files and fonts, user presets and libraries show their names). + let shown = crate::menus::menu_strings(); + let language_name = |l: &str| Lang::all().any(|lang| lang.name() == l); + // Font names and sizes, and installed plug-ins' own names, are not interface text. + let interface = |(label, id, p): &(&str, &str, serde_json::Value)| { + let plugin = *id == "plugin.dialog" || p.get("effect").and_then(serde_json::Value::as_str).is_some_and(|e| e.starts_with("plugin.")); + *id != "text.setStyle" && !plugin && shown.contains(*label) && !CZECH_KEEP_AS_IS.contains(label) && !language_name(label) + }; + let mut missing: Vec = labels.iter().filter(|l| interface(l)).map(|(l, ..)| l.to_string()).collect(); + missing.extend(toggled_labels()); + missing.extend(crate::menus::CONTEXT_LABELS.iter().map(|l| l.to_string())); + missing.retain(|l| !has(cs(), l)); + missing.sort(); + missing.dedup(); + assert!(missing.is_empty(), "untranslated Czech menu labels: {missing:?}"); + for keep in CZECH_KEEP_AS_IS { + assert!(labels.iter().any(|(l, ..)| l == keep), "`{keep}` isn't a menu label"); + assert!(!has(cs(), keep), "`{keep}` is both kept and translated"); + } + assert_eq!(tr(cs(), "File"), "Soubor"); +} + +#[test] +fn czech_plurals_have_three_forms() { + let forms: Vec = [0, 1, 2, 4, 5, 11, 21].into_iter().map(plural_czech).collect(); + assert_eq!(forms, [2, 0, 1, 1, 2, 2, 2]); +} + +/// Czech letters (and the punctuation Czech text uses) come from each family's own first font, not +/// from a fallback further down the stack. (`has_glyph` can't tell: it counts characters of the +/// face that draws missing glyphs, the first one, as missing.) +#[test] +fn czech_glyphs_are_available_without_system_fonts() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut output = ctx.run_ui(egui::RawInput::default(), |_| {}); + output.textures_delta.clear(); + ctx.fonts_mut(|fonts| { + let families: Vec<_> = fonts.definitions().families.iter().map(|(f, stack)| (f.clone(), stack.first().cloned())).collect(); + for (family, first) in families { + let first = first.unwrap(); + let mut font = fonts.fonts.font(&family); + let chars = font.characters(); + for ch in "áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ„“‚‘…–".chars() { + assert!(chars.get(&ch).is_some_and(|fonts| fonts.contains(&first)), "{first} ({family:?}) has no {ch}"); + } + } + }); +} + +/// A language chosen in an older version (saved with the UI state as `"language"`) carries over to +/// the `interfaceLanguage` preference, and isn't written back. +#[test] +fn a_language_saved_by_an_older_version_carries_over() { + for (saved, want) in [("ja", "ja"), ("cs", "cs"), ("en", "auto"), ("xx", "auto")] { + let ui: crate::state::UiState = serde_json::from_value(serde_json::json!({"language": saved})).unwrap(); + let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + app.ui = ui; + crate::prefs_dialog::restore(&mut app); + assert_eq!(app.session.prefs.interface_language, want, "saved {saved}"); + assert!(serde_json::to_value(&app.ui).unwrap().get("language").is_none()); + } + // A preference already set wins over the old field. + let ui: crate::state::UiState = + serde_json::from_value(serde_json::json!({"language": "ja", "engine_prefs": {"interfaceLanguage": "cs"}})).unwrap(); + let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + app.ui = ui; + crate::prefs_dialog::restore(&mut app); + assert_eq!(app.session.prefs.interface_language, "cs"); +} diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv new file mode 100644 index 000000000..d3a3ba7de --- /dev/null +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -0,0 +1,2899 @@ +# Traditional Chinese (zh-hant) catalog. Columns, separated by a TAB: context source translation +# +# context empty a plain UI string; `source` is the English text in the code +# @id a command id (e.g. file.saveAs): wins over the English text for +# that menu item, so one id can read differently from its twin +# @plural `source` is "one|other" (English forms); Chinese has one form, so +# the translation is a single string (see `plural` in i18n/mod.rs) +# anything else a disambiguating context passed to `tr_ctx` +# escapes \n \t \\ inside a column. `{name}` placeholders must appear in both columns. +# lines starting with # and blank lines are ignored. +# +# Clean-room: every entry was written from the plain meaning of the English label using the +# vector-drawing vocabulary in everyday use in Taiwan (錨點, 路徑, 筆畫, 填色, 工作區域, 路徑管理員 ...). +# No proprietary localisation resource, string table or product UI was consulted or copied; add +# entries the same way. Product and technology names (VectorCraft, ArtCraft, SVG, OpenType, CMYK, +# Lab ...) and paper sizes (Letter, Tabloid) stay in Latin letters; the trailing "…" of dialog +# commands is kept. Full-width punctuation (,。:) inside sentences; half-width parentheses and +# the " · " separators as in the English. Terms shared with PhotoCraft use the same words as its +# zh-hant catalog (圖層, 選取, 偏好設定, 混合模式 ...), so the two apps read alike. + + — missing — 遺失 + to search every command 以搜尋所有指令 + + Add Scale + 新增縮放比例 + 0.2 (quality) to 100 (speed) 0.2 (品質) 至 100 (速度) + 1 Color Job… 1 色印刷作業… + 1 {unit} on the artboard stands for {real} {unit} in the scene. 工作區域上的1 {unit}代表場景中的{real} {unit}。 + 2 Color Job… 2 色印刷作業… + 3 Color Job… 3 色印刷作業… + 30° Angles 30° 角度 + 3D and Materials 3D 與材質 + 3D Ribbon 3D 緞帶 + 45° Angles 45° 角度 + 60° Angles 60° 角度 + 90° & 45° & 30° Angles 90°、45° 與 30° 角度 + 90° & 45° Angles 90° 與 45° 角度 + 90° Angles 90° 角度 + < 1 sec < 1 秒 + <不透明度遮色片> + [1P-High View] [一點透視-高視角] + [1P-Low View] [一點透視-低視角] + [1P-Normal View] [一點透視-正常視角] + [2P-High View] [兩點透視-高視角] + [2P-Low View] [兩點透視-低視角] + [2P-Normal View] [兩點透視-正常視角] + [3P-Low View] [三點透視-低視角] + [3P-Normal View] [三點透視-正常視角] + [Default] is protected: New… starts an editable copy. [預設] 受到保護:「新增…」會開始一份可編輯的複本。 + [Unnamed] [未命名] + A File… 一個檔案… + A new preset starting from the selected one 以所選預設集為基礎建立新預設集 + A saved copy of the selected preset 所選預設集的已儲存拷貝 + About VectorCraft 關於 VectorCraft + above 以上 + Absolute Position 絕對位置 + Abutting 相鄰 + Action {n} 動作 {n} + Actions 動作 + Activate Document 啟用文件 + Active Perspective Plane 作用中的透視平面 + Actual Size 實際尺寸 + Add 新增 + Add Anchor (Pen) 增加錨點 (鋼筆) + Add Anchor Point 增加錨點 + Add Anchor Point Tool 增加錨點工具 + Add Anchor Points 增加錨點 + Add Effect 新增效果 + Add Freeform Line 新增任意線條 + Add Freeform Point 新增任意形狀錨點 + Add Guide 新增參考線 + Add Mesh Line 新增網格線 + Add New 新增 + Add New Effect 新增效果 + Add New Fill 新增填色 + Add New Stroke 新增筆畫 + Add Selected Artwork (Alt: as one asset) 新增所選圖稿 (Alt:合為一個資產) + Add Selected Artwork as Multiple Assets 將所選圖稿新增為多個資產 + Add Selected Artwork as One Asset 將所選圖稿新增為一個資產 + Add Spine Point 新增脊線點 + Add Swatches 新增色票 + Add to Graphic Styles 新增至繪圖樣式 + Add to Profiles 新增至描述檔 + Add to Profiles… 加入描述檔… + Add to Selection 加入選取範圍 + Add to Swatches 新增至色票 + Add Used Colors 新增使用中的顏色 + Additional Plug-ins Folder 額外的增效模組資料夾 + Adjust Adjoining Width Points 調整相鄰寬度點 + Adjust Color Balance… 調整色彩平衡… + Adjust Colors 調整顏色 + Adjust Options: 調整選項: + Advanced 進階 + Advanced Options 進階選項 + Align 對齊 + Align center 置中對齊 + Align left 靠左對齊 + Align Objects: 對齊物件: + Align On: 對齊於: + Align right 靠右對齊 + Align Stroke to Center 筆畫對齊中央 + Align Stroke to Inside 筆畫對齊內側 + Align Stroke to Outside 筆畫對齊外側 + Align Stroke: 對齊筆畫: + Align to Artboard 對齊工作區域 + Align to Glyph Bounds 對齊字符邊界 + Align to Key Object 對齊關鍵物件 + Align to Page 對齊頁面 + Align to Path 對齊路徑 + Align to Selection 對齊選取範圍 + Align To: 對齊至: + Align: 對齊: + Alignment Guides 對齊參考線 + Alignment: 對齊方式: + All 全部 + All Artwork Above 上方所有圖稿 + All Caps 全部大寫 + All Categories 所有類別 + All colours are within the CMYK gamut 所有顏色皆在 CMYK 色域內 + All on Active Artboard 作用中工作區域上的全部 + All on Same Layers 相同圖層上的全部 + All printer's marks 所有印表機標記 + All Text Objects 所有文字物件 + All the art 所有圖稿 + All Tools 所有工具 + Alt Alt + Alt: Alt: + Alter Paths for Appearance 依外觀調整路徑 + Amount 數量 + Amount: 數量: + Anchor Point 錨點 + Anchor Point Tool 錨點工具 + Anchor Points 錨點 + Anchor Points, Handle, and Bounding Box Display 錨點、控制把手與邊框顯示 + Anchor/Path Labels 錨點/路徑標籤 + Angle 角度 + Angle: 角度: + Angles 角度 + Animated Zoom 動畫縮放 + Anti-alias 消除鋸齒 + Anti-Alias 消除鋸齒 + Anti-alias Rasters 點陣影像消除鋸齒 + Anti-aliased Artwork 消除鋸齒的圖稿 + Anti-aliasing: 消除鋸齒: + Appearance 外觀 + Appearance Attribute 外觀屬性 + Appearance Item 外觀項目 + Appearance of Black 黑色外觀 + Appearance Panel 外觀面板 + Appearance panel 外觀面板 + Applies when the linked file is relinked or updated. 重新連結或更新連結檔案時套用。 + Apply Brush 套用筆刷 + Apply Character Style 套用字元樣式 + Apply Effect 套用效果 + Apply Graphic Style 套用繪圖樣式 + Apply Last Color 套用上次的顏色 + Apply Last Effect 套用上次的效果 + Apply Last Gradient 套用上次的漸層 + Apply Paragraph Style 套用段落樣式 + Apply SVG Filter… 套用 SVG 濾鏡… + Apply to All 全部套用 + Apply to All ({count} more missing) 全部套用 (另有 {count} 個遺失) + Arc 弧形 + Arc Lower… 下弧形… + Arc Tool 弧形工具 + Arc Upper… 上弧形… + Arch… 拱形… + Arc… 弧形… + Area / Path Type 區域/路徑文字 + Area Graph Tool 面積圖工具 + Area Text Objects 區域文字物件 + Area Type Options 區域文字選項 + Area Type Options… 區域文字選項… + Area Type Tool 區域文字工具 + Around Object 繞物件 + Arrange 排列順序 + Arrange: Bring to Front 排列順序:移至最前 + Arrowheads: 箭頭: + Art 圖稿 + Art using a deleted global swatch keeps its colour. 使用已刪除整體色票的圖稿會保留其顏色。 + Artboard Options 工作區域選項 + Artboard Options: edit with the Artboard tool 工作區域選項:請使用工作區域工具編輯 + Artboard Options… 工作區域選項… + Artboard Tool 工作區域工具 + Artboard {number} 工作區域 {number} + Artboard: 工作區域: + Artboards 工作區域 + ArtCraft Website ArtCraft 網站 + ArtCraft website ArtCraft 網站 + Artwork Colors 圖稿顏色 + As Multiple Assets 作為多個資產 + As Single Asset 作為單一資產 + Ask When Modified 修改時詢問 + Aspect Ratio 長寬比 + Asset Export 資產匯出 + Asset Export Settings 資產匯出設定 + Assets 資產 + Assign 指定 + Assign Profile… 指定描述檔… + Assign: 指定: + Attach to Active Plane 附加至作用中平面 + Attributes 屬性 + Author Title: 作者職稱: + Author: 作者: + Auto 自動 + Automatic 自動 + Auto Size New Area Type 自動調整新區域文字大小 + Auto Spell Check 自動拼字檢查 + Auto-Collapse Iconic Panels 自動收合圖示面板 + Auto-Rotate (turn the paper to each artboard) 自動旋轉 (將紙張轉向各工作區域) + Automatic flatness 自動平滑度 + Automatically 自動 + Automatically Save Recovery Data 自動儲存復原資料 + Automation 自動化 + Average 平均 + Average… 平均… + Axis: 軸: + Background Color: 背景顏色: + Background Contents 背景內容 + Background Contents: 背景內容: + Background: 背景: + Backspace Backspace + Bar Graph Tool 橫條圖工具 + Baseline Shift 基線位移 + Basic Latin 基本拉丁文 + Batch 批次 + Begin Recording 開始錄製 + Bend: 彎曲: + Bevel Join 斜角 + Bitmap 點陣圖 + Black 黑色 + Black and White 黑白 + Black and White Logo 黑白標誌 + BLANK DOCUMENT PRESETS 空白文件預設集 + Bleed 出血 + Bleed: 出血: + Bleeds 出血 + Blend 漸變 + Blend Front to Back 由前至後漸變 + Blend Horizontally 水平漸變 + Blend Info 漸變資訊 + Blend Options 漸變選項 + Blend Options… 漸變選項… + Blend Tool 漸變工具 + Blend Vertically 垂直漸變 + Blending Mode 混合模式 + Blends and Transparency 混合與透明度 + Bloat Tool 膨脹工具 + Blob Brush 點滴筆刷 + Blob Brush Tool 點滴筆刷工具 + Blue 藍 + Blur 模糊 + Bottom 下 + Bottom in Front 下方置前 + Bottom: 下: + Bounds 邊界 + Bounds (Fill Proportionally) 邊界 (按比例填滿) + Bounds (Fit Proportionally) 邊界 (按比例符合) + Break Link to Graphic Style 中斷繪圖樣式連結 + Break Link to Symbol 中斷符號連結 + Brick by Column 依欄磚狀 + Brick by Row 依列磚狀 + Brick Offset: 磚塊位移: + Brightness 亮度 + Brightness/Contrast… 亮度/對比… + Brights 亮色 + Bring Forward 前移 + Bring to Front 移至最前 + Bristle 鬃毛 + Bristle Brush Strokes 鬃毛筆刷筆畫 + Browser 瀏覽器 + Brush Affects: 筆刷影響: + Brush Definition 筆刷定義 + Brush Libraries 筆刷資料庫 + Brush Libraries (on the roadmap) 筆刷資料庫 (規劃中) + Brush Options… 筆刷選項… + Brush Strokes 筆刷筆畫 + Brushes 筆刷 + Built-in Libraries 內建資料庫 + Built-in presets are read-only: New… starts an editable copy. 內建預設集為唯讀:「新增…」會建立可編輯的複本。 + Built-in presets don't change: New makes a copy you can edit. 內建預設集無法變更:「新增」會建立可編輯的拷貝。 + Built-in presets don't change: New… starts an editable copy. 內建預設集不會變動:「新增…」會建立可編輯的複本。 + Bulge… 鼓起… + Bullet 項目符號 + Butt Cap 平端點 + Byte Order: 位元組順序: + Calligraphic 書法 + Can't print these settings: see the Summary 無法列印這些設定:請見摘要 + Cancel 取消 + Cancel Key Object 取消關鍵物件 + Canvas Color 畫布顏色 + Cap 端點 + Cap: 端點: + Cascade 重疊顯示 + Center Artwork 圖稿置中 + Center on Artboard 在工作區域置中 + Center-Justified Tab 置中對齊定位點 + Centimeters 公分 + Change 變更 + Change All 全部變更 + Change Case 變更大小寫 + Change Screen Mode (F) 變更螢幕模式 (F) + Change to Global Rulers 變更為全域尺標 + Change to Right-to-Left Layout 變更為由右至左版面配置 + Changed since the last Refresh. 自上次重新整理後已變更。 + Changes allowed: 允許的變更: + Changes made since it was last saved will be lost. 自上次儲存後所做的變更將會遺失。 + Changing these settings changes how raster effects look when exported, flattened or expanded. 變更這些設定會改變點陣效果在匯出、平面化或展開時的外觀。 + Channel 色版 + Character 字元 + Character / Paragraph 字元/段落 + Character Rotation 字元旋轉 + Character Set: 字元集: + Character Style 字元樣式 + Character Style Options 字元樣式選項 + Character Styles 字元樣式 + Check Links 檢查連結 + Check Spelling… 拼字檢查… + Choose a folder 選擇資料夾 + Choose… 選擇… + Circle 圓形 + Classic Defaults 經典預設值 + Clean Up… 清除… + Clear 清除 + Clear All Tabs 清除所有定位點 + Clear Appearance 清除外觀 + Clear Guides 清除參考線 + Clear Overrides 清除覆寫 + Clear Recent Files 清除最近檔案 + Click 按一下 + Click an anchor point 按一下錨點 + Click or drag 按一下或拖曳 + Click the art with the Gradient tool to add or select points. 以漸層工具點擊圖稿,即可新增或選取點。 + Click the first point 按一下第一個點 + Click the ramp or a type button to apply a gradient. 點擊漸層條或類型按鈕以套用漸層。 + Click to add a tab stop, drag to move it, drag it off the ruler to delete it 點擊以新增定位點,拖曳可移動,拖出尺標即可刪除 + Click to choose a swatch, Shift-click to mix a colour 點擊以選擇色票,Shift+點擊可混合顏色 + Click to edit the mask; Alt-click to view only the mask; Shift-click to disable or enable it 點擊以編輯遮色片;Alt+點擊僅檢視遮色片;Shift+點擊可停用或啟用 + Click to place each file at 100%, or drag to size it; arrow keys switch files, Esc skips one. 按一下以 100% 置入每個檔案,或拖曳以調整大小;方向鍵可切換檔案,Esc 可略過一個。 + Click to select, double-click to edit 按一下以選取,按兩下以編輯 + Click to toggle visibility 點擊以切換可見性 + Click to zoom in, Alt-click to zoom out, drag to pan, double-click to fit 按一下放大顯示,Alt 加按一下縮小顯示,拖曳以平移,按兩下以符合視窗 + Client-side (.html) 用戶端 (.html) + Clip 剪裁 + Clip Complex Regions 剪裁複雜區域 + Clip to Art 剪裁至圖稿 + Clip to Artboard 剪裁至工作區域 + Clip to Bounding Box 剪裁至邊框 + Clipboard as PDF 剪貼簿存為 PDF + Clipboard as PNG 剪貼簿存為 PNG + Clipboard as SVG 剪貼簿存為 SVG + Clipboard as Text 剪貼簿存為文字 + Clipboard Formats 剪貼簿格式 + Clipboard Handling 剪貼簿處理 + Clipping Mask 剪裁遮色片 + Clipping Masks 剪裁遮色片 + Close 關閉 + Close All 全部關閉 + Close Library 關閉資料庫 + Close Path 封閉路徑 + Cmd Cmd + CMYK CMYK + CMYK Color CMYK 色彩 + CMYK colours print with their own values; off, they go through the colour settings too. CMYK 顏色以其本身的數值列印;關閉時,也會經過色彩設定處理。 + Collate 逐份列印 + Collect for Export 收集以供匯出 + Collect in New Layer 收集至新圖層 + Collect Links in a Separate Folder 將連結收集到獨立的資料夾 + Color 顏色 + Color (,) 顏色 (,) + Color Adjustments 色彩調整 + Color bars 色彩條 + Color blindness – Deuteranopia-type 色盲 – 綠色盲型 + Color blindness – Protanopia-type 色盲 – 紅色盲型 + Color Burn 加深顏色 + Color conversion: 色彩轉換: + Color Dodge 加亮顏色 + Color Group 顏色群組 + Color Group: 顏色群組: + Color Group: {name} 顏色群組:{name} + Color Guide 色彩參考 + Color Guide Options 色彩參考選項 + Color Guide Options… 色彩參考選項… + Color images 彩色影像 + Color Library 顏色資料庫 + Color Library… 色彩資料庫… + Color Management 色彩管理 + Color Mixer (Shift-click the chip) 混色器 (Shift-按一下色塊) + Color Mode 色彩模式 + Color Mode: 色彩模式: + Color Model: 色彩模型: + Color Models 色彩模型 + Color Picker 顏色選擇器 + Color Picker… 顏色選擇器… + Color Reduction: 色彩縮減: + Color Settings 色彩設定 + Color Settings… 色彩設定… + Color Space 色彩空間 + Color Swatches 色票 + Color Table 色彩表 + Color Table ({n} color) 顏色表 ({n} 色) + Color Table ({n} colors) 顏色表 ({n} 色) + Color Theme 色彩主題 + Color Themes 色彩主題 + Color Type: 顏色類型: + Color: 顏色: + Colorize 上色 + Colors 顏色 + Colors: 顏色: + Column Graph Tool 長條圖工具 + Columns: 欄數: + Column… 欄… + Combine Slices 合併切片 + Coming soon — tracked in the parity plan 即將推出 — 已列入功能對等計畫 + Command Click to Select Objects Behind Cmd 加按一下可選取後方物件 + Command Dialog 指令對話框 + Community 社群 + Compatibility: 相容性: + Compatible Gradient and Gradient Mesh Printing 相容的漸層與漸層網格列印 + Complement 互補色 + Complexity: 複雜度: + Composite colours are converted to it; separations separate with it when it is a CMYK profile. 複合顏色會轉換為此描述檔;其為 CMYK 描述檔時,分色也會使用它。 + Compound Path 複合路徑 + Compress (RLE) 壓縮 (RLE) + Compress text and line art 壓縮文字與線條圖 + Compression 壓縮 + Compression › Color › Above Ppi 壓縮 › 顏色 › 高於 ppi + Compression: 壓縮: + Condition identifier: 條件識別碼: + Conflict {n} of {count} 衝突 {n}/{count} + Consolidate All Windows 合併所有視窗 + Constrain Angle 強制角度 + Constrain Width and Height Proportions 強制寬度與高度比例 + Construction Guides 建構參考線 + Contextual Alternates 前後關聯替代字 + Contextual Task Bar 快捷工作列 + Contrast 對比 + Control 控制 + Convert 轉換 + Convert All Spot Colors to Process 將所有特別色轉換為印刷色 + Convert All Strokes to Outlines 將所有筆畫轉換為外框 + Convert All Text to Outlines 將所有文字轉換為外框 + Convert Anchor Point 轉換錨點 + Convert Anchor Points 轉換錨點 + Convert Color 轉換顏色 + Convert Document Color Mode 轉換文件色彩模式 + Convert Process to Global 將印刷色轉換為整體色 + Convert text to outlines 將文字轉換為外框 + Convert To Area Type 轉換為區域文字 + Convert to Artboards 轉換為工作區域 + Convert to CMYK 轉換為 CMYK + Convert to Grayscale 轉換為灰階 + Convert To Outlines 轉換為外框 + Convert To Point Type 轉換為點狀文字 + Convert to RGB 轉換為 RGB + Convert to Shape 轉換為形狀 + Convert to sRGB 轉換為 sRGB + Copied the CSS of {rules} object(s) 已複製 {rules} 個物件的 CSS + Copies: 份數: + Copy 複製 + Copy (make a transformed copy) 複製 (製作變形後的拷貝) + Copy All Objects 複製所有物件 + Copy All Styles 複製所有樣式 + Copy Color Value (Hex) 複製顏色值 (Hex) + Copy Fonts Used in the Document 複製文件中使用的字體 + Copy Links 複製連結 + Copy Selected Style 複製所選樣式 + Copy Width Point 複製寬度點 + Copyright 版權 + Copyright Info URL: 著作權資訊網址: + Copyright Notice: 著作權聲明: + Copyright Status: 著作權狀態: + Copyright Symbol 著作權符號 + Corner Radius 圓角半徑 + Corner Radius: 圓角半徑: + Corner: 轉角: + Corners 轉角 + Couldn't copy to the system clipboard: {e} 無法複製到系統剪貼簿:{e} + Couldn't paste from the clipboard: {e} 無法從剪貼簿貼上:{e} + Couldn't save recovery data: {e} 無法儲存復原資料:{e} + Couldn't {} {name}: {e} 無法{} {name}:{e} + Create 建立 + Create Clipping Mask 建立剪裁遮色片 + Create Document 建立文件 + Create from Guides 從參考線建立 + Create from Selection 從選取範圍建立 + Create From: 建立來源: + Create Gradient Mesh… 建立漸層網格… + Create Graph 建立圖表 + Create New Layer 建立新圖層 + Create New Sublayer 建立新子圖層 + Create New Swatch… 建立新色票… + Create New {kind} Style 建立新的{kind}樣式 + Create Object Mosaic… 建立物件馬賽克… + Create Outlines 建立外框 + Create Path 建立路徑 + Create PDF layers from top-level layers 從最上層圖層建立 PDF 圖層 + Create PDF-Compatible File 建立與 PDF 相容的檔案 + Create Report 建立報告 + Create Slice 建立切片 + Create Sub-folders 建立子資料夾 + Create Text 建立文字 + Create Trim Marks 建立剪裁標記 + Created 建立時間 + Created: 建立時間: + Crop 裁切 + Crop Image 裁切影像 + Crop Marks 裁切標記 + Crop To 裁切至 + Crystallize Tool 結晶化工具 + CSS Properties CSS 屬性 + Ctrl Ctrl + Currency & Symbols 貨幣與符號 + Current Brush 目前的筆刷 + Current Colors ({total}) 目前顏色 ({total}) + Current Symbol 目前的符號 + Curvature 曲率 + Curvature Tool 曲線工具 + Curves… 曲線… + Custom 自訂 + Customize… 自訂… + Cut 剪下 + Cut Path at Selected Anchor Points 在所選錨點處切斷路徑 + Cyan 青色 + Dark 深色 + Darken 變暗 + dash 虛線 + Dash Pattern 虛線樣式 + Dashed 虛線 + Dashed Line 虛線 + Data Recovery 資料復原 + Data… 資料… + Dates 日期 + Decimal-Justified Tab 小數點對齊定位點 + Decimal: 小數: + Default 預設 + Default Actions 預設動作 + Default Figure 預設數字 + Default Fill and Stroke 預設填色與筆畫 + Default Fill and Stroke (D) 預設填色與筆畫 (D) + Default Language 預設語言 + Default Printer 預設印表機 + Default Swatches 預設色票 + Define Grid… 定義格點… + Define Perspective Grid 定義透視格點 + Define Perspective Grid… 定義透視格點… + Delete 刪除 + Delete All 全部刪除 + Delete Anchor Point 刪除錨點 + Delete Anchor Point Tool 刪除錨點工具 + Delete Artboard 刪除工作區域 + Delete Artboards 刪除工作區域 + Delete Brush 刪除筆刷 + Delete Character Style 刪除字元樣式 + Delete Empty Artboards 刪除空白工作區域 + Delete Freeform Point 刪除任意形狀錨點 + Delete Graphic Style 刪除繪圖樣式 + Delete Graphic Style (Alt-click: without asking) 刪除繪圖樣式 (Alt+點擊:不詢問) + Delete Layer 刪除圖層 + Delete Mesh Point 刪除網格點 + Delete Paragraph Style 刪除段落樣式 + Delete Pattern 刪除圖樣 + Delete PDF Preset 刪除 PDF 預設集 + Delete Perspective Grid Preset 刪除透視格點預設集 + Delete Point 刪除點 + Delete Preset 刪除預設集 + Delete Print Preset 刪除列印預設集 + Delete Profile 刪除描述檔 + Delete Save for Web Preset 刪除儲存為網頁用預設集 + Delete Selected Item 刪除所選項目 + Delete Selection 刪除選取範圍 + Delete Slices 刪除切片 + Delete Spine Point 刪除脊線點 + Delete Stop 刪除色標 + Delete Swatch 刪除色票 + Delete Symbol 刪除符號 + Delete Tab 刪除定位點 + Delete the graphic style “{name}”? 要刪除繪圖樣式「{name}」嗎? + Delete Theme 刪除主題 + Delete theme 刪除主題 + Delete these {count} swatches and groups? 要刪除這 {count} 個色票和群組嗎? + Delete these {n} graphic styles? 要刪除這 {n} 個繪圖樣式嗎? + Delete this saved preset 刪除此已儲存的預設集 + Delete this {n} graphic style? 要刪除這 {n} 個繪圖樣式嗎? + Delete Transparency Flattener Preset 刪除透明度平面化預設集 + Delete Width Point 刪除寬度點 + Delete Workspace 刪除工作區 + Delete {kind} Style 刪除{kind}樣式 + Delete “{name}”? 要刪除「{name}」嗎? + Depth: 深度: + Description 描述 + Description: 說明: + Deselect 取消選取 + Design… 設計… + Destination: 目的地: + Detail: 細節: + Devices 裝置 + Dialog 對話方塊 + Diamond 菱形 + Difference 差異化 + Differences from {preset}: 與{preset}的差異: + Dim Copies to: 將拷貝調暗至: + Dimensions 尺寸 + Direct Selection Tool 直接選取工具 + Direction Handles 控制把手 + Disable Auto Add/Delete 停用自動增加/刪除 + Disable Opacity Mask 停用不透明度遮色片 + Discard 捨棄 + Discard Empty Type 捨棄空白文字 + Discard Recovered Documents 捨棄已復原的文件 + Discard White Overprint in Output 輸出時捨棄白色疊印 + Discarded {} recovered document(s) 已捨棄 {} 份復原的文件 + Discord Discord + Discretionary Hyphen 選擇性連字號 + Discretionary Ligatures 任意連字 + Display All Blacks Accurately 精確顯示所有黑色 + Display All Blacks as Rich Black 將所有黑色顯示為濃黑 + Display Bitmaps as Anti-aliased Images in Pixel Preview 在像素預視中將點陣圖顯示為消除鋸齒的影像 + Display Options 顯示選項 + Display Print Size at 100% Zoom 以 100% 縮放顯示列印尺寸 + Distort & Transform 扭曲與變形 + Distort Appearance 扭曲外觀 + Distort Linear Gradients 扭曲線性漸層 + Distort Pattern Fills 扭曲圖樣填色 + Distortion 扭曲 + Distribute 均分 + Distribute Objects: 均分物件: + Distribute Spacing 均分間距 + Distribute Spacing: 均分間距: + Dither: 仿色: + Divide 分割 + Divide Horizontally Into 水平分割為 + Divide Objects Below 分割下方物件 + Divide Slices 分割切片 + Divide Slices… 分割切片… + Divide Vertically Into 垂直分割為 + Do Not Move 不移動 + Do you want to save the changes you made to “{name}”? 要儲存對「{name}」所做的變更嗎? + Document 文件 + Document Color Mode 文件色彩模式 + Document Info 文件資訊 + Document Ink Options 文件油墨選項 + Document JSON 文件 JSON + Document open password 文件開啟密碼 + Document profile 文件描述檔 + Document Raster Effects Settings 文件點陣效果設定 + Document Raster Effects Settings Dialog 文件點陣效果設定對話框 + Document Raster Effects Settings… 文件點陣效果設定… + Document Setup 文件設定 + Document Setup… 文件設定… + Document Swatches 文件色票 + Document Title: 文件標題: + Documents you create appear here. 您建立的文件會顯示在這裡。 + Don't Save 不儲存 + Don't Show Center 不顯示中心點 + Done 完成 + Dots 點 + Double Click To Isolate 按兩下以隔離 + Double Left Quotation Marks 左雙引號 + Double Quotes: 雙引號: + Double Right Quotation Marks 右雙引號 + Double-click 按兩下 + Double-click to edit the characters' paint 按兩下以編輯字元的上色 + Double-click to insert 按兩下以插入 + Double-click to rename 按兩下以重新命名 + Double-click to target the contents 按兩下以指定內容 + Download 下載 + Download: 下載: + Downloaded {what} 已下載 {what} + Downloaded {} file(s) 已下載 {} 個檔案 + Drag 拖曳 + Drag a handle 拖曳控制把手 + Drag art here, or select it and click +. Hold Alt for one asset of several objects. 將圖稿拖曳到這裡,或選取後按一下 +。按住 Alt 可將多個物件合為一個資產。 + Drag onto art to apply this appearance 拖曳到圖稿上以套用此外觀 + Drag the art to move it on the paper 拖曳圖稿以在紙張上移動 + Draw 繪製 + Draw Behind 背後繪圖 + Draw in Perspective 在透視中繪製 + Draw Inside 內部繪圖 + Draw Normal 正常繪圖 + Draw: 繪製: + Drawing Mode 繪圖模式 + Drop Shadow 製作陰影 + Drop Shadow… 陰影… + Duotone Gradients 雙色調漸層 + Duplicate 製作複本 + Duplicate and Offset 10 pt 製作複本並位移 10 pt + Duplicate Artboard 製作工作區域複本 + Duplicate Artboards 製作工作區域複本 + Duplicate Brush 製作筆刷複本 + Duplicate Character Style 製作字元樣式複本 + Duplicate Effect 製作效果複本 + Duplicate Graphic Style 製作繪圖樣式複本 + Duplicate Item 製作項目複本 + Duplicate Layer 製作圖層複本 + Duplicate Paragraph Style 製作段落樣式複本 + Duplicate Selected Item 製作所選項目的複本 + Duplicate Slice 製作切片複本 + Duplicate Swatch 製作色票複本 + Duplicate Symbol 製作符號複本 + Duplicate {kind} Style 製作{kind}樣式的複本 + Dutch 荷蘭文 + DXF Import Options DXF 匯入選項 + DXF Info DXF 資訊 + DXF Options DXF 選項 + DXF Options Dialog DXF 選項對話框 + e.g. 1-3, 5 例如 1-3, 5 + Each page prints as one image, at the document's raster effects resolution. 每頁會以文件的點陣效果解析度列印為單一影像。 + Earth Tones 大地色系 + East Asian Type 東亞文字 + Edit 編輯 + Edit Artboards 編輯工作區域 + Edit Clipping Path 編輯剪裁路徑 + Edit Color Group 編輯顏色群組 + Edit Colors 編輯顏色 + Edit Contents 編輯內容 + Edit Custom Dictionary… 編輯自訂字典… + Edit Envelope 編輯封套 + Edit Freeform Point 編輯任意形狀錨點 + Edit Gradient 編輯漸層 + Edit Opacity Mask 編輯不透明度遮色片 + Edit or Apply Color Group 編輯或套用顏色群組 + Edit or Apply Color Group… 編輯或套用顏色群組… + Edit or Apply Colors 編輯或套用顏色 + Edit Original 編輯原稿 + Edit Pattern 編輯圖樣 + Edit PDF Preset 編輯 PDF 預設集 + Edit Print Preset 編輯列印預設集 + Edit Selection… 編輯選取範圍… + Edit Symbol 編輯符號 + Edit Text 編輯文字 + Edit the applied effect, or add another one? 要編輯已套用的效果,還是新增另一個? + Edit the selected point's colour in the Color panel 在顏色面板中編輯所選點的顏色 + Edit the stop 編輯色標 + Edit Theme 編輯主題 + Edit theme 編輯主題 + Edit Toolbar 編輯工具列 + Edit Views 編輯檢視 + Edit Views… 編輯檢視… + Edit… 編輯… + Effect 效果 + Effect Options 效果選項 + Effective PPI 有效 PPI + Effects 效果 + Effect… 效果… + Ellipse 橢圓形 + Ellipse Tool 橢圓工具 + Ellipse… 橢圓形… + Ellipsis 刪節號 + Em Dash 長破折號 + Em Space 全形空格 + Embed 嵌入 + Embed Fonts (for other applications) 內嵌字體 (供其他應用程式使用) + Embed Fonts (Glyphs Used) 內嵌字體 (使用的字符) + Embed ICC Profile 內嵌 ICC 描述檔 + Embed ICC Profiles 內嵌 ICC 描述檔 + Embed Image 內嵌影像 + Embed Image(s) 內嵌影像 + Embed page thumbnails 嵌入頁面縮圖 + Embedded 已內嵌 + Embedded Image 嵌入的影像 + Embedded {n} image(s) 已嵌入 {n} 張影像 + Embedded {n} image(s); {missing} missing: relink them first 已嵌入 {n} 張影像;{missing} 張遺失:請先重新連結 + Emulsion: 藥膜: + En Dash 短破折號 + En Space 半形空格 + Enable Background Export 啟用背景匯出 + Enable Background Save 啟用背景儲存 + Enable Contextual Task Bar 啟用快捷工作列 + Enable copying of text, images and other content 啟用複製文字、影像與其他內容 + Enable in-menu font previews 啟用選單內字體預覽 + Enable Missing Glyph Protection 啟用遺失字符保護 + Enable Opacity Mask 啟用不透明度遮色片 + Enable plaintext metadata 啟用純文字中繼資料 + Enable Rubber Band for 啟用橡皮筋預覽,適用於 + Enable text access for screen readers 啟用螢幕閱讀器的文字存取 + Enable Touch Gestures 啟用觸控手勢 + Enable Touch Workspace 啟用觸控工作區 + Encoding 編碼 + Encoding: 編碼: + Encryption: {name} (set by Compatibility) 加密:{name} (由相容性設定) + End arrowhead 結尾箭頭 + English English + English: UK 英文:英國 + English: USA 英文:美國 + Enter Enter + Enter Isolation Mode 進入隔離模式 + Entire Font 整個字體 + Envelope Distort 封套扭曲 + Envelope Info 封套資訊 + Envelope Mesh 封套網格 + Envelope Options 封套選項 + Envelope Options… 封套選項… + EPS Options EPS 選項 + EPS Options Dialog EPS 選項對話框 + Eraser 橡皮擦 + Eraser Tool 橡皮擦工具 + Error 錯誤 + Esc Esc + Essentials 基本功能 + Essentials Classic 基本功能 經典 + Every (minutes) 每隔 (分鐘) + Every option is at its default. 所有選項皆為預設值。 + Every option matches {base}. 所有選項皆與{base}相同。 + Every-line Composer 逐行排版程式 + Exact dash lengths 精確虛線長度 + Exceptions (comma separated) 例外 (以逗號分隔) + Exclude 差集 + Exclusion 排除 + Exit Isolation Mode 結束隔離模式 + Expand 展開 + Expand Appearance 展開外觀 + Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap 展開適用於複合形狀 (按住 Alt 按一下形狀模式),已列入開發計畫 + Expand Brush Strokes 展開筆刷筆畫 + Expand Compound Shape 展開複合形狀 + Expand Gradient Mesh 展開漸層網格 + Expand Gradient To 展開漸層為 + Expand Options 展開選項 + Expand Repeat 展開重複 + Expand Shape 展開形狀 + Expand: 展開: + Expand… 展開… + Export 匯出 + Export All… 全部匯出… + Export and Clipboard Transparency Flattener Settings 匯出與剪貼簿透明度平面化設定 + Export Artboard 匯出工作區域 + Export As 匯出為 + Export As PNG… 匯出為 PNG… + Export As SVG… 匯出為 SVG… + Export Asset 匯出資產 + Export Assets 匯出資產 + Export As… 匯出為… + Export CSS 匯出 CSS + Export CSS… 匯出 CSS… + Export Document 匯出文件 + Export DXF 匯出 DXF + Export EPS 匯出 EPS + Export for Screens 匯出為螢幕適用 + Export for Screens Settings 匯出為螢幕適用設定 + Export for Screens… 匯出為螢幕適用… + Export Keyboard Shortcuts… 匯出鍵盤快速鍵… + Export Option 匯出選項 + Export PDF 匯出 PDF + Export PDF Presets 匯出 PDF 預設集 + Export Perspective Grid Presets 匯出透視格點預設集 + Export Print Presets 匯出列印預設集 + Export Selected Art Only 僅匯出選取的圖稿 + Export Selected CSS… 匯出選取的 CSS… + Export Selection… 匯出選取範圍… + Export Settings 匯出設定 + Export the selected assets (else all of them) 匯出選取的資產 (未選取時匯出全部) + Export to: 匯出至: + Export Transparency Flattener Presets 匯出透明度平面化預設集 + Export {count} Selected 匯出 {count} 個選取項目 + Export: 匯出: + Exported {count} file(s) to {folder} 已匯出 {count} 個檔案至 {folder} + Exported {path} 已匯出 {path} + Exported {what} 已匯出 {what} + Exported {what} with {} note(s): {first} 已匯出 {what},附 {} 則附註:{first} + Exporting {} 正在匯出 {} + Export… 匯出… + Extrude & Bevel… 突出與斜角… + Eyedropper 滴管 + Eyedropper Applies: 滴管套用: + Eyedropper Options 滴管選項 + Eyedropper Picks Up: 滴管擷取: + Eyedropper Tool 滴管工具 + Eyedropper: click the art to sample the stop's colour 滴管:點擊圖稿以取樣色標的顏色 + Feather… 羽化… + Feet 英呎 + Feet & Inches 英呎與英吋 + Fewer Elements 減少 元素 + Fidelity: 精確度: + Figure Space 數字空格 + Figure: 數字: + File 檔案 + File Dimensions 檔案尺寸 + File Format: 檔案格式: + File Formats 檔案格式 + File Handling 檔案處理 + File Info 檔案資訊 + File Info Dialog 檔案資訊對話框 + File Info… 檔案資訊… + File Name: 檔案名稱: + File › Place… brings in an image, linked or embedded. 檔案 › 置入… 可置入影像,可選擇連結或內嵌。 + Files 檔案 + Files of a PDF standard can't be password-protected. PDF 標準的檔案無法設定密碼保護。 + Files of this standard tag their colours with ICC profiles. 此標準的檔案會以 ICC 描述檔標記顏色。 + Fill 填色 + Fill & Stroke 填色與筆畫 + Fill (X), double-click for the Color Picker 填色 (X),按兩下開啟檢色器 + Fill and Stroke 填色與筆畫 + Fill Color 填色顏色 + Fill New Type Objects With Placeholder Text 以預留位置文字填滿新文字物件 + Fill Rule 填色規則 + Fill with Placeholder Text 以預留位置文字填滿 + Fill: 填色: + Fill: click for swatches, Shift-click for the color mixer 填色:按一下開啟色票,按住 Shift 按一下開啟混色器 + Find 尋找 + Find and Replace… 尋找與取代… + Find Font 尋找字體 + Find Font… 尋找字體… + Find Next 尋找下一個 + First Page 第一頁 + First-line Left Indent 首行左縮排 + Fisheye… 魚眼… + Fish… 魚形… + Fit All 符合全部 + Fit All in Window 全部符合視窗 + Fit Artboard in Window 使工作區域符合視窗 + Fit dashes to corners and ends 讓虛線貼合轉角與端點 + Fit Headline 符合標題 + Fit in View 符合檢視 + Fit in Window 符合視窗 + Fit on Screen 顯示全頁 + Fit to Artwork Bounds 符合圖稿邊界 + Fit to Selected Art 符合選取的圖稿 + Flag… 旗形… + Flare 光暈 + Flare Tool 光暈工具 + Flare Tool Options 光暈工具選項 + Flat Image 平面影像 + Flatness: 平滑度: + Flatten Transparency 透明度平面化 + Flatten Transparency… 透明度平面化… + Flattener preset: 平面化預設集: + Flattener presets 平面化預設集 + Flattener Preview 平面化預視 + Flip Across 跨軸翻轉 + Flip Along 沿軸翻轉 + Flip Along Horizontal Axis 沿水平軸翻轉 + Flip Along Vertical Axis 沿垂直軸翻轉 + Flip Horizontal 水平翻轉 + Flip Row Order 反轉列順序 + Flip Vertical 垂直翻轉 + Float in Window 在視窗中浮動 + Focal Fill 焦點填色 + Focal Stroke 焦點筆畫 + Folder 資料夾 + Folder Name: 資料夾名稱: + Font 字體 + Font Family 字體家族 + Font Family & Style 字體家族與樣式 + Font Family, Style & Size 字體家族、樣式與大小 + Font List 字體清單 + Font Preview Size 字體預覽大小 + Font Size 字體大小 + Font: 字體: + Fonts 字體 + Fonts in Document 文件中的字體 + Fonts in Document: {count} 文件中的字體:{count} + Fonts whose licence doesn't allow embedding are not copied. 授權不允許嵌入的字體不會複製。 + Forced Line Break 強制換行 + Format 格式 + Format Options 格式選項 + Format Settings 格式設定 + Format: 格式: + Formats: 格式: + Fractional Widths 小數字寬 + Fractions 分數 + Free 自由 + Free Distort 隨意扭曲 + Free Distort… 隨意扭曲… + Free Transform Tool 任意變形工具 + Freeform Gradient 任意形狀漸層 + French 法文 + Frequencies and angles are the output device's to apply. 頻率與角度由輸出裝置負責套用。 + Frequency 頻率 + Full Document 完整文件 + Full Screen Mode 全螢幕模式 + Full Screen Mode with Menu Bar 含選單列的全螢幕模式 + Gamma Gamma + Gamut Check 色域檢查 + Gamut Warning 色域警告 + gap 間隙 + Gap Options… 間隙選項… + Gaussian Blur… 高斯模糊… + General 一般 + General › Placement › X 一般 › 位置 › X + General › Thumbnails 一般 › 縮圖 + Generate CSS 產生 CSS + Generate CSS: the whole document's CSS while nothing is selected 產生 CSS:未選取任何物件時,產生整份文件的 CSS + German 德文 + Get Preferences 取得偏好設定 + Get Text Range 取得文字範圍 + Global 全域 + Global Brush Dimensions 整體筆刷尺寸 + Global Process Color 整體印刷色 + Global Process Color, CMYK 整體印刷色,CMYK + glyph {id} 字符 {id} + Glyphs 字符 + Go To Link 前往連結 + GPL palettes keep solid colours only, as RGB. GPL 調色盤僅保留純色,並以 RGB 儲存。 + GPU Performance GPU 效能 + Gradient 漸層 + Gradient (.) 漸層 (.) + Gradient across stroke 橫跨筆畫的漸層 + Gradient along stroke 沿筆畫的漸層 + Gradient and Mesh Resolution: 漸層與網格解析度: + Gradient Fill: click to apply, drag onto art 漸層填色:點擊以套用,或拖曳到圖稿上 + Gradient Finishes 漸層質感 + Gradient Mesh 漸層網格 + Gradient swatches 漸層色票 + Gradient Tool 漸層工具 + Gradient Vector 漸層向量 + Gradient within stroke 筆畫內的漸層 + Gradients 漸層 + Graph 圖表 + Graph Data 圖表資料 + Graph Type 圖表類型 + Graphic Style 繪圖樣式 + Graphic Style Libraries 繪圖樣式資料庫 + Graphic Style Libraries Menu 繪圖樣式資料庫選單 + Graphic Style Library 繪圖樣式資料庫 + Graphic Style Options 繪圖樣式選項 + Graphic Style Options… 繪圖樣式選項… + Graphic Styles 繪圖樣式 + Graphic Styles Options 繪圖樣式選項 + Graphics 圖形 + Gravity 重力 + Grays 灰色 + Grays and Neutrals 灰色與中性色 + Grayscale 灰階 + Grayscale images 灰階影像 + Greek & Cyrillic 希臘文與西里爾文 + Green 綠 + Grid 格點 + Grid Color & Opacity 格點顏色與不透明度 + Grid Colors: 格點顏色: + Grid Size: 格點大小: + Gridline every 格線間隔 + Gridline every: 格線間隔: + Grids In Back 格點置於後方 + Group 群組 + Group Selection Tool 群組選取工具 + Guides 參考線 + Guides & Grid 參考線與格點 + H Spacing: 水平間距: + H: 高: + Hair Space 極細空格 + Hand Tool 手形工具 + Hand-Drawn 手繪 + Handles 控制把手 + Hard Light 實光 + Harmony Rules: 色彩調和規則: + Harmony Sets 色彩調和組 + Height 高度 + Height: 高度: + Help 說明 + Help › Search Commands… 說明 › 搜尋指令… + Hex by Column 六角形 (依欄) + Hex by Row 六角形 (依列) + Hide 隱藏 + Hide Artboards 隱藏工作區域 + Hide Bounding Box 隱藏邊框 + Hide Code 隱藏程式碼 + Hide Contextual Task Bar 隱藏快捷工作列 + Hide Corner Widget 隱藏轉角 Widget + Hide Corner Widget for angles greater than 角度大於以下數值時隱藏轉角 Widget + Hide Edges 隱藏邊緣 + Hide Gradient Annotator 隱藏漸層註解者 + Hide Grid 隱藏格點 + Hide Guides 隱藏參考線 + Hide Hidden Characters 隱藏隱藏字元 + Hide Note 隱藏註解 + Hide Options 隱藏選項 + Hide Print Tiling 隱藏列印分頁 + Hide Rulers 隱藏尺標 + Hide Slices 隱藏切片 + Hide Text Threads 隱藏文字串連 + Hide Thumbnail 隱藏縮圖 + Hide Thumbnails 隱藏縮圖 + Hide Transparency Grid 隱藏透明度格點 + High 高 + High (300 ppi) 高 (300 ppi) + Highlight Alternate Glyphs 醒目提示替代字符 + Highlight anchors on mouse over 滑鼠移過時醒目提示錨點 + Highlight Substituted Fonts 醒目提示被取代的字體 + Highlight Substituted Glyphs 醒目提示被取代的字符 + History 步驟記錄 + History Options… 步驟記錄選項… + History States 步驟記錄狀態 + Hollow 空心 + Home 首頁 + Horizon Height: 地平線高度: + Horizontal 水平 + Horizontal % 水平 % + Horizontal Align Center 水平置中對齊 + Horizontal Align Left 水平靠左對齊 + Horizontal Align Right 水平靠右對齊 + Horizontal Distribute Center 水平均分中心 + Horizontal Distribute Left 水平均分左緣 + Horizontal Distribute Right 水平均分右緣 + Horizontal Distribute Space 水平均分間距 + Horizontal Grid: 水平格點: + Horizontal Plane 水平面 + Horizontal Scale % 水平縮放 % + Horizontal: 水平: + How colours outside the press's gamut are separated. 印刷機色域以外的顏色如何分色。 + Hue 色相 + Hue/Saturation… 色相/飽和度… + Hyphenate 使用連字號 + Hyphenation 連字號 + Hyphenation… 連字號… + ICC Profile ICC 描述檔 + Identify Objects By 識別物件依據 + Ignore 略過 + Ignore Artboards (all the art as one page) 忽略工作區域 (所有圖稿視為單一頁面) + Ignore White 忽略白色 + Image 影像 + Image Info 影像資訊 + Image Map: 影像地圖: + Image Size 影像尺寸 + Image Trace 影像描圖 + Image Trace Presets 影像描圖預設集 + Image: 影像: + Images: 影像: + Import Keyboard Shortcuts… 匯入鍵盤快速鍵… + Import PDF 匯入 PDF + Import PDF Presets 匯入 PDF 預設集 + Import Perspective Grid Presets 匯入透視格點預設集 + Import Print Presets 匯入列印預設集 + Import Transparency Flattener Presets 匯入透明度平面化預設集 + Imported {what}: {} 已匯入 {what}:{} + Import… 匯入… + In Tangent Handles 內側切線控制把手 + In the selected objects 在選取的物件中 + Inches 英吋 + Include Blacks with CMY: 包含 CMY 的黑色: + Include Bleed 包含出血 + Include Cmy Blacks 包含 CMY 黑色 + Include CMYK PostScript in RGB Files 在 RGB 檔案中包含 CMYK PostScript + Include Document Thumbnails 包含文件縮圖 + Include Hidden Layers 包含隱藏圖層 + Include Linked Files 包含連結檔案 + Include Metadata 包含中繼資料 + Include non-printing layers 包含不列印的圖層 + Include SVG Code 包含 SVG 程式碼 + Include Swatches for Tints 包含色調的色票 + Inflate… 膨脹… + Info 資訊 + Ink 油墨 + Inner Glow… 內光暈… + Input Black 輸入黑色 + Input White 輸入白色 + Insert Break Character 插入分行字元 + Insert Character 插入字元 + Insert Special Character 插入特殊字元 + Insert Whitespace Character 插入空白字元 + Inspect Document 檢查文件 + Inspect Object 檢查物件 + Install Plug-in 安裝增效模組 + Install Plug-in… 安裝增效模組… + Installed plug-in {} 已安裝增效模組 {} + Intensity: 強度: + Intent: 目的: + Interlaced 交錯 + Internal CSS 內部 CSS + Internet Standard RGB (sRGB) 網際網路標準 RGB (sRGB) + Intersect 交集 + Inverse 反轉 + Invert 反轉 + Invert Colors 反轉顏色 + Invert Mask 反轉遮色片 + Isolate 隔離 + Isolate Blending 隔離混合 + Isolate Group 隔離群組 + Italian 義大利文 + Join 連接 + Join Our Discord 加入我們的 Discord + Join our Discord 加入我們的 Discord + Join the ArtCraft community on Discord (discord.gg/artcraft) 加入 Discord 上的 ArtCraft 社群 (discord.gg/artcraft) + Join the lines of each paragraph 合併每個段落的各行 + Join Tool 連接工具 + Justification… 對齊設定… + Justify all lines 兩端對齊所有行 + Justify with last line aligned center 兩端對齊,最後一行置中 + Justify with last line aligned left 兩端對齊,最後一行靠左 + Justify with last line aligned right 兩端對齊,最後一行靠右 + Keep a link to an image file instead of only embedding it 保留影像檔案的連結,而不只是嵌入 + Keep both sides the same width 保持兩側寬度相同 + Keep Formatting 保留格式 + Keep Original 保留原稿 + Keep Plain Text 保留純文字 + Keep the pasted color as a new swatch with a numbered name. 將貼上的顏色保留為新色票,並以編號命名。 + Keep this row's colors (exclude) 保留此列的顏色 (排除) + Keep: 保留: + Kerning (0 = Auto) 字距微調 (0 = 自動) + Keyboard Increment 鍵盤增量 + Keyboard Shortcut Conflicts 鍵盤快速鍵衝突 + Keyboard Shortcut Set 鍵盤快速鍵組 + Keyboard Shortcuts… 鍵盤快速鍵… + Keywords: 關鍵字: + Knife 美工刀 + Knockout Group 去底群組 + Lab Lab + Lab Spot Colors: Lab 特別色: + Lab Values Lab 值 + Landscape 橫向 + Interface Language 介面語言 + Language 語言 + Language Options 語言選項 + Language: 語言: + Large 大 + Large List View 大型清單檢視 + Large Tabs 大型頁籤 + Large Thumbnail View 大縮圖檢視 + Large Thumbnails 大縮圖 + Lasso Tool 套索工具 + Last Effect: {name} 最後一次效果:{name} + Last Effect: {} 上次效果:{} + Last Effect… 最後一次效果… + Last Page 最後一頁 + Latin Extended 拉丁文擴充 + Latin-1 Supplement 拉丁文-1 補充 + Layer 圖層 + Layer Names 圖層名稱 + Layer Options… 圖層選項… + Layers 圖層 + Layout 版面配置 + Layout: 版面配置: + Leader: 前導字元: + Leading 行距 + Left 左 + Left Grid: 左側格點: + Left in Front 左側在前 + Left Indent 左縮排 + Left Plane 左平面 + Left-Justified Tab 靠左定位點 + Left: 左: + Legacy Macintosh RGB (Gamma 1.8) 舊版 Macintosh RGB (Gamma 1.8) + Legacy vector clipboard 舊版向量剪貼簿 + Legacy vector clipboard (no transparency) 舊版向量剪貼簿 (無透明度) + Less 較少 + Letter Letter + Levels… 色階… + Libraries 資料庫 + Ligatures are off while the text is tracked. 文字套用字距時,連字功能會關閉。 + Light 淺色 + Light Blue 淺藍色 + Lighten 變亮 + Lightness 亮度 + Limit Color Guide to Library 將色彩參考限制於資料庫 + Limit colors to swatch library 將顏色限制於色票資料庫 + Limit colors to swatch library: {name} 將顏色限制於色票資料庫:{name} + Limit to Library: 限定於資料庫: + Limit: 限度: + Line 直線 + Line Art and Text Resolution: 線條圖與文字解析度: + Line Breaks 換行 + Line Color 線條顏色 + Line Cut 線條切割 + Line Cut Tool 線條切割工具 + Line Endings: 換行字元: + Line Graph Tool 折線圖工具 + Line Segment 線段 + Line Segment Tool 線段工具 + Line Segment Tool Options 線段工具選項 + Linear Gradient 線性漸層 + Lines 直線 + Lines: clicks add points joined by a line 線條:每次點擊會新增以線連接的點 + Link 連結 + Link harmony colors 連結調和顏色 + Link Info 連結資訊 + Link Opacity Mask 連結不透明度遮色片 + Link start and end arrowhead scales 連結起點與終點箭頭的縮放 + Link the mask 連結遮色片 + Linked 已連結 + Linked File 連結檔案 + Linked File Not Found 找不到連結檔案 + Links 連結 + Liquify 液化 + List Graphic Styles 列出繪圖樣式 + List Keyboard Shortcuts 列出鍵盤快速鍵 + List Patterns 列出圖樣 + List Plug-ins 列出增效模組 + List Preferences 列出偏好設定 + List Slices 列出切片 + List View 清單檢視 + List Workspaces 列出工作區 + Live Paint 即時上色 + Live Paint Bucket 即時上色油漆桶 + Live Paint Bucket (Stroke) 即時上色油漆桶 (筆畫) + Live Paint Info 即時上色資訊 + Live Paint Selection Tool 即時上色選取工具 + Live Shape Properties 即時形狀內容 + Load Color Profile… 載入色彩描述檔… + Load Image into Clipboard 將影像載入剪貼簿 + Load Metafile into Clipboard 將中繼檔載入剪貼簿 + Load PDF into Clipboard 將 PDF 載入剪貼簿 + Load Place Cursor 載入置入游標 + Load Profile… 載入描述檔… + Load SVG into Clipboard 將 SVG 載入剪貼簿 + Load Text into Clipboard 將文字載入剪貼簿 + Local Libraries 本機資料庫 + Locate Object 尋找物件 + Location 位置 + Location: 位置: + Lock 鎖定 + Lock (⌘2) 鎖定 (⌘2) + Lock Grid 鎖定格點 + Lock Guides 鎖定參考線 + Lock Slices 鎖定切片 + Lock Station Point 鎖定基準點 + Lossless (lossy WebP isn't available yet) 無失真 (尚未提供有失真的 WebP) + Lossy: 失真度: + Low 低 + lowercase 全部小寫 + Luminosity 明度 + LZW Compression LZW 壓縮 + Mac Mac + Magenta 洋紅色 + Magic Wand 魔術棒 + Magic Wand Options 魔術棒選項 + Magic Wand Settings 魔術棒設定 + Magic Wand Tool 魔術棒工具 + Maintain Envelope Shape 維持封套形狀 + Make 製作 + Make a theme on Create and save it. 按一下「建立」即可製作主題並儲存。 + Make all bleed settings the same 將所有出血設定設為相同 + Make and Expand 製作並展開 + Make and Expand… 製作並展開… + Make Clipping Mask 製作剪裁遮色片 + Make Compound Shape 製作複合形狀 + Make Envelope 製作封套 + Make Guides 製作參考線 + Make Mask 製作遮色片 + Make Opacity Mask 製作不透明度遮色片 + Make Pixel Perfect 像素完美化 + Make the bleed values differ 讓出血值各自不同 + Make with Mesh… 以網格製作… + Make with Top Object 以最上層物件製作 + Make with Warp… 以彎曲製作… + Make/Release Clipping Mask 製作/釋放剪裁遮色片 + Make… 製作… + Manage Workspaces 管理工作區 + Manage Workspaces… 管理工作區… + Manually 手動 + Mark as trapped 標示為已陷印 + Mark Older Files as [Converted] When Opened 開啟舊檔時標示為 [已轉換] + Marker… 標記… + Marks 標記 + Marks and Bleed 標記與出血 + Marks and Bleeds 標記與出血 + Match User Interface Brightness 符合使用者介面亮度 + Materials… 材質… + Matte 邊緣色 + Matte: 遮色: + Maximize 最大化 + Maximum 最大 + Maximum Editability 最高可編輯性 + Measure Tool 測量工具 + Measurement Labels 測量標籤 + Media 媒體 + Medium 中 + Medium (150 ppi) 中 (150 ppi) + Medium Dark 中等深色 + Medium Light 中等淺色 + Medium Thumbnail View 中縮圖檢視 + Medium Thumbnails 中縮圖 + Menu Dialog 選單對話框 + Merge 合併 + Merge colors into one row 將顏色合併為一列 + Merge Graphic Styles 合併繪圖樣式 + Merge Graphic Styles… 合併繪圖樣式… + Merge Layers 合併圖層 + Merge Swatches 合併色票 + Mesh Tool 網格工具 + Message: 訊息: + Metadata: 中繼資料: + Metallic Gradients 金屬漸層 + Meters 公尺 + Method: 方法: + Millimeters 公釐 + Minify 縮減檔案大小 + Minimal 最少 + Minimize 最小化 + Minus Back 去除後方 + Minus Front 去除前方 + Mirror 鏡射 + Mirror & Cut 鏡射並切割 + Mirror & Cut Tool 鏡射並切割工具 + Missing 遺失 + Missing: {path} 遺失:{path} + Miter Join 尖角 + Miter Limit 尖角限度 + Miter Limit: 尖角限度: + Mixed Appearances 混合外觀 + Mixed Objects 混雜物件 + Mode: 模式: + Model 模型 + Modified 已修改 + Modified since it was read: {path} 讀取後已被修改:{path} + Modified: 修改時間: + Modify 修改 + Monitor RGB 顯示器 RGB + Mono 單色 + Monochrome images 單色影像 + More 更多 + More Pathfinder options 更多路徑管理員選項 + More Settings 更多設定 + Move 移動 + Move All Objects 移動所有物件 + Move Anchors 移動錨點 + Move Appearance 移動外觀 + Move Artboard 移動工作區域 + Move Artboard Up/Down 上移/下移工作區域 + Move Direction Handle 移動控制把手 + Move Down 下移 + Move Effect 移動效果 + Move Envelope Mesh Point 移動封套網格點 + Move Graphic Style 移動繪圖樣式 + Move Guide 移動參考線 + Move in Perspective 在透視中移動 + Move Locked and Hidden Artwork with Artboard 移動工作區域時一併移動鎖定與隱藏的圖稿 + Move Mesh Point 移動網格點 + Move Plane 移動平面 + Move Plane to Match Object 移動平面以符合物件 + Move Slices 移動切片 + Move Spine Point 移動脊線點 + Move Swatch 移動色票 + Move Up 上移 + Move… 移動… + Multiply 色彩增值 + My Themes 我的主題 + N pt per 每 N pt + N slices …, evenly spaced N 個切片…,等距分佈 + Name 名稱 + Name 40% 名稱 40% + Name* @ 66.67 % (RGB/Preview) 名稱* @ 66.67 % (RGB/預覽) + Name* @ 66.67% (RGB/Preview) 名稱* @ 66.67% (RGB/預覽) + Name: 名稱: + Navigate 導覽 + Navigator 導覽器 + Needs the desktop app 需要桌面版應用程式 + Neutral Gradients 中性漸層 + New 新增 + New Art Appearance 新增圖稿外觀 + New Art Brush from Selection 從選取範圍新增藝術筆刷 + New Art Has Basic Appearance 新圖稿具有基本外觀 + New Artboard 新增工作區域 + New Bristle Brush 新增鬃毛筆刷 + New Brush… 新增筆刷… + New Calligraphic Brush 新增書法筆刷 + New Character Style 新增字元樣式 + New Color Group 新增顏色群組 + New Color Group… 新增顏色群組… + New Document Presets 新增文件預設集 + New file 新增檔案 + New from Template 從範本新增 + New from Template… 從範本新增… + New Graphic Style 新增繪圖樣式 + New Graphic Style (Alt-click to name it) 新增繪圖樣式 (Alt+點擊可命名) + New Graphic Style… 新增繪圖樣式… + New Layer… 新增圖層… + New Opacity Masks Are Clipping 新的不透明度遮色片為剪裁 + New Opacity Masks Are Inverted 新的不透明度遮色片為反轉 + New Paragraph Style 新增段落樣式 + New Pattern Brush from Selection 從選取範圍新增圖樣筆刷 + New PDF Preset 新增 PDF 預設集 + New Print Preset 新增列印預設集 + New Scatter Brush from Selection 從選取範圍新增散布筆刷 + New Sublayer… 新增子圖層… + New Swatch 新增色票 + New Swatch… 新增色票… + New Symbol 新增符號 + New Symbol… 新增符號… + New theme from the current color 以目前顏色新增主題 + New View 新增檢視 + New View… 新增檢視… + New Window 新增視窗 + New Workspace 新增工作區 + New Workspace… 新增工作區… + New {kind} Style… 新增{kind}樣式… + New… 新增… + Next 下一個 + Next Library 下一個資料庫 + Next Object Above 上方的下一個物件 + Next Object Below 下方的下一個物件 + Next Page 下一頁 + Next page 下一頁 + No actions. Press ● to record. 沒有動作。按 ● 開始錄製。 + No assets 沒有資產 + No assets yet. Collect art with Object › Collect for Export, or drag it into the Asset Export panel. 尚無資產。請使用「物件 › 收集以供匯出」收集圖稿,或將圖稿拖曳到資產匯出面板。 + No Break 不斷行 + No brushes 沒有筆刷 + No colors to recolor (preserved colors are left out). 沒有可重新上色的顏色 (已保留的顏色不計入)。 + No CSS 無 CSS + No document 無文件 + No Document 無文件 + No document open 沒有開啟的文件 + No document open. 沒有開啟的文件。 + No filter plug-ins installed 未安裝濾鏡增效模組 + No fonts are available. 沒有可用的字體。 + No gradient swatches 沒有漸層色票 + No graphic styles 沒有繪圖樣式 + No images 沒有影像 + No matches. 沒有相符項目。 + No matching fonts 沒有相符的字體 + No options 沒有選項 + No printer settings to open here 此處沒有可開啟的印表機設定 + No saved themes 沒有已儲存的主題 + No saved views (View → New View…) 沒有已儲存的檢視 (檢視 → 新增檢視…) + No Selection 未選取 + No selection 未選取 + No symbols in this document 此文件中沒有符號 + No text selected 未選取文字 + No Thumbnails 無縮圖 + Noise 雜訊 + Nonbreaking Hyphen 不斷行連字號 + Nonbreaking Space 不斷行空格 + None 無 + None (/) 無 (/) + None (keep transparency) 無 (保留透明度) + None. 無。 + Normal 正常 + Normal Screen Mode 正常螢幕模式 + Note: 註解: + Nothing matches the Show filter. 沒有項目符合「顯示」篩選條件。 + Nothing to print 沒有可列印的內容 + Nudge 微移 + Nudge in Perspective 在透視中微移 + Number of Artboards: 工作區域數量: + Number of Colors: 顏色數量: + Number of Recent Files to Display 最近使用檔案的顯示數量 + Number of Recent Fonts 最近使用字體的數量 + Numbers Without Units Are Points 無單位的數字視為 pt + Object 物件 + Object Highlighting 物件醒目提示 + Object IDs: 物件 ID: + Object Name 物件名稱 + Object Properties 物件內容 + Object Selection by Path Only 僅依路徑選取物件 + Objects 物件 + Objects using them keep their look but are no longer linked. 使用它們的物件會保持外觀,但不再連結。 + of characters used 使用的字元 + of {count} 共 {count} + Off 關 + Offset Path 位移複製 + Offset Path… 位移複製… + Offset X X 位移 + Offset: 位移: + OK 確定 + On 開 + On Copy 複製時 + On Paste 貼上時 + On Screen 螢幕上 + One file per artboard: -.{ext} 每個工作區域一個檔案:-.{ext} + One page per artboard. 每個工作區域一頁。 + One Point 一點 + One Point Perspective 一點透視 + One Point Perspective Preset 1 一點透視預設集 1 + One Point Perspective Preset 2 一點透視預設集 2 + One Point Perspective Preset 3 一點透視預設集 3 + One Point Perspective Preset 4 一點透視預設集 4 + One Point Perspective Preset 5 一點透視預設集 5 + Only Web Colors 僅限網頁色彩 + Opacity 不透明度 + Opacity & Mask Define Knockout Shape 不透明度與遮色片定義挖空形狀 + Opacity Mask 不透明度遮色片 + Opacity Mask Info 不透明度遮色片資訊 + Opacity Mask Options 不透明度遮色片選項 + Opacity: 不透明度: + Opacity: 50% Multiply 不透明度:50% 色彩增值 + Opacity: Default 不透明度:預設 + Opacity: {value} 不透明度:{value} + Opaque 不透明 + Open 開啟 + Open a document to edit its patterns. 開啟文件以編輯其圖樣。 + Open a document to navigate it. 開啟文件以進行導覽。 + Open a document to preview its flattening. 開啟文件以預覽其平面化結果。 + Open a document to preview its pages 開啟文件即可預覽其頁面 + Open a document to see its artboards. 開啟文件以檢視其工作區域。 + Open a document to see its graphic styles. 開啟文件即可檢視其繪圖樣式。 + Open a document to see its history. 開啟文件以檢視其步驟記錄。 + Open a document to see its swatches. 開啟文件以檢視其色票。 + Open Document 開啟文件 + Open Documents As Tabs 以分頁開啟文件 + Open Graphic Style Library 開啟繪圖樣式資料庫 + Open Location after Export 匯出後開啟位置 + Open password: 開啟密碼: + Open Paths 開放路徑 + Open Recent File 1 開啟最近使用的檔案 1 + Open Recent File 10 開啟最近使用的檔案 10 + Open Recent File 11 開啟最近使用的檔案 11 + Open Recent File 12 開啟最近使用的檔案 12 + Open Recent File 13 開啟最近使用的檔案 13 + Open Recent File 14 開啟最近使用的檔案 14 + Open Recent File 15 開啟最近使用的檔案 15 + Open Recent File 16 開啟最近使用的檔案 16 + Open Recent File 17 開啟最近使用的檔案 17 + Open Recent File 18 開啟最近使用的檔案 18 + Open Recent File 19 開啟最近使用的檔案 19 + Open Recent File 2 開啟最近使用的檔案 2 + Open Recent File 20 開啟最近使用的檔案 20 + Open Recent File 21 開啟最近使用的檔案 21 + Open Recent File 22 開啟最近使用的檔案 22 + Open Recent File 23 開啟最近使用的檔案 23 + Open Recent File 24 開啟最近使用的檔案 24 + Open Recent File 25 開啟最近使用的檔案 25 + Open Recent File 26 開啟最近使用的檔案 26 + Open Recent File 27 開啟最近使用的檔案 27 + Open Recent File 28 開啟最近使用的檔案 28 + Open Recent File 29 開啟最近使用的檔案 29 + Open Recent File 3 開啟最近使用的檔案 3 + Open Recent File 30 開啟最近使用的檔案 30 + Open Recent File 4 開啟最近使用的檔案 4 + Open Recent File 5 開啟最近使用的檔案 5 + Open Recent File 6 開啟最近使用的檔案 6 + Open Recent File 7 開啟最近使用的檔案 7 + Open Recent File 8 開啟最近使用的檔案 8 + Open Recent File 9 開啟最近使用的檔案 9 + Open Recent Files 開啟最近使用的檔案 + Open Swatch Library 開啟色票資料庫 + Open the URL in the web browser 在網頁瀏覽器中開啟網址 + Opened with {} note(s): {} 已開啟,附 {} 則附註:{} + OpenType OpenType + Open… 開啟… + Optical Margin Alignment 視覺邊界對齊 + Optimize for fast web view 最佳化以便快速網頁檢視 + Optimized 最佳化 + Options 選項 + Options: 選項: + Options… 選項… + or more spaces with a tab 個以上的空格取代為定位字元 + Order: 順序: + Ordinals 序數字 + Orientation 方向 + Orientation: 方向: + Original 原始 + Original: click to restore 原始:按一下以回復 + Other 其他 + Other Layers 其他圖層 + Other Libraries 其他資料庫 + Other Library… 其他資料庫… + Other Script… 其他指令碼… + Out of Gamut Warning 超出色域警告 + Out of Web Color Warning 非網頁安全色警告 + Out Tangent Handles 外切線控制把手 + Outer Glow… 外光暈… + Outline 外框 + Outline Stroke 外框筆畫 + Outline Text 外框文字 + Outlines and Rules 外框與規則線 + Output 輸出 + Output All Blacks Accurately 精確輸出所有黑色 + Output All Blacks as Rich Black 以濃黑輸出所有黑色 + Output Black 輸出黑色 + Output condition: 輸出條件: + Output Intent 輸出目的 + Output intent profile: 輸出目的描述檔: + Output White 輸出白色 + Output › Mode 輸出 › 模式 + Output: 輸出: + Overlap: 重疊: + Overlapping 重疊 + Overlay 覆蓋 + Overprint 疊印 + Overprint and Transparency Flattener 疊印與透明度平面化 + Overprint and Transparency Flattener Options 疊印與透明度平面化選項 + Overprint Black… 疊印黑色… + Overprint Fill 疊印填色 + Overprint Preview 疊印預視 + Overprint Preview (separations) 疊印預視 (分色) + Overprint Stroke 疊印筆畫 + Overprint: 疊印: + Overprints: 疊印: + Override Character Color 覆寫字元顏色 + Package 封裝 + Package collects a saved document with its linked files and fonts. 封裝會將已儲存的文件連同其連結檔案與字體一併收集。 + Package created 已建立封裝 + Packaged {n} file(s) in {folder}{note} 已將 {n} 個檔案封裝於 {folder}{note} + Packaged {n} file(s){note} 已封裝 {n} 個檔案{note} + Package… 封裝… + Page 頁面 + Page information 頁面資訊 + Page Isolated Blending 頁面隔離混合 + Page Knockout Group 頁面挖空群組 + Pages 頁面 + Paintbrush 繪圖筆刷 + Paintbrush Tool 繪圖筆刷工具 + Painting 繪畫 + Panel menu 面板選單 + Panel Options… 面板選項… + Panels 面板 + Paragraph 段落 + Paragraph Style 段落樣式 + Paragraph Style Options 段落樣式選項 + Paragraph Styles 段落樣式 + Paragraph Symbol 段落符號 + Part of ArtCraft. MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + VectorCraft. ArtCraft 的一部分。MIT OR Apache-2.0。字體:Source Sans 3、Inter、JetBrains Mono (OFL)。圖示:Lucide (ISC) + VectorCraft。 + Password 密碼 + Paste 貼上 + Paste in Back 貼至後方 + Paste in Front 貼至前方 + Paste in Place 原地貼上 + Paste Offset 貼上位移 + Paste on All Artboards 貼至所有工作區域 + Paste Remembers Layers 貼上時記住圖層 + Paste without Formatting 貼上但不含格式 + Pasted 已貼上 + Pastels 粉彩 + Path 路徑 + Path Eraser 路徑橡皮擦 + Path Eraser Tool 路徑橡皮擦工具 + Pathfinder 路徑管理員 + Pathfinder Options… 路徑管理員選項… + Pathfinders: 路徑管理員: + Paths 路徑 + Paths: {paths} Anchors: {anchors} Colors: {colors} 路徑:{paths} 錨點:{anchors} 顏色:{colors} + Pattern 圖樣 + Pattern Editing 圖樣編輯 + Pattern Options 圖樣選項 + Pattern options are available in pattern editing mode. 圖樣選項可在圖樣編輯模式中使用。 + PDF PDF + PDF 1.3 files have no transparency: the preset flattens it. PDF 1.3 檔案不支援透明度:預設集會將其平面化。 + PDF Info PDF 資訊 + PDF Preset PDF 預設集 + PDF Preset: PDF 預設集: + PDF Presets PDF 預設集 + PDF Presets… PDF 預設集… + PDF Settings PDF 設定 + PDF/A files carry their own output intent. PDF/A 檔案自帶輸出目的。 + PDF/X-1a files are CMYK: every colour is converted to the destination (blank: the output intent's CMYK profile), untagged. PDF/X-1a 檔案為 CMYK:所有顏色都會轉換至目的地 (留空:輸出目的的 CMYK 描述檔),且不加標記。 + Pen Tool 筆型工具 + Pencil 鉛筆 + Pencil Tool 鉛筆工具 + per slice 每個切片 + Percent: 百分比: + Perceptual Scales 感知尺度 + Performance 效能 + Performance & Storage 效能與儲存空間 + Permissions 權限 + Permissions password: 權限密碼: + Perspective 透視 + Perspective Grid 透視格點 + Perspective Grid Options 透視格點選項 + Perspective Grid Preset 透視格點預設集 + Perspective Grid Presets 透視格點預設集 + Perspective grid presets 透視格點預設集 + Perspective Grid Presets… 透視格點預設集… + Perspective Grid Settings 透視格點設定 + Perspective Grid Tool 透視格點工具 + Perspective Plane Options… 透視平面選項… + Perspective Selection Tool 透視選取工具 + Phone 390×844 手機 390×844 + Picas 派卡 + Pie Graph Tool 圓餅圖工具 + Pixel Preview 像素預視 + Pixels 像素 + Place 置入 + Place DXF 置入 DXF + Place onto a new locked, dimmed template layer below the current layer 置入至目前圖層下方新的鎖定且淡化的範本圖層 + Place PDF 置入 PDF + Place Symbol Instance 置入符號範例 + Place the pages with the placement again 以擺放方式重新放置頁面 + Placed File Info 已置入檔案資訊 + Placement Options 置入選項 + Placement Options… 置入選項… + Placement: 擺放方式: + Place… 置入… + Plates 色版 + Platform: 平台: + Play Current Selection 播放目前選取項目 + Plug-in Info 增效模組資訊 + Plug-ins 增效模組 + Plug-in… 增效模組… + Point Sample 單點取樣 + Point Text Objects 點狀文字物件 + Points 點 + Points: clicks add free points 點:按一下即新增自由點 + Polar Grid 極座標格點 + Polar Grid Tool 極座標格點工具 + Polygon 多邊形 + Polygon Properties: 多邊形內容: + Polygon Tool 多邊形工具 + Portrait 直向 + Portuguese 葡萄牙文 + Position 位置 + Postcard 明信片 + PostScript: PostScript: + PPI PPI + Preferences 偏好設定 + Preferences… 偏好設定… + Prefix: 前綴: + Presentation Attributes 呈現屬性 + Presentation Mode 簡報模式 + Preserve Alpha Transparency 保留 Alpha 透明度 + Preserve Appearance and Overprints 保留外觀與疊印 + Preserve CMYK Numbers 保留 CMYK 數值 + Preserve Editing Capabilities 保留編輯功能 + Preserve editing capabilities 保留編輯功能 + Preserve Lightness 保留亮度 + Preserve Overprints and Spot Colors 保留疊印與特別色 + Preserve Paths 保留路徑 + Preserve Shape Using: 保留形狀的方式: + Preserve Spot Colors 保留特別色 + Preserve Text Appearance 保留文字外觀 + Preserve Text Editability 保留文字可編輯性 + Preserve: 保留: + PRESET DETAILS 預設集詳細資料 + Preset Name 預設集名稱 + Preset name: 預設集名稱: + Preset {} 預設集 {} + Preset: 預設集: + Presets you save with the Save Preset button appear here. 以「儲存預設集」按鈕儲存的預設集會顯示在這裡。 + Presets: 預設集: + Press 按下 + Preview 預覽 + Preview in Browser 在瀏覽器中預覽 + Preview in Browser needs the desktop app (the web downloads files instead) 在瀏覽器中預覽需要桌面版應用程式 (網頁版會改為下載檔案) + Preview Mode 預覽模式 + Preview the document as it is now 以目前狀態預覽文件 + Previous 上一個 + Previous Library 上一個資料庫 + Previous Page 上一頁 + Previous page 上一頁 + Primary 主要 + Print 列印 + Print as Bitmap 以點陣圖列印 + Print Layers: 列印圖層: + Print Preset 列印預設集 + Print Preset: 列印預設集: + Print Presets 列印預設集 + Print presets 列印預設集 + Print Presets… 列印預設集… + Print Preview 列印預覽 + Print Setup 列印設定 + Print Tiling 列印拼貼 + Print Tiling Pages 列印拼貼頁面 + Print Tiling Tool 列印拼貼工具 + Printer 印表機 + Printer mark type: 印表機標記類型: + Printer profile: 印表機描述檔: + Printer Setup… 印表機設定… + Printer: 印表機: + Printers 印表機 + Printing / Exporting 列印/匯出 + Printing allowed: 允許列印: + Printing and Proofing 列印與校樣 + Print… 列印… + Process Color 印刷色 + Profile inclusion: 描述檔內嵌: + Profile: 描述檔: + Progressive 漸進式 + Projecting Cap 方端點 + Proof Colors 校樣色彩 + Proof Setup 校樣設定 + Properties 內容 + Proportional Oldstyle 比例舊式數字 + Pucker & Bloat… 縮攏與膨脹… + Pucker Tool 縮攏工具 + Punctuation 標點符號 + Punctuation Space 標點空格 + Puppet Warp 操控彎曲 + Puppet Warp Pins 操控彎曲圖釘 + Puppet Warp Tool 操控彎曲工具 + Quality {quality} 品質 {quality} + Quality: 品質: + Quarter Space 四分之一空格 + Quick Actions 快速動作 + Quickly start a new file 快速開始新檔案 + Quit VectorCraft 結束 VectorCraft + Radar Graph Tool 雷達圖工具 + Radial 放射狀 + Radial Glows 放射狀光暈 + Radial Gradient 放射狀漸層 + Radius 1 半徑 1 + Radius 2 半徑 2 + Radius1 半徑1 + Radius2 半徑2 + Rainbow 彩虹 + Random 隨機 + Randomize Colors 隨機排列顏色 + Randomly change color order 隨機變更顏色順序 + Randomly change saturation and brightness 隨機變更飽和度與亮度 + Range: 範圍: + Raster Effects 點陣效果 + Raster File Format: 點陣檔案格式: + Raster Sample Size: 點陣取樣大小: + Raster/Vector Balance: 點陣/向量平衡: + Rasterize 點陣化 + Rasterize Unsupported Art 點陣化不支援的圖稿 + Rasterize… 點陣化… + Rasters 點陣圖 + Rating: 評等: + Real World: 真實世界: + Real-time Drawing and Editing 即時繪圖與編輯 + Rearrange All Artboards (on the roadmap) 重新排列所有工作區域 (規劃中) + Rearrange All Artboards… 重新排列所有工作區域… + Recall Selection 叫出選取範圍 + RECENT 最近使用 + Recent 最近使用 + Recent Colors 最近使用的顏色 + Recent File 最近使用的檔案 + Recent Files 最近使用的檔案 + Recent Font 最近使用的字體 + Recent Font 1 最近使用的字體 1 + Recent Font 10 最近使用的字體 10 + Recent Font 2 最近使用的字體 2 + Recent Font 3 最近使用的字體 3 + Recent Font 4 最近使用的字體 4 + Recent Font 5 最近使用的字體 5 + Recent Font 6 最近使用的字體 6 + Recent Font 7 最近使用的字體 7 + Recent Font 8 最近使用的字體 8 + Recent Font 9 最近使用的字體 9 + Recent Fonts 最近使用的字體 + Recent URLs 最近使用的網址 + Recolor 重新上色 + Recolor Artwork 重新上色圖稿 + Recolor Artwork… 重新上色圖稿… + Recolor this row 重新上色此列 + Recolor with Preset 以預設集重新上色 + Recover Documents 找回文件 + Recovery Data 救援資料 + Rectangle 矩形 + Rectangle Cut 矩形切割 + Rectangle Cut Tool 矩形切割工具 + Rectangle Properties: 矩形內容: + Rectangle Tool 矩形工具 + Rectangle: Sunshine 矩形:陽光 + Rectangle… 矩形… + Rectangular Grid 矩形格線 + Rectangular Grid Tool 矩形格線工具 + Red 紅色 + Redefine Character Style 重新定義字元樣式 + Redefine Graphic Style 重新定義繪圖樣式 + Redefine Graphic Style “{name}” 重新定義繪圖樣式「{name}」 + Redefine Paragraph Style 重新定義段落樣式 + Redefine Symbol 重新定義符號 + Redefine {kind} Style 重新定義{kind}樣式 + Redo 重做 + Redo {name} 重做 {name} + Redo {} 重做 {} + Reduce Colors 縮減顏色 + Reduce to Basic Appearance 縮減至基本外觀 + Reduction: 縮減: + Reference Point 參考點 + Reflect 鏡射 + Reflect Horizontal 水平鏡射 + Reflect Tool 鏡射工具 + Reflect X 鏡射 X + Reflect Y 鏡射 Y + Reflect… 鏡射… + Refresh 重新整理 + Refresh Font List 重新整理字體清單 + Registered Trademark Symbol 註冊商標符號 + Registration marks 對位標記 + Registry name: 登錄名稱: + Regular 標準 + Release 釋放 + Release Compound Shape 釋放複合形狀 + Release Guides 釋放參考線 + Release Opacity Mask 釋放不透明度遮色片 + Release Selection 釋放選取範圍 + Release with Perspective 釋放透視 + Relink 重新連結 + Relink Linked Files to the Document 將連結檔案重新連結至文件 + Relink to Folder… 重新連結至資料夾… + Relinked {n} image(s) 已重新連結 {n} 張影像 + Relinked {n} image(s); not in that folder: {} 已重新連結 {n} 張影像;不在該資料夾中:{} + Relinked {} image(s) 已重新連結 {} 張影像 + Relink… 重新連結… + Reload Plug-ins 重新載入增效模組 + Remove 移除 + Remove Anchor Points 移除錨點 + Remove Asset 移除資產 + Remove blank lines between paragraphs 移除段落之間的空白行 + Remove Brush Stroke 移除筆刷筆畫 + Remove Effect 移除效果 + Remove Guide 移除參考線 + Remove Item 移除項目 + Remove Plug-in 移除增效模組 + Remove Selected Assets 移除所選資產 + Remove Threading 移除串連 + Remove “{keyword}” 移除「{keyword}」 + Rename 重新命名 + Rename Asset 重新命名資產 + Rename Character Style 重新命名字元樣式 + Rename Graphic Style 重新命名繪圖樣式 + Rename Paragraph Style 重新命名段落樣式 + Rename Workspace 重新命名工作區 + Render Threads (-1 = Automatic) 算圖執行緒 (-1 = 自動) + Rendering intent: 演算目的: + Reorder 重新排序 + Reorder Appearance Item 重新排序外觀項目 + Repeat 重複 + Repeat Options 重複選項 + Repeat Pathfinder 重複路徑管理員 + Repeat Tab 重複定位點 + Repeat {name} 重複 {name} + Replace 取代 + Replace Font 取代字體 + Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 以其他檔案取代,或忽略:其影像會繼續顯示隨文件儲存的預覽。 + Replace Spine 取代軸線 + Replace Symbol 取代符號 + Replace With Font 取代為字體 + Replaces the saved preset of that name. 取代同名的已儲存預設集。 + Replace… 取代… + Require a password to open the document 開啟文件時需要密碼 + Reselect 重新選取 + Reset 重設 + Reset Bounding Box 重設邊框 + Reset Envelope Mesh 重設封套網格 + Reset Envelope Shape 重設封套形狀 + Reset Essentials 重設基本功能 + Reset Keyboard Shortcuts 重設鍵盤快速鍵 + Reset OpenType Features 重設 OpenType 功能 + Reset Panel 重設面板 + Reset Preferences 重設偏好設定 + Reset Profiles 重設描述檔 + Reset Rotate View 重設旋轉檢視 + Reset the rows to the artwork's colors 將各列重設為圖稿的顏色 + Reset to Default 重設為預設值 + Reset to White, Black 重設為白色、黑色 + Reset with Mesh 以網格重設 + Reset with Mesh… 以網格重設… + Reset with Warp 以彎曲重設 + Reset with Warp… 以彎曲重設… + Reset {name} 重設 {name} + Reset {} 重設 {} + Reshape 改變外框 + Reshape Segment 改變線段外框 + Reshape Tool 改變外框工具 + Resolution: 解析度: + Responsive 回應式 + Restore 還原 + Restore every preference to its default (applied on OK) 將所有偏好設定還原為預設值 (按下確定後套用) + Restore Recovered Documents 還原找回的文件 + Restored documents open unsaved: save them to keep them. 還原的文件會以未儲存狀態開啟:請儲存以保留。 + Restored {} recovered document(s): save them to keep them 已還原 {} 份復原的文件:請儲存以保留 + Restrict printing, editing and other tasks 限制列印、編輯及其他操作 + Reverse Front to Back 反轉前後順序 + Reverse Gradient 反轉漸層 + Reverse Order 反轉順序 + Reverse Path Direction 反轉路徑方向 + Reverse Path Direction Off 關閉反轉路徑方向 + Reverse Path Direction On 開啟反轉路徑方向 + Reverse Spine 反轉軸線 + Revert 回復 + Revert to the saved version of “{name}”? 要回復為「{name}」已儲存的版本嗎? + Revolve… 迴轉… + RGB Color RGB 色彩 + Right 右 + Right Grid: 右側格點: + Right in Front 右側在前 + Right Indent 右縮排 + Right Plane 右側平面 + Right-Justified Tab 靠右定位點 + Right: 右: + Rise… 上升… + Roman Hanging Punctuation 羅馬懸掛標點 + Rotate 旋轉 + Rotate 90° CW 順時針旋轉 90° + Rotate Tool 旋轉工具 + Rotate View Tool 旋轉檢視工具 + Rotate… 旋轉… + Rotation 旋轉 + Roughen… 粗糙效果… + Round Cap 圓端點 + Round Corners… 圓角… + Round Join 圓角 + Rounded Rectangle 圓角矩形 + Rounded Rectangle Tool 圓角矩形工具 + Rounded Rectangle… 圓角矩形… + Rows: 列數: + Rule: 規則線: + Rulers 尺標 + Rulers & Grids 尺標與格點 + Run Plug-in 執行增效模組 + Same 相同 + Same As Source 與來源相同 + Sample Color 取樣顏色 + Saturate 飽和 + Saturate… 增加飽和度… + Saturation 飽和度 + Save 儲存 + Save a Copy 儲存拷貝 + Save a Copy… 儲存拷貝… + Save As 另存新檔 + Save as PDF… 儲存為 PDF… + Save as preset: 儲存為預設集: + Save as Template 另存範本 + Save as Template… 另存範本… + Save As… 另存新檔… + Save changes to {name} 儲存對 {name} 的變更 + Save color group to Swatch panel 將顏色群組儲存至色票面板 + Save Colors as Swatches 將顏色儲存為色票 + Save Document 儲存文件 + Save Document Info… 儲存文件資訊… + Save each artboard to a separate file 將每個工作區域儲存為個別檔案 + Save for Office Documents 儲存為 Office 文件用 + Save for Office Documents… 儲存為 Office 文件用… + Save for Web 儲存為網頁用 + Save for Web (Legacy)… 儲存為網頁用 (舊版)… + Save for Web Presets 儲存為網頁用預設集 + Save for Web Preview 儲存為網頁用預覽 + Save for Web Settings 儲存為網頁用設定 + Save Graphic Style Library 儲存繪圖樣式資料庫 + Save Graphic Style Library… 儲存繪圖樣式資料庫… + Save Grid as Preset 將格點儲存為預設集 + Save Grid as Preset… 將格點儲存為預設集… + Save PDF 儲存 PDF + Save PDF Dialog 儲存 PDF 對話框 + Save PDF Preset 儲存 PDF 預設集 + Save Perspective Grid Preset 儲存透視格點預設集 + Save Preset 儲存預設集 + Save Preset… 儲存預設集… + Save Print Preset 儲存列印預設集 + Save Recovery Data 儲存救援資料 + Save Save for Web Preset 儲存「儲存為網頁用」預設集 + Save selected colors as swatches 將所選顏色儲存為色票 + Save Selection… 儲存選取範圍… + Save Swatch Library 儲存色票資料庫 + Save Swatch Library… 儲存色票資料庫… + Save the current layout under this name 以此名稱儲存目前的版面配置 + Save the document first? 要先儲存文件嗎? + Save the selected preset to a file 將所選預設集儲存為檔案 + Save the settings as a preset 將設定儲存為預設集 + Save Theme 儲存主題 + Save theme to My Themes 將主題儲存至「我的主題」 + Save these settings as a preset 將這些設定儲存為預設集 + Save these settings as a print preset 將這些設定儲存為列印預設集 + Save to Swatches 儲存至色票 + Save To: 儲存至: + Save Transparency Flattener Preset 儲存透明度平面化預設集 + Save Transparency Flattener Preset… 儲存透明度平面化預設集… + SAVED 已儲存 + Saved 已儲存 + Saved recovery data for {title} 已儲存 {title} 的復原資料 + Saved Selections 已儲存的選取範圍 + Saved to User Defined: {} 已儲存至使用者自訂:{} + Saved View 1 已儲存的檢視 1 + Saved View 10 已儲存的檢視 10 + Saved View 2 已儲存的檢視 2 + Saved View 3 已儲存的檢視 3 + Saved View 4 已儲存的檢視 4 + Saved View 5 已儲存的檢視 5 + Saved View 6 已儲存的檢視 6 + Saved View 7 已儲存的檢視 7 + Saved View 8 已儲存的檢視 8 + Saved View 9 已儲存的檢視 9 + Saved Views 已儲存的檢視 + Saved {n} files beside {path} 已將 {n} 個檔案儲存於 {path} 旁 + Saved {path} 已儲存 {path} + Saved {path} with {} note(s): {first} 已儲存 {path},附 {} 則附註:{first} + Saves the current bars, toolbar and panel layout. 儲存目前的列、工具列與面板配置。 + Save… 儲存… + Saving 正在儲存 + Saving Poster.vectorcraft 正在儲存 Poster.vectorcraft + Saving recovery data for {} 正在儲存 {} 的復原資料 + Saving {} 正在儲存 {} + Scale 縮放 + Scale Corners 縮放圓角 + Scale Cursor Proportional to UI 游標依介面比例縮放 + Scale Lineweights 縮放線條寬度 + Scale Strokes & Effects 縮放筆畫與效果 + Scale to Fit Artboard 縮放至符合工作區域 + Scale Tool 縮放工具 + Scale: 縮放比例: + Scale… 縮放… + Scaling 縮放 + Scaling: 縮放: + Scallop Tool 扇形化工具 + Scans: 掃描: + Scatter 散佈 + Scatter Graph Tool 散佈圖工具 + Scissors Tool 剪刀工具 + Scratch Disks 暫存磁碟 + Screen 濾色 + Screen (72 ppi) 螢幕 (72 ppi) + Screen Mode 螢幕模式 + Screens 螢幕 + Scribble… 塗抹… + Scripts 指令碼 + Search 搜尋 + Search All 搜尋全部 + Search commands and tools 搜尋指令與工具 + Search commands and tools… 搜尋指令與工具… + Search Commands… 搜尋指令… + Secondary 次要 + Section Symbol 章節符號 + Security 安全性 + Select 選取 + Select a text object 選取文字物件 + Select a text object to edit its character attributes. 選取文字物件以編輯其字元屬性。 + Select a text object to edit its paragraph attributes. 選取文字物件以編輯其段落屬性。 + Select a text object to set its tab stops. 選取文字物件以設定其定位點。 + Select All 全選 + Select All Art on Layer 選取圖層上的所有圖稿 + Select All Assets 選取所有資產 + Select All Instances 選取所有範例 + Select All Pins 全選圖釘 + Select All Unused 選取所有未使用項目 + Select an image to see its Link Info. 選取影像以檢視其連結資訊。 + Select an image to trace 選取要描圖的影像 + Select Anchors 選取錨點 + Select Appearance Item 選取外觀項目 + Select art and choose Object › Pattern › Make. 選取圖稿,然後選擇「物件 › 圖樣 › 製作」。 + Select art and click New Symbol to make one. 選取圖稿並按一下「新增符號」即可建立。 + Select art and use New Brush to make one. 選取圖稿,然後使用「新增筆刷」來製作。 + Select Freeform Point 選取任意形狀點 + Select Gradient Stop 選取漸層色標 + Select Objects 選取物件 + Select objects to see their CSS, or Generate CSS for the whole document. 選取物件以檢視其 CSS,或對整份文件「產生 CSS」。 + Select Same Tint % 選取相同色調 % + Select Slices 選取切片 + Select styled art and click New Graphic Style. 選取已套用樣式的圖稿並按一下「新增繪圖樣式」。 + Select text 選取文字 + Select Text by Font 依字體選取文字 + Select text or click into text with the Type tool to insert glyphs 使用文字工具選取文字或按一下文字內部,以插入字符 + Select text to set its OpenType features. 選取文字以設定其 OpenType 功能。 + Select the art (and, on top of it, the mask object) 選取圖稿 (以及位於其上方的遮色片物件) + Select Tool 選取工具 + Select: 選取: + Selected Artwork 選取的圖稿 + Selected Styles Only 僅限選取的樣式 + Selected Swatches 選取的色票 + Selected Swatches Only 僅限選取的色票 + Selection 選取範圍 + Selection & Anchor Display 選取和錨點顯示 + Selection Only 僅限選取範圍 + Selection Tool 選取工具 + Send Backward 後移 + Send to Back 移至最後 + Send to Current Layer 移至目前圖層 + Sentence case 句首大寫 + Separate colors into rows 將顏色分成多列 + Separate several keywords with commas. 多個關鍵字請以逗號分隔。 + Separations list the document's inks here: one page per ink that prints. 分色會在此列出文件的油墨:每種會列印的油墨各一頁。 + Separations Preview 分色預視 + Serialize Document 序列化文件 + Server-side (.map) 伺服器端 (.map) + Set 1 組 1 + Set Anchors 設定錨點 + Set base color 設定基準色 + Set base color to the current color 將基本色設為目前顏色 + Set Bounds 設定邊界 + Set Current Layer 設定目前圖層 + Set Key Object 設定關鍵物件 + Set Keyboard Shortcut 設定鍵盤快速鍵 + Set Mesh Point Color 設定網格點顏色 + Set Preference 設定偏好設定 + Set Slice Rectangle 設定切片矩形 + Set Tab Stops 設定定位點 + Set Text 設定文字 + Set the selected color to the current color 將選取的顏色設為目前顏色 + Set Zoom 設定縮放 + Settings… 設定… + Setup… 設定… + Shadows and Glows 陰影與光暈 + Shape 形狀 + Shape Builder 形狀建立 + Shape Builder Regions 形狀建立區域 + Shape Builder Tool 形狀建立工具 + Shape Effects 形狀效果 + Shape Modes: 形狀模式: + Shape Properties: 形狀內容: + Shaper Tool Shaper 工具 + Shapes 形狀 + Shear 傾斜 + Shear Tool 傾斜工具 + Shear… 傾斜… + Shell Lower… 下弧形… + Shell Upper… 上弧形… + Shift Shift + Shift to Color… 位移至顏色… + Shifts the tints of global and spot colors; other colors stay. 位移整體色與特別色的色調;其他顏色維持不變。 + Show 顯示 + Show Active Plane Widget 顯示作用中平面 Widget + Show All 全部顯示 + Show All Hidden Attributes 顯示所有隱藏屬性 + Show All Plates 顯示所有色版 + Show all plates 顯示所有色版 + Show All Swatches 顯示所有色票 + Show and print from the Lab definition; PDF files carry it as the ink's alternate color. 依 Lab 定義顯示與列印;PDF 檔案會將其作為油墨的替代色。 + Show and separate through the working CMYK space, as in older documents. 如同舊文件,透過工作 CMYK 色彩空間顯示與分色。 + Show Artboards 顯示工作區域 + Show Bounding Box 顯示邊框 + Show Brush Size 顯示筆刷大小 + Show Center 顯示中心點 + Show Code 顯示程式碼 + Show Color Groups 顯示顏色群組 + Show Color Swatches 顯示顏色色票 + Show Corner Widget 顯示轉角 Widget + Show East Asian Options 顯示東亞選項 + Show Edges 顯示邊緣 + Show Embedded 顯示已嵌入 + Show Find Field 顯示尋找欄位 + Show Font Names in English 以英文顯示字體名稱 + Show Gradient Annotator 顯示漸層註解者 + Show Gradient Swatches 顯示漸層色票 + Show Grid 顯示格點 + Show Grid (⌘') 顯示格點 (⌘') + Show Guides 顯示參考線 + Show handles when multiple anchors are selected 選取多個錨點時顯示控制把手 + Show Hidden Characters 顯示隱藏字元 + Show Images in Outline Mode 在外框模式中顯示影像 + Show in Folder 在資料夾中顯示 + Show Indic Options 顯示印度文字選項 + Show Link Info 顯示連結資訊 + Show List View 顯示清單檢視 + Show Mesh 顯示網格 + Show Missing 顯示遺失 + Show Modified 顯示已修改 + Show Note 顯示註解 + Show Options 顯示選項 + Show Package 顯示封裝 + Show Panel 顯示面板 + Show Pattern Swatches 顯示圖樣色票 + Show Perspective Grid 顯示透視格點 + Show Pixel Grid (Above 600% Zoom) 顯示像素格點 (縮放超過 600% 時) + Show Print Tiling 顯示列印分頁 + Show Rulers 顯示尺標 + Show Rulers (⌘R) 顯示尺標 (⌘R) + Show Slice Numbers 顯示切片編號 + Show Slices 顯示切片 + Show Swatch Bounds 顯示色票邊界 + Show Swatch Kinds 顯示色票種類 + Show Text Threads 顯示文字串連 + Show The Home Screen When No Documents Are Open 沒有開啟文件時顯示首頁 + Show Thumbnail 顯示縮圖 + Show Thumbnail View 顯示縮圖檢視 + Show Thumbnails 顯示縮圖 + Show Tile Edge 顯示拼貼邊緣 + Show Tints/Shades 顯示淺色調/深色調 + Show Tool Tips 顯示工具提示 + Show Transparency Grid 顯示透明度格點 + Show Video Rulers 顯示視訊尺標 + Show Vivid/Muted 顯示鮮豔/柔和 + Show Warm/Cool 顯示暖色/冷色 + Show {kind} Brushes 顯示{kind}筆刷 + Show/hide plate (Alt-click: only this plate) 顯示/隱藏色版 (Alt-按一下:僅此色版) + Show: 顯示: + Shown with the optimised preview. 於最佳化預覽時顯示。 + Side 1: 側面 1: + Side 2: 側面 2: + Sides: 邊數: + Simplify 簡化 + Simplify: 簡化: + Simplify… 簡化… + Simulate Colored Paper 模擬有色紙張 + Simulate Paper 模擬紙張 + Single / Double Column 單欄 / 雙欄 + Single Left Quotation Mark 左單引號 + Single Quotes: 單引號: + Single Right Quotation Mark 右單引號 + Single-line Composer 單行排版程式 + Sixth Space 六分之一空格 + Size 大小 + Size Tile to Art 拼貼符合圖稿大小 + Size/Leading 大小/行距 + Size: 大小: + Size: {size} Tracking: {tracking} 大小:{size} 字距:{tracking} + Skew 傾斜 + Skin Tone Ramps 膚色漸階 + Skip Blank Artboards 略過空白工作區域 + Sky Gradients 天空漸層 + Slice 切片 + Slice Options 切片選項 + Slice Options… 切片選項… + Slice Selection Tool 切片選取工具 + Slice Tool 切片工具 + Slice Type: 切片類型: + Slices 切片 + slices across, evenly spaced 個橫向切片,間距均等 + slices down, evenly spaced 個縱向切片,間距均等 + Slices: 切片: + Small 小 + Small Caps 小型大寫 + Small Caps (on the roadmap) 小型大寫字母 (規劃中) + Small Caps: 小型大寫字母: + Small List View 小型清單檢視 + Small Thumbnail View 小縮圖檢視 + Small Thumbnails 小縮圖 + Smaller ↔ Larger 較小 ↔ 較大 + Smart Guides 智慧型參考線 + Smart Guides (View → Smart Guides) 智慧型參考線 (檢視 → 智慧型參考線) + Smart Guides (⌘U) 智慧型參考線 (⌘U) + Smart Punctuation… 智慧型標點… + Smooth 平滑 + Smooth Color 平滑顏色 + Smooth Tool 平滑工具 + Snap Curves To Lines 將曲線靠齊線條 + Snap Options 靠齊選項 + Snap to Glyph 靠齊字符 + Snap to Grid 靠齊格點 + Snap to Pixel 靠齊像素 + Snap to Point 靠齊控制點 + Snapping Tolerance 靠齊容許度 + Social Square Post 1080×1080 社群正方形貼文 1080×1080 + Soft Light 柔光 + Solid 實心 + Sort by Kind 依類型排序 + Sort by Name 依名稱排序 + Sort by Status 依狀態排序 + Source code on GitHub GitHub 上的原始碼 + Source Sans 3 Source Sans 3 + Space After Paragraph 段後間距 + Space Before Paragraph 段前間距 + Spaces 空格 + Spacing Guides 間距參考線 + Spacing: 間距: + Spanish 西班牙文 + Specified Distance 指定距離 + Specified Steps 指定階數 + Specify: 指定: + Spectrum Gradients 光譜漸層 + Spelling 拼字 + Spiral 螺旋 + Spiral Tool 螺旋工具 + Split Freeform Line 分割任意形狀線條 + Split Into Grid 分割為格線 + Split Into Grid… 分割為格線… + Split Path 分割路徑 + spot 特別色 + Spot Color 特別色 + Spot Colors 特別色 + Spot Colors… 特別色… + Spread: 擴散: + Square 正方形 + Squeeze… 擠壓… + Stacked Bar Graph Tool 堆疊橫條圖工具 + Stacked Column Graph Tool 堆疊長條圖工具 + Stair Step 階梯 + Standard Ligatures 標準連字 + Standard Vertical Roman Alignment 標準垂直羅馬字對齊 + Standard: 標準: + Star 星形 + Star Tool 星形工具 + Start arrowhead 起點箭頭 + Start Global Edit 開始整體編輯 + Status 狀態 + Steps: 步驟: + Stop Editing Opacity Mask 停止編輯不透明度遮色片 + Stop editing the opacity mask 停止編輯不透明度遮色片 + Stop Playing/Recording 停止播放/錄製 + Stray Points 零散錨點 + Strikethrough 刪除線 + Stroke 筆畫 + Stroke (X), double-click for the Color Picker 筆畫 (X),按兩下開啟檢色器 + Stroke Color 筆畫顏色 + Stroke Options 筆畫選項 + Stroke options 筆畫選項 + Stroke Weight 筆畫寬度 + Stroke: 筆畫: + Stroke: click for swatches, Shift-click for the color mixer 筆畫:按一下開啟色票,按住 Shift 按一下開啟混色器 + Style 樣式 + Style Attributes 樣式屬性 + Style Attributes (Entity References) 樣式屬性 (實體參照) + Style Name: 樣式名稱: + Style: 樣式: + Styling: 樣式設定: + Stylize 風格化 + Subdivisions 細分 + Subscript 下標 + Subscript (on the roadmap) 下標 (規劃中) + Subscript: 下標: + Subset fonts below: 字體子集化門檻: + Subtract 減去 + Suffix 後綴 + Summary 摘要 + Superscript 上標 + Superscript (on the roadmap) 上標 (規劃中) + Superscript: 上標: + SVG Filters SVG 濾鏡 + SVG Interactivity SVG 互動性 + SVG Options SVG 選項 + SVG Profile: SVG 描述檔: + Swap Fill and Stroke 對調填色和筆畫 + Swap Fill and Stroke (Shift+X) 對調填色與筆畫 (Shift+X) + Swap start and end arrowheads 對調起點與終點箭頭 + Swap the selected object for the file, keeping its place and transform 以檔案取代選取的物件,並保留其位置與變形 + Swash 花體字 + Swatch Conflict 色票衝突 + Swatch Conflicts 色票衝突 + Swatch Libraries 色票資料庫 + Swatch Libraries Menu 色票資料庫選單 + Swatch Library 色票資料庫 + Swatch Name: 色票名稱: + Swatch Options 色票選項 + Swatch Options… 色票選項… + Swatch Tiles 色票拼貼 + Swatches 色票 + Switch workspace 切換工作區 + Symbol Instance 符號範例 + Symbol Libraries 符號資料庫 + Symbol Libraries (on the roadmap) 符號資料庫 (已列入路線圖) + Symbol Screener Tool 符號濾色器工具 + Symbol Scruncher Tool 符號緊縮器工具 + Symbol Shifter Tool 符號移位器工具 + Symbol Sizer Tool 符號縮放器工具 + Symbol Spinner Tool 符號旋轉器工具 + Symbol Sprayer 符號噴灑器 + Symbol Sprayer Tool 符號噴灑器工具 + Symbol Stainer Tool 符號染色器工具 + Symbol Styler Tool 符號樣式器工具 + Symbolism Tool 符號工具 + Symbols 符號 + System Layout 系統配置 + Tab 定位字元 + Tab Stops 定位點 + Tabs 定位點 + Tabular Lining 等寬齊線數字 + Tabular Oldstyle 等寬舊式數字 + Target 目標 + Target Contents 目標內容 + Target: 目標: + Tate-chu-yoko 縱中橫 + Temperature 色溫 + Temperature/Tint… 色溫/色調… + Template 範本 + Templates 範本 + Templates Folder 範本資料夾 + Templates… 範本… + Text Displayed in Cell: 儲存格中顯示的文字: + Text Export Options 文字匯出選項 + Text Fill Color 文字填色 + Text Import Options 文字匯入選項 + Text Stroke Color 文字筆畫顏色 + Text Wrap 文字繞圖 + Text Wrap Options 文字繞圖選項 + Text Wrap Options… 文字繞圖選項… + The bounds of the visible art. 可見圖稿的邊界。 + The cell shows the type object's text as HTML. 儲存格以 HTML 顯示文字物件的文字。 + The document has no colour swatches. 文件中沒有色票。 + The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. 印表機無法觸及的紙張邊緣:圖稿會放在其內側,預覽中會以虛線顯示。 + The page as it prints 頁面列印時的樣子 + the preset 該預設集 + The Print Tiling tool placed the pages. 已由列印拼貼工具放置頁面。 + The selected objects have no CSS of their own (see below). 選取的物件沒有自己的 CSS (請見下方)。 + The system's settings of this printer 此印表機的系統設定 + Thin Space 細空格 + Third Space 三分之一空格 + Third Vanishing Point: 第三消失點: + This document already has a swatch named “{name}” with another color. 此文件已有名為「{name}」但顏色不同的色票。 + This library is empty. 此資料庫是空的。 + This panel is on the roadmap (see the parity plan). 此面板已列入規劃 (請見對等計畫)。 + Threaded Text 串連文字 + Threads 串連 + Three Point 三點 + Three Point Perspective 三點透視 + Three Point Perspective Preset 1 三點透視預設集 1 + Three Point Perspective Preset 2 三點透視預設集 2 + Three Point Perspective Preset 3 三點透視預設集 3 + Three Point Perspective Preset 4 三點透視預設集 4 + Three Point Perspective Preset 5 三點透視預設集 5 + Threshold 臨界值 + Thumbnail View 縮圖檢視 + Tile 並排顯示 + Tile Edge Color 拼貼邊緣顏色 + Tile Edge Color… 拼貼邊緣顏色… + Tile Full Pages 並排完整頁面 + Tile Range: 拼貼範圍: + Tile Type: 拼貼類型: + tile {number} 拼貼 {number} + Tiles are numbered across, then down; leave the range empty for every tile. 拼貼先由左至右再由上至下編號;範圍留空表示全部拼貼。 + Tint 色調 + Tip extends past the end point 尖端延伸超過終點 + Tip on the end point 尖端位於終點 + Title Case 標題式大小寫 + to 至 + Toggle Fill/Stroke Focus 切換填色/筆畫焦點 + Toggle Selection 切換選取 + Toggle single/double column 切換單欄/雙欄 + Tolerance 容許度 + Tolerance: 容許度: + Tool Option 工具選項 + Tool Options… 工具選項… + Toolbar: Advanced / Basic 工具列:進階 / 基本 + Toolbar: Single/Double Column 工具列:單欄/雙欄 + Toolbars 工具列 + Tools 工具 + Top 上 + Top in Front 上方在前 + Top: 上: + Total Width: 總寬度: + Touch Type Tool 觸控文字工具 + Trace 描圖 + Tracing 描圖中 + Tracking 字距 + Trademark Symbol 商標符號 + Transform 變形 + Transform Again 再次變形 + Transform Both 兩者皆變形 + Transform Each 個別變形 + Transform Each… 個別變形… + Transform in Perspective 透視變形 + Transform Object Only 僅變形物件 + Transform Panel 變形面板 + Transform Pattern Only 僅變形圖樣 + Transform Pattern Tiles 變形圖樣拼貼 + Transform Patterns 變形圖樣 + Transform Tools 變形工具 + Transforms 變形 + Transform… 變形… + Transparency 透明度 + Transparency Flattener Presets 透明度平面化預設集 + Transparency Flattener Presets… 透明度平面化預設集… + Transparency Grid 透明度格點 + Transparency Info 透明度資訊 + Transparency is flattened with the preset. Type is written as outlines; gradients as bands of colour. 透明度會依預設集平面化。文字以外框寫出;漸層以色帶寫出。 + Transparency is flattened with the preset. Type is written as outlines; gradients stay smooth. 透明度會依預設集平面化。文字以外框寫出;漸層維持平滑。 + Transparency Options 透明度選項 + Transparent 透明 + Transparent (Default) 透明 (預設) + Transparent Background 透明背景 + Transverse (the page a quarter turn on the paper) 橫向轉置 (頁面在紙張上轉四分之一圈) + Trap… 補漏白… + Trim 剪裁 + Trim mark weight: 剪裁標記寬度: + Trim marks 剪裁標記 + Trim View 剪裁檢視 + True colour needs version 2004 or later: the nearest of 256 colours is written. 全彩需要 2004 或更新的版本:會寫入 256 色中最接近的顏色。 + Turn off Data Recovery for complex documents 對複雜文件關閉資料復原 + Tweak… 隨意筆畫… + Twirl Rate: 旋轉速率: + Twirl Tool 旋轉扭曲工具 + Twist… 螺旋… + Two Point 兩點 + Two Point Perspective 兩點透視 + Two Point Perspective Preset 1 兩點透視預設集 1 + Two Point Perspective Preset 2 兩點透視預設集 2 + Two Point Perspective Preset 3 兩點透視預設集 3 + Two Point Perspective Preset 4 兩點透視預設集 4 + Two Point Perspective Preset 5 兩點透視預設集 5 + Type 文字 + Type becomes outlines, and strokes a CAD line can't draw become filled shapes. 文字會變成外框,CAD 線條無法繪製的筆畫會變成填色形狀。 + Type Object Selection by Path Only 僅以路徑選取文字物件 + Type on a Path 路徑文字 + Type on a Path Options… 路徑文字選項… + Type on a Path Tool 路徑文字工具 + Type Orientation 文字方向 + Type stays text, and every stroke is a line with its lineweight and dashes. 文字維持為文字,每個筆畫都是帶有線寬與虛線的線條。 + Type Tool 文字工具 + Type: 類型: + Type… 文字… + Typography 字體排印 + UI Brightness 介面亮度 + UI Scaling 介面縮放 + Underline 底線 + Undo 復原 + Undo {name} 復原{name} + Undo {} 復原 {} + Unembed 取消嵌入 + Unembedded to {} 已取消嵌入至 {} + Unembed… 取消嵌入… + Ungroup 解散群組 + Ungroup Color Group 解散顏色群組 + Unicode Unicode + Unicode (UTF-16) Unicode (UTF-16) + Unicode (UTF-8) Unicode (UTF-8) + Uniform 等比例 + Unique 唯一 + Unite 聯集 + Unite and Simplify 聯集並簡化 + Units 單位 + Units: 單位: + Units: {units} 單位:{units} + Unlink harmony colors 取消連結調和色彩 + Unlink Opacity Mask 取消連結不透明度遮色片 + Unlink the mask 取消連結遮色片 + Unlock 解除鎖定 + Unlock All 全部解除鎖定 + Unlock Grid 解除鎖定格點 + Unlock Guides 解除鎖定參考線 + Unnamed Objects 未命名物件 + Unprintable margin: 不可列印邊界: + Unshift 取消位移 + Untitled 未命名 + Update Link 更新連結 + Update Links 更新連結 + Update Modified Links 更新已修改的連結 + Updated {} image(s) 已更新 {} 張影像 + UPPERCASE 全部大寫 + URL: URL: + Use 使用 + Use Artboards 使用工作區域 + Use Black Point Compensation 使用黑點補償 + Use Compression 使用壓縮 + Use document bleed settings 使用文件的出血設定 + Use Even-Odd Fill Rule 使用奇偶填色規則 + Use Japanese Crop Marks 使用日式裁切標記 + Use Low Resolution Proxy for Linked EPS 連結的 EPS 使用低解析度代理 + Use Non-Zero Winding Fill Rule 使用非零纏繞填色規則 + Use Precise Cursors 使用精確游標 + Use Pressure Pen 使用壓感筆 + Use Preview Bounds 使用預覽邊界 + Use Registration Point for Symbol 符號使用參考點 + Use Square for Previews 預覽使用方形 + Use Text for Previews 預覽使用文字 + Use the document's swatch: pasted colors take its color. 使用文件的色票:貼上的顏色會採用其顏色。 + Use the Selected Text's Font 使用選取文字的字體 + Use Typographer's Quotes 使用印刷體引號 + User Defined 使用者定義 + User Defined Libraries 使用者定義資料庫 + User Graphic Style Library 1 使用者繪圖樣式資料庫 1 + User Graphic Style Library 10 使用者繪圖樣式資料庫 10 + User Graphic Style Library 2 使用者繪圖樣式資料庫 2 + User Graphic Style Library 3 使用者繪圖樣式資料庫 3 + User Graphic Style Library 4 使用者繪圖樣式資料庫 4 + User Graphic Style Library 5 使用者繪圖樣式資料庫 5 + User Graphic Style Library 6 使用者繪圖樣式資料庫 6 + User Graphic Style Library 7 使用者繪圖樣式資料庫 7 + User Graphic Style Library 8 使用者繪圖樣式資料庫 8 + User Graphic Style Library 9 使用者繪圖樣式資料庫 9 + User Interface 使用者介面 + User Library 使用者資料庫 + User Preset 使用者預設集 + User Swatch Library 1 使用者色票資料庫 1 + User Swatch Library 10 使用者色票資料庫 10 + User Swatch Library 2 使用者色票資料庫 2 + User Swatch Library 3 使用者色票資料庫 3 + User Swatch Library 4 使用者色票資料庫 4 + User Swatch Library 5 使用者色票資料庫 5 + User Swatch Library 6 使用者色票資料庫 6 + User Swatch Library 7 使用者色票資料庫 7 + User Swatch Library 8 使用者色票資料庫 8 + User Swatch Library 9 使用者色票資料庫 9 + V Spacing: 垂直間距: + V: V: + Variable Width Profile 可變寬度描述檔 + Variables 變數 + Variation: 變化: + Vector Effects 向量效果 + Vector Halftone 向量半色調 + Vector Halftone… 向量半色調… + Vector illustration — fast, open, scriptable. 向量插圖:快速、開放、可用指令碼操控。 + VectorCraft VectorCraft + VectorCraft didn't quit normally last time. These documents had unsaved changes: VectorCraft 上次未正常結束。下列文件有未儲存的變更: + VectorCraft Help… VectorCraft 說明… + VectorCraft on getartcraft.com getartcraft.com 上的 VectorCraft + VectorCraft on GitHub GitHub 上的 VectorCraft + Vectors 向量 + Version {version} — open-source vector illustration in pure Rust. 版本 {version} — 以純 Rust 打造的開放原始碼向量插畫軟體。 + Version: 版本: + Vertical 垂直 + Vertical % 垂直 % + Vertical Align Bottom 垂直靠下對齊 + Vertical Align Center 垂直置中對齊 + Vertical Align Top 垂直靠上對齊 + Vertical Area Type Tool 垂直區域文字工具 + Vertical Distribute Bottom 垂直均分下緣 + Vertical Distribute Center 垂直均分中心 + Vertical Distribute Space 垂直均分間距 + Vertical Distribute Top 垂直均分上緣 + Vertical Scale % 垂直縮放 % + Vertical Type on a Path Tool 垂直路徑文字工具 + Vertical Type Tool 垂直文字工具 + Vertical: 垂直: + Very High 極高 + View 檢視 + View Artboard Only 僅檢視工作區域 + View Opacity Mask 檢視不透明度遮色片 + View PDF after saving 儲存後檢視 PDF + View {n} 檢視 {n} + Viewing Angle: 檢視角度: + Viewing Distance: 檢視距離: + W: 寬: + Warnings 警告 + Warp 彎曲 + Warp Options 彎曲選項 + Warp Tool 彎曲工具 + Wave… 波形… + Web 網頁 + Web 1920×1080 網頁 1920×1080 + Web Safe 216 網頁安全色 216 + Web Safe RGB 網頁安全 RGB + Web Shift 網頁位移 + Web Snap: 網頁靠齊: + Weight 寬度 + Weight: 寬度: + Welcome to VectorCraft 歡迎使用 VectorCraft + When pasting text 貼上文字時 + White 白色 + Widget Position: Widget 位置: + Width 寬度 + Width and Height 寬度與高度 + Width Point 寬度點 + Width Point Edit 寬度點編輯 + Width Point Edit… 寬度點編輯… + Width Profile 寬度描述檔 + Width Profiles 寬度描述檔 + Width Tool 寬度工具 + Width: 寬度: + Window 視窗 + Windows Windows + Working CMYK 使用中的 CMYK + Working CMYK (don't tag) 工作 CMYK (不加標記) + Workspace 工作區 + Workspace {i} 工作區 {i} + Wrinkle Tool 皺摺工具 + Write Layers 寫入圖層 + XML ID XML ID + Yards 碼 + Yellow 黃色 + Your browser downloads the files (several as one .zip). 瀏覽器會下載這些檔案 (多個檔案會合為一個 .zip)。 + Your changes will be lost if you don't save them. 若不儲存,您的變更將會遺失。 + Zig Zag… 鋸齒化… + Zoom In 放大顯示 + Zoom Out 縮小顯示 + Zoom to Selection 縮放至選取範圍 + Zoom Tool 縮放工具 + Zoom with Mouse Wheel 使用滑鼠滾輪縮放 + {count} file(s) in {folder}{note}. Show the package folder? {folder} 中有 {count} 個檔案{note}。要顯示封裝資料夾嗎? + {count} linked file(s) not found 找不到 {count} 個連結檔案 + {count} linked image(s) changed since this document was saved. 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} 個未命名物件:請為它們命名,或在面板選單中開啟「未命名物件」。 + {effect} is already applied 已套用 {effect} + {format} Options {format} 選項 + {kind} Gradient {kind}漸層 + {kind} Style Options… {kind}樣式選項… + {kind}, {model} {kind},{model} + {name} (spot) {name} (特別色) + {pages} in all, {count} per copy 共 {pages},每份 {count} + {page} of {count} 第 {page} 頁,共 {count} 頁 + {percent}% opacity 不透明度 {percent}% + {secs} sec {secs} 秒 + {shortcut} with the tool sizes the brush 以工具搭配 {shortcut} 調整筆刷大小 + {title} (selection) {title} (選取範圍) + {tool} Options {tool}選項 + {tool} Tool Options {tool}工具選項 + {warning}: click to correct to the closest color ({hex}) {warning}:按一下以修正為最接近的顏色 ({hex}) + “{file}” is protected by a password. 「{file}」受密碼保護。 + ● Recording ● 錄製中 +@plural {n} item|{n} items {n} 個項目 +@plural Color Table ({n} color)|Color Table ({n} colors) 色彩表 ({n} 色) +@plural Delete this {n} graphic style?|Delete these {n} graphic styles? 要刪除這 {n} 個繪圖樣式嗎? +@plural {n} Layer|{n} Layers {n} 個圖層 +@plural {n} Object|{n} Objects {n} 個物件 +@plural {n} area highlighted|{n} areas highlighted 已標示 {n} 個區域 +@plural {n} color|{n} colors {n} 色 +@plural {n} fill|{n} fills {n} 個填色 +@plural {n} glyph|{n} glyphs {n} 個字符 +@plural {n} object highlighted|{n} objects highlighted 已標示 {n} 個物件 +@plural {n} star|{n} stars {n} 個星芒 +@plural {n} step|{n} steps {n} 個步驟 +@plural {n} stroke|{n} strokes {n} 個筆畫 + the object to select | 物件以選取 | + to select multiple objects | 可選取多個物件 | + the object to duplicate 物件可製作複本 + an anchor or path segment to select it | 錨點或路徑線段以選取 | + to move | 可移動 | + to add 可新增 + to add a corner point | 可新增尖角錨點 | + to add a smooth point | 可新增平滑錨點 | + to close | 可封閉路徑 | + to finish 可結束 + to add smooth points | 可新增平滑錨點 | + to toggle corner | 可切換尖角 | + to create point type | 可建立點狀文字 | + to create area type | 可建立區域文字 | + to exit editing 可結束編輯 + to draw | 可繪製 | + to constrain proportions | 可等比例 | + from center | 可從中心繪製 | + for exact size 可輸入精確尺寸 + to set the reference point | 可設定參考點 | + to transform | 可變形 | + for exact values 可輸入精確數值 + to pan the view 可平移檢視 + to zoom in | 可放大 | + to zoom out | 可縮小 | + to zoom into an area 可放大至指定區域 + an object to copy its attributes | 物件可複製其屬性 | + to apply the selection's to it | 可將選取範圍的屬性套用至該物件 | + to sample a color 可取樣顏色 + across a selected object to set the gradient direction 橫越所選物件可設定漸層方向 + to select an artboard | 可選取工作區域 | + on the canvas to create one 畫布可建立工作區域 + a stroke to add a width point | 筆畫可新增寬度點 | + to change one side | 可只變更單側 | + to select more points | 可選取更多點 | + a point to edit it | 寬度點可編輯 | + to remove points 可移除點 + the art to add a pin | 圖稿可新增圖釘 | + to select more pins | 可選取更多圖釘 | + a pin to warp | 圖釘可彎曲 | + near a selected pin to rotate | 所選圖釘附近可旋轉 | + to remove pins 可移除圖釘 + across paths to push them along | 橫越路徑可推移路徑 | + to size the brush ( 可調整筆刷大小 ( + keeps its proportions) 可維持比例) + over paths, and hold still to keep going | 經過路徑,停住不動可持續作用 | + over paths to roughen their outlines | 經過路徑可使外框粗糙化 | + to move in perspective | 可在透視中移動 | + to copy | 可複製 | + while dragging to move perpendicular to the plane | 拖曳時可垂直於平面移動 | + to scale in perspective 可在透視中縮放 + an object, then another to blend them | 一個物件,再按另一個可建立漸變 | + to blend from it | 可從該處開始漸變 | + to set spacing and orientation 可設定間距與方向 + : : + Isolate Selected Group 隔離選取的群組 + Release Clipping Mask 釋放剪裁遮色片 + Make Compound Path 製作複合路徑 + Release Compound Path 釋放複合路徑 + — substituted by {style} — 以 {style} 取代 + — {n} characters from another font — {n} 個字符來自其他字體 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 81cff3729..3cf4855b7 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -5,6 +5,14 @@ //! shortcuts, the ⌘K palette and the control channel ([`control`]). #![forbid(unsafe_code)] +/// Translate a UI string literal into the language the UI is drawn in (see [`i18n`]). +#[macro_export] +macro_rules! tl { + ($s:expr) => { + $crate::i18n::t($s) + }; +} + pub mod background; mod brand; pub mod canvas; @@ -640,6 +648,14 @@ impl VectorcraftApp { /// eframe isn't a dependency of this crate (the host owns the event loop); these entry points are /// called from the host's `eframe::App` impl. impl VectorcraftApp { + /// The language the UI is drawn in: the Preferences dialog's choice while it is open (so a + /// change shows before OK), else the `interfaceLanguage` preference (`auto` = the system's). + pub fn ui_language(&self) -> i18n::Lang { + let editing = + self.ui.dialog.as_ref().filter(|d| d.kind == "preferences").and_then(|d| d.fields.get("interfaceLanguage")).and_then(Value::as_str); + i18n::Lang::from_pref(editing.unwrap_or(&self.session.prefs.interface_language)) + } + /// Per-frame logic before layout (control channel, shortcuts, inbox). A bug that panics costs /// one frame and shows an error, instead of closing the app with unsaved work. pub fn logic(&mut self, ctx: &egui::Context) { @@ -649,6 +665,7 @@ impl VectorcraftApp { } fn logic_frame(&mut self, ctx: &egui::Context) { + i18n::set_current(self.ui_language()); if !self.styled { theme::install_fonts(ctx); theme::apply(ctx, self.ui.brightness); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index e99e1ad4c..d58b2e387 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -50,7 +50,12 @@ use Item::Sep; /// UI-level commands: (id, label, shortcut, params doc). pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ - ("app.language", "Interface Language", "", "{lang: en|ja|cs} — persistent interface language"), + ( + "app.language", + "Interface Language", + "", + "{lang: auto|} the interface language, persisted as the `interfaceLanguage` preference (`auto` follows the system locale; codes: prefs.list › interfaceLanguage, e.g. en, ja, cs, zh-hant)", + ), ("file.open", "Open…", "Cmd+O", "{path?}"), ( "file.save", @@ -673,10 +678,16 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ /// Handle a UI command. `None` = not a UI command (the engine handles it). pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option> { if id == "app.language" { - let language = p.get("lang").and_then(Value::as_str).and_then(crate::i18n::Language::parse); - let Some(language) = language else { return Some(Err("lang must be en, ja or cs".into())) }; - app.ui.language = language; - return Some(Ok(json!(language))); + let lang = p.get("lang").and_then(Value::as_str).unwrap_or(""); + let value = if lang.eq_ignore_ascii_case("auto") { + "auto".to_string() + } else if let Some(l) = crate::i18n::Lang::from_code(lang) { + l.code().to_string() + } else { + let codes: Vec<&str> = std::iter::once("auto").chain(crate::i18n::Lang::all().map(|l| l.code())).collect(); + return Some(Err(format!("lang must be one of {}", codes.join(", ")))); + }; + return Some(app.run("prefs.set", json!({"key": "interfaceLanguage", "value": value})).map(|_| json!(value))); } if let Some(r) = crate::panels::character::intercept_text_command(app, id) { return Some(r); @@ -1110,7 +1121,13 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option Option { if id == "app.language" { - return Some(p.get("lang").and_then(Value::as_str).and_then(crate::i18n::Language::parse) == Some(app.ui.language)); + let lang = p.get("lang").and_then(Value::as_str).unwrap_or(""); + let pref = app.session.prefs.interface_language.as_str(); + return Some(if lang.eq_ignore_ascii_case("auto") { + crate::i18n::Lang::from_code(pref).is_none() + } else { + crate::i18n::Lang::from_code(pref).is_some_and(|l| l.code().eq_ignore_ascii_case(lang)) + }); } let v = &app.ui.view; Some(match id { @@ -1241,6 +1258,33 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String { } } +/// The label of a menu item as drawn: [`dynamic_label`] in the UI language. Labels assembled +/// around a name (Undo *Move*, Reset *Essentials*, Last Effect: *Drop Shadow*) are translated as +/// templates so the name can move; names that are user data (files, views, fonts) pass through. +pub fn display_label(app: &VectorcraftApp, id: &str, label: &str) -> String { + let lang = crate::i18n::current(); + let fmt = crate::i18n::fmt; + match id { + "edit.undo" | "edit.redo" => { + let last = app.session.active().and_then(|d| if id == "edit.undo" { d.history.undo.last() } else { d.history.redo.last() }); + match last { + Some(h) if id == "edit.undo" => fmt(tl!("Undo {name}"), &[("name", tl!(&h.label))]), + Some(h) => fmt(tl!("Redo {name}"), &[("name", tl!(&h.label))]), + None => tl!(if id == "edit.undo" { "Undo" } else { "Redo" }).to_string(), + } + } + "window.workspace.reset" => fmt(tl!("Reset {name}"), &[("name", tl!(&app.ui.workspace))]), + "effect.last" => match &app.last_effect { + Some((e, _)) => { + let name = vectorcraft_effects::effect_info(e).map(|i| i.label).unwrap_or(e.as_str()); + fmt(tl!("Last Effect: {name}"), &[("name", tl!(name))]) + } + None => crate::i18n::tr_id(lang, id, label).to_string(), + }, + _ => crate::i18n::tr_id(lang, id, &dynamic_label(app, id, label)).to_string(), + } +} + /// Menu slots that are left out while they have nothing to show (unused saved views, recent files, /// User Defined swatch and graphic style libraries). fn hidden_when_disabled(id: &str) -> bool { @@ -1466,11 +1510,10 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { c("Settings…", "edit.preferences"), sub( "Language", - vec![ - cp("English", "app.language", json!({"lang": "en"})), - cp("日本語", "app.language", json!({"lang": "ja"})), - cp("Čeština", "app.language", json!({"lang": "cs"})), - ], + std::iter::once(cp("Automatic", "app.language", json!({"lang": "auto"}))) + .chain(std::iter::once(Sep)) + .chain(crate::i18n::Lang::all().map(|l| cp(l.name(), "app.language", json!({"lang": l.code()})))) + .collect(), ), Sep, sub("UI Brightness", Brightness::ALL.iter().map(|b| cp(b.label(), "window.brightness", json!({"brightness": b.id()}))).collect()), @@ -2244,9 +2287,9 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 { .ui(ui, |ui| { for (i, (title, items)) in tree.iter().enumerate() { let text = if i == 0 { - egui::RichText::new(app.ui.language.tr(title)).font(theme::semibold(13.0)).color(t.text) + egui::RichText::new(tl!(title)).font(theme::semibold(13.0)).color(t.text) } else { - egui::RichText::new(app.ui.language.tr(title)).size(13.0).color(t.text) + egui::RichText::new(tl!(title)).size(13.0).color(t.text) }; ui.menu_button(text, |ui| menu_body(app, ui, items, &mut clicked)); } @@ -2278,10 +2321,10 @@ fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], checks: ui.separator(); } Item::Header(h) => { - ui.label(egui::RichText::new(app.ui.language.tr(h)).size(11.0).color(t.text_dim)); + ui.label(egui::RichText::new(tl!(h)).size(11.0).color(t.text_dim)); } Item::Sub(label, children) => { - ui.menu_button(app.ui.language.tr(label), |ui| { + ui.menu_button(tl!(label), |ui| { widgets::menu_scroll(ui, |ui| { ui.set_min_width(200.0); render_items(app, ui, children, checks, clicked); @@ -2290,10 +2333,10 @@ fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], checks: } Item::Todo(label, sc) => { ui.add_enabled_ui(false, |ui| { - ui.add(egui::Button::new(app.ui.language.tr(label)).shortcut_text(pretty_shortcut(sc))); + ui.add(egui::Button::new(tl!(label)).shortcut_text(pretty_shortcut(sc))); }) .response - .on_disabled_hover_text("Coming soon — tracked in the parity plan"); + .on_disabled_hover_text(tl!("Coming soon — tracked in the parity plan")); } Item::Cmd(label, id, p) => { let en = enabled(app, id); @@ -2301,8 +2344,7 @@ fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], checks: if (!en && hidden_when_disabled(id)) || swapped_out(app, id) { continue; } - let label = dynamic_label(app, id, label); - let label = app.ui.language.tr(&label).to_string(); + let label = display_label(app, id, label); let sc = item_shortcut(id, p).map(pretty_shortcut).unwrap_or_default(); let chk = checks.then(|| checked(app, id, p)).flatten(); let text = match chk { @@ -2729,6 +2771,127 @@ pub fn item_shortcut(id: &str, p: &Value) -> Option<&'static str> { } } +/// Labels only the canvas context menu ([`context_items`]) shows, so the catalog tests can insist +/// every one of them is translated (a test checks the list against the menu). +pub const CONTEXT_LABELS: &[&str] = &[ + "Isolate Selected Group", + "Exit Isolation Mode", + "Make Clipping Mask", + "Release Clipping Mask", + "Make Compound Path", + "Release Compound Path", + "Select All", +]; + +/// The labels [`dynamic_label`] can show in place of an item's own (Show/Hide pairs and the like), +/// so the catalog tests can insist every one of them is translated. +pub const DYNAMIC_LABELS: &[&str] = &[ + "Preview", + "Outline", + "Hide Edges", + "Show Edges", + "Hide Corner Widget", + "Show Corner Widget", + "Hide Text Threads", + "Show Text Threads", + "Hide Hidden Characters", + "Show Hidden Characters", + "Hide Gradient Annotator", + "Show Gradient Annotator", + "Hide Artboards", + "Show Artboards", + "Hide Rulers", + "Show Rulers", + "Hide Bounding Box", + "Show Bounding Box", + "Hide Transparency Grid", + "Show Transparency Grid", + "Hide Guides", + "Show Guides", + "Hide Grid", + "Show Grid", + "Unlock Guides", + "Lock Guides", + "Show Slices", + "Hide Slices", + "Hide Print Tiling", + "Show Print Tiling", + "Edit Envelope", + "Edit Contents", + "Undo", + "Redo", + "Unlock Grid", + "Lock Grid", +]; + +/// Every English string the menus, the command palette, the panel registry, the toolbar and the +/// Preferences dialog can show: what a complete language catalog has to cover. Names that are user +/// data (fonts, recent files, saved views, libraries) and the point sizes are left out. +pub fn menu_strings() -> std::collections::BTreeSet { + use std::collections::BTreeSet; + fn user_data(id: &str) -> bool { + hidden_when_disabled(id) || id.starts_with("type.recentFont") + } + fn walk(items: &[Item], under: &str, out: &mut BTreeSet) { + for it in items { + match it { + Item::Cmd(l, id, _) => { + let size = l.strip_suffix(" pt").is_some_and(|n| n.chars().all(|c| c.is_ascii_digit())); + // VectorCraft › Language lists each language by its own name. + let language_name = *id == "app.language" && crate::i18n::Lang::all().any(|lang| lang.name() == *l); + if !user_data(id) && !size && !language_name && *l != "—" { + out.insert(l.to_string()); + } + } + Item::Todo(l, _) | Item::Header(l) => { + out.insert(l.to_string()); + } + Item::Sub(l, children) => { + out.insert(l.to_string()); + // Type › Font lists the installed families: names, not UI text. + if !(under == "Type" && *l == "Font") { + walk(children, l, out); + } + } + Item::Sep => {} + } + } + } + let mut out = BTreeSet::new(); + for (title, items) in menu_tree() { + out.insert(title.to_string()); + walk(&items, title, &mut out); + } + out.extend(DYNAMIC_LABELS.iter().map(|s| s.to_string())); + out.extend(CONTEXT_LABELS.iter().map(|s| s.to_string())); + out.extend(UI_COMMANDS.iter().map(|c| c.1.to_string())); + for c in vectorcraft_engine::cmd::command_specs() { + out.insert(c.label.to_string()); + out.extend(c.menu.iter().map(|m| m.to_string())); + } + out.extend(ICON_PANELS.iter().map(|p| p.1.to_string())); + out.extend(["Properties", "Layers", "Libraries"].map(str::to_string)); + out.extend(vectorcraft_tools::catalog::all_tools().map(|t| t.label.to_string())); + out.extend(crate::toolbar::BASIC.iter().map(|c| c.0.to_string())); + out.extend(vectorcraft_color::BlendMode::ALL.iter().map(|m| m.label().to_string())); + out.extend(vectorcraft_effects::effect_catalog().iter().map(|e| e.label.to_string())); + out.extend(vectorcraft_engine::cmd::prefscmds::PREF_CATEGORIES.iter().map(|c| c.to_string())); + for sp in vectorcraft_engine::cmd::prefscmds::PREF_SPECS { + out.insert(sp.label.to_string()); + if !sp.section.is_empty() { + out.insert(sp.section.to_string()); + } + if let vectorcraft_engine::cmd::prefscmds::PrefKind::Choice(opts) = sp.kind { + out.extend(opts.iter().map(|o| o.1.to_string())); + } + } + out.extend(crate::prefs_dialog::UI_FIELDS.iter().map(|f| f.2.to_string())); + out.extend(crate::dialogs::button_labels().into_iter().map(str::to_string)); + out.extend(crate::chrome::hint_strings().into_iter().map(str::to_string)); + out.remove(""); + out +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index 31ac6527d..aa7699c3d 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -23,6 +23,12 @@ pub fn items() -> Vec<(String, String, String)> { items } +/// A palette label in the UI language: each `›`-separated menu segment and the command label +/// translate on their own. +fn shown_label(label: &str) -> String { + label.split(" › ").map(crate::i18n::t).collect::>().join(" › ") +} + pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { if !app.ui.palette_open { return; @@ -32,19 +38,28 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let items = items(); let matches: Vec<&(String, String, String)> = items .iter() - .filter(|(l, id, _)| q.is_empty() || q.split_whitespace().all(|w| l.to_lowercase().contains(w) || id.to_lowercase().contains(w))) + .filter(|(l, id, _)| { + q.is_empty() || { + // Match the English text (what agents document), the shown text and the id. + let shown = shown_label(l).to_lowercase(); + q.split_whitespace().all(|w| l.to_lowercase().contains(w) || shown.contains(w) || id.to_lowercase().contains(w)) + } + }) .take(14) .collect(); let mut run: Option = None; egui::Area::new(egui::Id::new("palette")).order(egui::Order::Foreground).anchor(egui::Align2::CENTER_TOP, [0.0, 90.0]).show(ctx, |ui| { egui::Frame::popup(ui.style()).fill(t.panel).inner_margin(egui::Margin::same(10)).show(ui, |ui| { ui.set_width(520.0); - let r = ui.add(egui::TextEdit::singleline(&mut app.ui.palette_query).hint_text("Search commands and tools…").desired_width(500.0)); + let r = ui.add(egui::TextEdit::singleline(&mut app.ui.palette_query).hint_text(tl!("Search commands and tools…")).desired_width(500.0)); r.request_focus(); ui.add_space(6.0); for (i, (label, id, sc)) in matches.iter().enumerate() { let resp = ui.add( - egui::Button::new(label.as_str()).shortcut_text(menus::pretty_shortcut(sc)).min_size(egui::vec2(500.0, 24.0)).selected(i == 0), + egui::Button::new(shown_label(label)) + .shortcut_text(menus::pretty_shortcut(sc)) + .min_size(egui::vec2(500.0, 24.0)) + .selected(i == 0), ); if resp.clicked() { run = Some(id.clone()); diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index b33b407bb..3ad51aa19 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -76,7 +76,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { ui.painter().text( r.right_center() - egui::vec2(6.0, 0.0), egui::Align2::RIGHT_CENTER, - format!("{} step{}", a.steps.len(), if a.steps.len() == 1 { "" } else { "s" }), + crate::i18n::tn(a.steps.len() as u64, "{n} step", "{n} steps"), egui::FontId::proportional(11.0), t.text_dim, ); @@ -94,7 +94,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { // Bottom bar: stop, record, play, new, delete. ui.horizontal(|ui| { let recording = app.ui.recording.is_some(); - if widgets::icon_button(ui, "square", "Stop Playing/Recording", false, 24.0).clicked() + if widgets::icon_button(ui, "square", tl!("Stop Playing/Recording"), false, 24.0).clicked() && let Some((set, name, start)) = app.ui.recording.take() { let steps = app.session.journal_for_action(start); @@ -104,7 +104,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { } let (r, resp) = ui.allocate_exact_size(egui::vec2(24.0, 24.0), egui::Sense::click()); ui.painter().circle_filled(r.center(), 6.0, if recording { egui::Color32::from_rgb(0xe0, 0x30, 0x30) } else { t.icon }); - if resp.on_hover_text("Begin Recording").clicked() && !recording { + if resp.on_hover_text(tl!("Begin Recording")).clicked() && !recording { let n = app.ui.action_sets.first().map(|s| s.actions.len()).unwrap_or(0) + 1; if app.ui.action_sets.is_empty() { app.ui.action_sets.push(ActionSet { name: "Set 1".into(), actions: vec![] }); @@ -112,12 +112,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let set = selected.map(|s| s.0).unwrap_or(0); app.ui.recording = Some((set, format!("Action {n}"), app.session.journal.len())); } - if widgets::icon_button(ui, "dc-actions", "Play Current Selection", false, 24.0).clicked() + if widgets::icon_button(ui, "dc-actions", tl!("Play Current Selection"), false, 24.0).clicked() && let Some(s) = selected { play_req = Some(s); } - if widgets::icon_button(ui, "trash-2", "Delete Selection", false, 24.0).clicked() + if widgets::icon_button(ui, "trash-2", tl!("Delete Selection"), false, 24.0).clicked() && let Some((si, ai)) = selected && let Some(s) = app.ui.action_sets.get_mut(si) && ai < s.actions.len() @@ -126,7 +126,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { ui.data_mut(|d| d.insert_temp::>(selected_id, None)); } if recording { - ui.label(egui::RichText::new("● Recording").color(egui::Color32::from_rgb(0xe0, 0x30, 0x30))); + ui.label(egui::RichText::new(tl!("● Recording")).color(egui::Color32::from_rgb(0xe0, 0x30, 0x30))); } }); if let Some((si, ai)) = play_req @@ -136,7 +136,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { } let _ = icons::exists; if app.ui.action_sets.is_empty() { - dim_label(ui, "No actions. Press ● to record."); + dim_label(ui, tl!("No actions. Press ● to record.")); } } diff --git a/crates/ui-egui/src/panels/align.rs b/crates/ui-egui/src/panels/align.rs index 16d4d2095..a469412f6 100644 --- a/crates/ui-egui/src/panels/align.rs +++ b/crates/ui-egui/src/panels/align.rs @@ -55,27 +55,27 @@ pub const DISTRIBUTE: [(&str, &str, &str, &str); 6] = [ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let n = selection_len(app); let to: AlignTo = pstate(ui.ctx(), "align-to"); - widgets::subheader(ui, "Align Objects:"); + widgets::subheader(ui, tl!("Align Objects:")); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 5.0; for (i, (icon, tip, axis, v)) in ALIGN.iter().enumerate() { if i == 3 { ui.add_space(6.0); } - if widgets::icon_button_enabled(ui, icon, tip, false, n >= 1, 32.0).clicked() { + if widgets::icon_button_enabled(ui, icon, tl!(tip), false, n >= 1, 32.0).clicked() { app.run("object.align", align_params(json!({*axis: v}), to, n)).ok(); } } }); widgets::divider(ui); - widgets::subheader(ui, "Distribute Objects:"); + widgets::subheader(ui, tl!("Distribute Objects:")); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 5.0; for (i, (icon, tip, axis, v)) in DISTRIBUTE.iter().enumerate() { if i == 3 { ui.add_space(6.0); } - if widgets::icon_button_enabled(ui, icon, tip, false, n >= 2, 32.0).clicked() { + if widgets::icon_button_enabled(ui, icon, tl!(tip), false, n >= 2, 32.0).clicked() { app.run("object.distribute", json!({*axis: v})).ok(); } } @@ -86,16 +86,16 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { widgets::divider(ui); ui.horizontal(|ui| { ui.vertical(|ui| { - widgets::subheader(ui, "Distribute Spacing:"); + widgets::subheader(ui, tl!("Distribute Spacing:")); let spacing: f64 = pstate::>(ui.ctx(), "align-spacing").unwrap_or(0.0); let key = to == AlignTo::Key; ui.horizontal(|ui| { let on = n >= 2; let sp = if key { Some(spacing) } else { None }; - if widgets::icon_button_enabled(ui, "dc-dist-vspace", "Vertical Distribute Space", false, on, 28.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-dist-vspace", tl!("Vertical Distribute Space"), false, on, 28.0).clicked() { app.run("object.distributeSpacing", json!({"axis": "vertical", "spacing": sp})).ok(); } - if widgets::icon_button_enabled(ui, "dc-dist-hspace", "Horizontal Distribute Space", false, on, 28.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-dist-hspace", tl!("Horizontal Distribute Space"), false, on, 28.0).clicked() { app.run("object.distributeSpacing", json!({"axis": "horizontal", "spacing": sp})).ok(); } ui.add_enabled_ui(key, |ui| { @@ -107,14 +107,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); ui.separator(); ui.vertical(|ui| { - widgets::subheader(ui, "Align To:"); + widgets::subheader(ui, tl!("Align To:")); ui.horizontal(|ui| { for (v, icon, tip) in [ - (AlignTo::Selection, "dc-alignto-selection", "Align to Selection"), - (AlignTo::Key, "dc-alignto-key", "Align to Key Object"), - (AlignTo::Artboard, "dc-alignto-artboard", "Align to Artboard"), + (AlignTo::Selection, "dc-alignto-selection", tl!("Align to Selection")), + (AlignTo::Key, "dc-alignto-key", tl!("Align to Key Object")), + (AlignTo::Artboard, "dc-alignto-artboard", tl!("Align to Artboard")), ] { - if widgets::icon_button(ui, icon, tip, to == v, 28.0).clicked() { + if widgets::icon_button(ui, icon, tl!(tip), to == v, 28.0).clicked() { set_pstate(ui.ctx(), "align-to", v); } } @@ -125,19 +125,19 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "align-hide-options"); - if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), "align-hide-options", !hidden); } let pb = app.session.prefs.use_preview_bounds; - if menu_item(ui, "Use Preview Bounds", true, pb) { + if menu_item(ui, tl!("Use Preview Bounds"), true, pb) { super::transform::set_pref(app, "usePreviewBounds", !pb); } let to: AlignTo = pstate(ui.ctx(), "align-to"); - if menu_item(ui, "Cancel Key Object", to == AlignTo::Key, false) { + if menu_item(ui, tl!("Cancel Key Object"), to == AlignTo::Key, false) { set_pstate(ui.ctx(), "align-to", AlignTo::Selection); app.run("select.key", json!({})).ok(); } - menu_item(ui, "Align to Glyph Bounds", false, false); + menu_item(ui, tl!("Align to Glyph Bounds"), false, false); } #[cfg(test)] diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs index 2f4822eaa..32d0d990e 100644 --- a/crates/ui-egui/src/panels/appearance.rs +++ b/crates/ui-egui/src/panels/appearance.rs @@ -157,11 +157,11 @@ pub fn effect_label(id: &str) -> String { /// "Opacity: Default" or "Opacity: 50% Multiply". pub fn opacity_text(opacity: f32, blend: BlendMode) -> String { if (opacity - 1.0).abs() < 1e-4 && blend == BlendMode::Normal { - "Default".into() + tl!("Default").into() } else if blend == BlendMode::Normal { format!("{:.0}%", opacity * 100.0) } else { - format!("{:.0}% {}", opacity * 100.0, blend.label()) + format!("{:.0}% {}", opacity * 100.0, tl!(blend.label())) } } @@ -188,7 +188,7 @@ fn eye(ui: &mut Ui, r: Rect, id: impl std::hash::Hash + std::fmt::Debug, on: boo t.icon }; icons::paint(ui, if on { "eye" } else { "eye-off" }, er, col); - enabled && resp.on_hover_text("Click to toggle visibility").clicked() + enabled && resp.on_hover_text(tl!("Click to toggle visibility")).clicked() } fn chevron(ui: &mut Ui, r: Rect, id: impl std::hash::Hash + std::fmt::Debug, open: bool) -> bool { @@ -241,7 +241,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.painter().line_segment([r.left_bottom(), r.right_bottom()], Stroke::new(1.0, t.input_border)); let mixed = mixed_appearances(app); - let label = if mixed { "Mixed Appearances" } else { object_label(app) }; + let label = if mixed { tl!("Mixed Appearances") } else { tl!(object_label(app)) }; // A linked object names its graphic style ("Rectangle: Sunshine"), and so does new art // with nothing selected. let style = match app.session.selection_graphic_style() { @@ -277,7 +277,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { /// The object row's thumbnail drags this object's appearance onto art ([`PanelDrag::Appearance`]; /// a chip of its fill follows the pointer). fn thumbnail_drag(ui: &mut Ui, th: Rect, id: NodeId) { - let resp = ui.interact(th, ui.id().with("ap-thumb"), Sense::drag()).on_hover_text("Drag onto art to apply this appearance"); + let resp = ui.interact(th, ui.id().with("ap-thumb"), Sense::drag()).on_hover_text(tl!("Drag onto art to apply this appearance")); widgets::drag_source(ui, &resp, || PanelDrag::Appearance(id)); if resp.dragged() { ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing); @@ -320,7 +320,7 @@ fn default_stack(app: &mut VectorcraftApp, ui: &mut Ui) { let fx = |ui: &mut Ui, e: &Effect, id: (Option, usize)| { let (r, _) = row(ui, false); eye(ui, r, ("ap-def-fx", id), e.visible, false); - text(ui, pos2(r.left() + EYE_W + 24.0 + if id.0.is_some() { 12.0 } else { 0.0 }, r.center().y), &effect_label(&e.id), false); + text(ui, pos2(r.left() + EYE_W + 24.0 + if id.0.is_some() { 12.0 } else { 0.0 }, r.center().y), tl!(&effect_label(&e.id)), false); icons::paint(ui, "dc-fx", Rect::from_center_size(r.right_center() - vec2(14.0, 0.0), vec2(16.0, 16.0)), t.icon); }; for (i, it) in ap.items.iter().enumerate().rev() { @@ -328,8 +328,8 @@ fn default_stack(app: &mut VectorcraftApp, ui: &mut Ui) { eye(ui, r, ("ap-def-eye", i), it.visible(), false); let lx = r.left() + EYE_W + 24.0; if it.is_fill() { - text(ui, pos2(lx, r.center().y), "Fill:", false); - } else if link(ui, pos2(lx, r.center().y), ("ap-def-link", i), "Stroke:") { + text(ui, pos2(lx, r.center().y), tl!("Fill:"), false); + } else if link(ui, pos2(lx, r.center().y), ("ap-def-link", i), tl!("Stroke:")) { app.ui.open_panel = Some("stroke".into()); } chip(ui, Rect::from_min_size(pos2(r.left() + EYE_W + 76.0, r.center().y - 9.0), vec2(18.0, 18.0)), it.paint()); @@ -347,7 +347,12 @@ fn default_stack(app: &mut VectorcraftApp, ui: &mut Ui) { } let (r, _) = row(ui, false); eye(ui, r, "ap-def-op", true, false); - text(ui, r.left_center() + vec2(EYE_W + 24.0, 0.0), &format!("Opacity: {}", opacity_text(opacity, blend)), false); + text( + ui, + r.left_center() + vec2(EYE_W + 24.0, 0.0), + &crate::i18n::fmt(tl!("Opacity: {value}"), &[("value", &opacity_text(opacity, blend))]), + false, + ); } /// A stack row being dragged. @@ -409,12 +414,12 @@ fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel, object_row: } let lx = r.left() + EYE_W + 24.0; if is_stroke { - if link(ui, pos2(lx, r.center().y), ("ap-link", i), "Stroke:") { + if link(ui, pos2(lx, r.center().y), ("ap-link", i), tl!("Stroke:")) { select_row(app, ui.ctx(), Sel::Item(i)); app.ui.open_panel = Some("stroke".into()); } } else { - text(ui, pos2(lx, r.center().y), "Fill:", false); + text(ui, pos2(lx, r.center().y), tl!("Fill:"), false); } // Swatch with a swatches popup that sets this item's paint; Shift-click opens the Color // panel's mixer on this row instead. @@ -422,7 +427,7 @@ fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel, object_row: chip(ui, cr, paint); let cresp = ui .interact(cr.expand(2.0), ui.id().with(("ap-chip", i)), Sense::click()) - .on_hover_text("Click to choose a swatch, Shift-click to mix a colour"); + .on_hover_text(tl!("Click to choose a swatch, Shift-click to mix a colour")); let mixer = cresp.clicked() && ui.input(|inp| inp.modifiers.shift); if mixer { select_row(app, ui.ctx(), Sel::Item(i)); @@ -597,7 +602,7 @@ fn drop_command(n: &Node, rows: &[(Slot, Rect)], what: Dragged, p: egui::Pos2, c fn contents_row(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, label: &str, sel: Sel) -> (Rect, egui::Response) { let (r, resp) = row(ui, sel == Sel::Contents); eye(ui, r, "ap-contents-eye", true, false); - text(ui, pos2(r.left() + EYE_W + 24.0, r.center().y), label, false); + text(ui, pos2(r.left() + EYE_W + 24.0, r.center().y), tl!(label), false); let characters = matches!(n.kind, vectorcraft_doc::NodeKind::Text(_)); if characters { for (k, stroke) in [false, true].into_iter().enumerate() { @@ -605,7 +610,8 @@ fn contents_row(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, label: &str, se chip(ui, cr, n.proxy_paint(stroke, None).map_or(&Paint::None, |(p, ..)| p)); } } - let resp = resp.on_hover_text(if characters { "Double-click to edit the characters' paint" } else { "Double-click to target the contents" }); + let resp = + resp.on_hover_text(if characters { tl!("Double-click to edit the characters' paint") } else { tl!("Double-click to target the contents") }); if resp.clicked() || resp.double_clicked() { select_row(app, ui.ctx(), Sel::Contents); } @@ -623,7 +629,7 @@ fn contents_row(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, label: &str, se fn stroke_notes(ui: &Ui, r: Rect, st: &vectorcraft_doc::StrokeLayer) { let t = Tokens::get(ui.ctx()); let painter = ui.painter_at(r); - let dashed = st.dash.as_ref().is_some_and(|d| d.is_dashed()).then_some("Dashed"); + let dashed = st.dash.as_ref().is_some_and(|d| d.is_dashed()).then_some(tl!("Dashed")); let notes: Vec<&str> = st.brush.as_deref().into_iter().chain(dashed).collect(); let mut x = r.left(); if !notes.is_empty() { @@ -646,7 +652,7 @@ fn opacity_row(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, opaci let (r, resp) = row(ui, false); eye(ui, r, ("ap-op-eye", item), true, false); let lx = r.left() + EYE_W + 24.0 + if item.is_some() { 12.0 } else { 0.0 }; - let clicked = link(ui, pos2(lx, r.center().y), ("ap-op-link", item), "Opacity:") || resp.clicked(); + let clicked = link(ui, pos2(lx, r.center().y), ("ap-op-link", item), tl!("Opacity:")) || resp.clicked(); text(ui, pos2(lx + 56.0, r.center().y), &opacity_text(opacity, blend), false); egui::Popup::menu(&resp) .open_memory(clicked.then_some(egui::SetOpenCommand::Toggle)) @@ -680,7 +686,7 @@ fn effect_row(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: usi set_pstate(ui.ctx(), &open_key, !open); } let lx = r.left() + EYE_W + 24.0; - if link(ui, pos2(lx, r.center().y), ("ap-fx-link", item, k), &effect_label(&e.id)) { + if link(ui, pos2(lx, r.center().y), ("ap-fx-link", item, k), tl!(&effect_label(&e.id))) { select_row(app, ui.ctx(), this); if has_options(&e.id) { app.run("effect.dialog", json!({"effect": e.id, "index": k, "item": item})).ok(); @@ -722,12 +728,12 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: egui::Frame::NONE.fill(t.panel_darker).inner_margin(egui::Margin { left: (EYE_W + 12.0) as i8, right: 6, top: 4, bottom: 4 }).show(ui, |ui| { ui.spacing_mut().item_spacing.y = 3.0; if params.is_empty() { - widgets::dim_label(ui, "No options"); + widgets::dim_label(ui, tl!("No options")); } for (key, v) in ¶ms { ui.horizontal(|ui| { let label = humanize(key); - ui.add_sized(vec2(84.0, 22.0), egui::Label::new(egui::RichText::new(label).size(11.5).color(t.text)).truncate()); + ui.add_sized(vec2(84.0, 22.0), egui::Label::new(egui::RichText::new(tl!(&label)).size(11.5).color(t.text)).truncate()); if let Some(cur) = widgets::blend_param(key, v) { if let Some(m) = widgets::blend_param_dropdown(ui, ("fx-blend", item, k, key.as_str()), cur) { change = Some((key.clone(), m)); @@ -815,26 +821,26 @@ fn swatch_picker(app: &mut VectorcraftApp, ui: &mut Ui, index: usize) { fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, node: Option<&Node>, sel: Sel) { let has = node.is_some(); widgets::bottom_bar(ui, |ui| { - if widgets::icon_button_enabled(ui, "dc-new-stroke", "Add New Stroke", false, has, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-new-stroke", tl!("Add New Stroke"), false, has, 24.0).clicked() { app.run("appearance.addStroke", json!({})).ok(); } - if widgets::icon_button_enabled(ui, "dc-new-fill", "Add New Fill", false, has, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-new-fill", tl!("Add New Fill"), false, has, 24.0).clicked() { app.run("appearance.addFill", json!({})).ok(); } - let fx = widgets::icon_button_enabled(ui, "dc-fx", "Add New Effect", false, has, 24.0); + let fx = widgets::icon_button_enabled(ui, "dc-fx", tl!("Add New Effect"), false, has, 24.0); egui::Popup::menu(&fx).show(|ui| { fx_menu(app, ui); }); ui.add_space((ui.available_width() - 3.0 * 28.0).max(0.0)); let basic = node.is_none_or(|n| n.appearance.is_basic()); - if widgets::icon_button_enabled(ui, "dc-clear", "Clear Appearance", false, has, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-clear", tl!("Clear Appearance"), false, has, 24.0).clicked() { app.run("appearance.clear", json!({})).ok(); } let can = has && sel.editable(); - if widgets::icon_button_enabled(ui, "dc-new-item", "Duplicate Selected Item", false, can, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-new-item", tl!("Duplicate Selected Item"), false, can, 24.0).clicked() { duplicate_selected(app, ui, node, sel); } - if widgets::icon_button_enabled(ui, "trash-2", "Delete Selected Item", false, can, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Selected Item"), false, can, 24.0).clicked() { delete_selected(app, ui, node, sel); } let _ = basic; @@ -876,10 +882,10 @@ pub(crate) fn fx_menu(app: &mut VectorcraftApp, ui: &mut Ui) { } } for (g, items) in groups { - ui.menu_button(g, |ui| { + ui.menu_button(tl!(&g), |ui| { crate::widgets::menu_scroll(ui, |ui| { for (id, label) in items { - if ui.button(format!("{label}…")).clicked() { + if ui.button(format!("{}…", tl!(&label))).clicked() { app.run("effect.dialog", json!({"effect": id})).ok(); ui.close(); } @@ -893,41 +899,43 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let node = first_subject(app); let has = node.is_some(); let sel = current_sel(app, ui.ctx(), node.as_ref()); - if menu_item(ui, "Add New Fill", has, false) { + if menu_item(ui, tl!("Add New Fill"), has, false) { app.run("appearance.addFill", json!({})).ok(); } - if menu_item(ui, "Add New Stroke", has, false) { + if menu_item(ui, tl!("Add New Stroke"), has, false) { app.run("appearance.addStroke", json!({})).ok(); } ui.separator(); - if menu_item(ui, "Duplicate Item", has && sel.editable(), false) { + if menu_item(ui, tl!("Duplicate Item"), has && sel.editable(), false) { duplicate_selected(app, ui, node.as_ref(), sel); } - if menu_item(ui, "Remove Item", has && sel.editable(), false) { + if menu_item(ui, tl!("Remove Item"), has && sel.editable(), false) { delete_selected(app, ui, node.as_ref(), sel); } - if menu_item(ui, "Clear Appearance", has, false) { + if menu_item(ui, tl!("Clear Appearance"), has, false) { app.run("appearance.clear", json!({})).ok(); } - if menu_item(ui, "Reduce to Basic Appearance", has && !node.as_ref().is_some_and(|n| n.appearance.is_basic()), false) { + if menu_item(ui, tl!("Reduce to Basic Appearance"), has && !node.as_ref().is_some_and(|n| n.appearance.is_basic()), false) { app.run("appearance.reduceToBasic", json!({})).ok(); } ui.separator(); let basic = app.session.prefs.new_art_basic; - if menu_item(ui, "New Art Has Basic Appearance", true, basic) { + if menu_item(ui, tl!("New Art Has Basic Appearance"), true, basic) { app.run("appearance.setNewArtBasic", json!({ "on": !basic })).ok(); } let hide: bool = pstate(ui.ctx(), "ap-hide-thumb"); - if menu_item(ui, if hide { "Show Thumbnail" } else { "Hide Thumbnail" }, true, false) { + if menu_item(ui, if hide { tl!("Show Thumbnail") } else { tl!("Hide Thumbnail") }, true, false) { set_pstate(ui.ctx(), "ap-hide-thumb", !hide); } ui.separator(); let style = app.session.selection_graphic_style().map(|(g, _)| g.name.clone()); - let redefine = style.as_ref().map_or_else(|| "Redefine Graphic Style".into(), |n| format!("Redefine Graphic Style \u{201c}{n}\u{201d}")); + let redefine = style + .as_ref() + .map_or_else(|| tl!("Redefine Graphic Style").into(), |n| crate::i18n::fmt(tl!("Redefine Graphic Style “{name}”"), &[("name", n)])); if menu_item(ui, &redefine, style.is_some(), false) { app.run("graphicStyle.redefine", json!({})).ok(); } - if menu_item(ui, "Show All Hidden Attributes", node.as_ref().is_some_and(|n| n.appearance.has_hidden()), false) { + if menu_item(ui, tl!("Show All Hidden Attributes"), node.as_ref().is_some_and(|n| n.appearance.has_hidden()), false) { app.run("appearance.showAllHidden", json!({})).ok(); } } diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index 1c5265c75..69dee3463 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -16,7 +16,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let abs: Vec = app.session.active().map(|d| d.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default(); if abs.is_empty() { - super::empty_state(ui, "dc-artboards", "No document", "Open a document to see its artboards."); + super::empty_state(ui, "dc-artboards", tl!("No document"), tl!("Open a document to see its artboards.")); return; } let sel = selected(ui, abs.len()); @@ -61,7 +61,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let opt = egui::Rect::from_center_size(r.right_center() - vec2(14.0, 0.0), vec2(14.0, 14.0)); let oresp = ui.interact(opt, ui.id().with(("ab-opt", i)), Sense::click()); icons::paint(ui, "dc-artboard-options", opt, if oresp.hovered() { t.text_strong } else { t.icon }); - if oresp.on_hover_text("Artboard Options: edit with the Artboard tool").clicked() { + if oresp.on_hover_text(tl!("Artboard Options: edit with the Artboard tool")).clicked() { set_pstate(ui.ctx(), "ab-sel", i); app.select_tool("artboard"); } @@ -76,22 +76,22 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); let n = abs.len(); widgets::bottom_bar(ui, |ui| { - widgets::icon_button_enabled(ui, "dc-rearrange", "Rearrange All Artboards (on the roadmap)", false, false, 24.0); + widgets::icon_button_enabled(ui, "dc-rearrange", tl!("Rearrange All Artboards (on the roadmap)"), false, false, 24.0); ui.add_space((ui.available_width() - 4.0 * 28.0).max(0.0)); - if widgets::icon_button_enabled(ui, "dc-arrow-up", "Move Up", false, sel > 0, 24.0).clicked() + if widgets::icon_button_enabled(ui, "dc-arrow-up", tl!("Move Up"), false, sel > 0, 24.0).clicked() && app.run("artboard.reorder", json!({"index": sel, "to": sel - 1})).is_ok() { set_pstate(ui.ctx(), "ab-sel", sel - 1); } - if widgets::icon_button_enabled(ui, "dc-arrow-down", "Move Down", false, sel + 1 < n, 24.0).clicked() + if widgets::icon_button_enabled(ui, "dc-arrow-down", tl!("Move Down"), false, sel + 1 < n, 24.0).clicked() && app.run("artboard.reorder", json!({"index": sel, "to": sel + 1})).is_ok() { set_pstate(ui.ctx(), "ab-sel", sel + 1); } - if widgets::icon_button(ui, "dc-new-item", "New Artboard", false, 24.0).clicked() && app.run("artboard.new", json!({})).is_ok() { + if widgets::icon_button(ui, "dc-new-item", tl!("New Artboard"), false, 24.0).clicked() && app.run("artboard.new", json!({})).is_ok() { set_pstate(ui.ctx(), "ab-sel", n); } - if widgets::icon_button_enabled(ui, "trash-2", "Delete Artboard", false, n > 1, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Artboard"), false, n > 1, 24.0).clicked() { app.run("artboard.delete", json!({"index": sel})).ok(); } }); @@ -100,27 +100,27 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let n = app.session.active().map(|d| d.doc.artboards.len()).unwrap_or(0); let sel = selected(ui, n); - if menu_item(ui, "New Artboard", n > 0, false) { + if menu_item(ui, tl!("New Artboard"), n > 0, false) { app.run("artboard.new", json!({})).ok(); } - if menu_item(ui, "Duplicate Artboards", n > 0, false) { + if menu_item(ui, tl!("Duplicate Artboards"), n > 0, false) { app.run("artboard.duplicate", json!({"index": sel})).ok(); } - if menu_item(ui, "Delete Artboards", n > 1, false) { + if menu_item(ui, tl!("Delete Artboards"), n > 1, false) { app.run("artboard.delete", json!({"index": sel})).ok(); } - if menu_item(ui, "Rename", n > 0, false) { + if menu_item(ui, tl!("Rename"), n > 0, false) { set_pstate(ui.ctx(), "ab-edit", Some(sel)); } - menu_item(ui, "Delete Empty Artboards", false, false); + menu_item(ui, tl!("Delete Empty Artboards"), false, false); ui.separator(); - menu_item(ui, "Convert to Artboards", false, false); - if menu_item(ui, "Artboard Options…", n > 0, false) { + menu_item(ui, tl!("Convert to Artboards"), false, false); + if menu_item(ui, tl!("Artboard Options…"), n > 0, false) { app.select_tool("artboard"); } - menu_item(ui, "Rearrange All Artboards…", false, false); + menu_item(ui, tl!("Rearrange All Artboards…"), false, false); ui.separator(); - if menu_item(ui, "Fit to Artwork Bounds", n > 0, false) { + if menu_item(ui, tl!("Fit to Artwork Bounds"), n > 0, false) { app.run("artboard.fitToArt", json!({"index": sel})).ok(); } } diff --git a/crates/ui-egui/src/panels/asset_export.rs b/crates/ui-egui/src/panels/asset_export.rs index 64700d849..dad16612f 100644 --- a/crates/ui-egui/src/panels/asset_export.rs +++ b/crates/ui-egui/src/panels/asset_export.rs @@ -146,7 +146,7 @@ fn tiles(ui: &mut Ui, st: &DocState, sel: &[u64], ev: &mut Events) { ); } } - let resp = resp.on_hover_text(format!("{}\nDouble-click to rename", a.name)); + let resp = resp.on_hover_text(format!("{}\n{}", a.name, tl!("Double-click to rename"))); if resp.double_clicked() { ui.data_mut(|d| d.insert_temp(rename_id, (a.id, a.name.clone()))); } else if resp.clicked() { @@ -251,7 +251,7 @@ fn settings(ui: &mut Ui, st: &DocState) -> (Option, usize) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let ctx = ui.ctx().clone(); let Some(st) = app.session.active() else { - widgets::dim_label(ui, "No document"); + widgets::dim_label(ui, tl!("No document")); return; }; let sel = selected(&ctx, st); @@ -261,7 +261,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.set_min_height(110.0); ui.set_width(ui.available_width()); if st.doc.assets.is_empty() { - super::empty_state(ui, "share-2", "No assets", "Drag art here, or select it and click +. Hold Alt for one asset of several objects."); + super::empty_state( + ui, + "share-2", + tl!("No assets"), + tl!("Drag art here, or select it and click +. Hold Alt for one asset of several objects."), + ); } else { tiles(ui, st, &sel, &mut ev); } @@ -270,7 +275,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { drop_zone(ui, &zone, &mut ev); }); ui.add_space(6.0); - widgets::section_header(ui, "Export Settings"); + widgets::section_header(ui, tl!("Export Settings")); let (change, files_each) = settings(ui, st); let all: Vec = st.doc.assets.iter().map(|a| a.id).collect(); let has_art = !st.selection.is_empty(); @@ -280,17 +285,21 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.add_enabled_ui(!all.is_empty(), |ui| { - let label = if sel.is_empty() || sel.len() == all.len() { "Export".to_string() } else { format!("Export {} Selected", sel.len()) }; - export_clicked = widgets::primary_button(ui, &label).on_hover_text("Export the selected assets (else all of them)").clicked(); + let label = if sel.is_empty() || sel.len() == all.len() { + tl!("Export").to_string() + } else { + crate::i18n::fmt(tl!("Export {count} Selected"), &[("count", &sel.len().to_string())]) + }; + export_clicked = widgets::primary_button(ui, &label).on_hover_text(tl!("Export the selected assets (else all of them)")).clicked(); }); }); }); let (mut screens, mut add, mut remove) = (false, false, false); widgets::bottom_bar(ui, |ui| { - screens = widgets::icon_button(ui, "monitor", "Export for Screens…", false, 24.0).clicked(); + screens = widgets::icon_button(ui, "monitor", tl!("Export for Screens…"), false, 24.0).clicked(); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - remove = widgets::icon_button_enabled(ui, "trash-2", "Remove Selected Assets", false, !sel.is_empty(), 24.0).clicked(); - add = widgets::icon_button_enabled(ui, "plus", "Add Selected Artwork (Alt: as one asset)", false, has_art, 24.0).clicked(); + remove = widgets::icon_button_enabled(ui, "trash-2", tl!("Remove Selected Assets"), false, !sel.is_empty(), 24.0).clicked(); + add = widgets::icon_button_enabled(ui, "plus", tl!("Add Selected Artwork (Alt: as one asset)"), false, has_art, 24.0).clicked(); }); }); // Act on what the panel saw (the document is no longer borrowed). @@ -345,22 +354,22 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(st) = app.session.active() else { return }; let (has_art, sel) = (!st.selection.is_empty(), selected(&ctx, st)); let all: Vec = st.doc.assets.iter().map(|a| a.id).collect(); - if menu_item(ui, "Add Selected Artwork as One Asset", has_art, false) { + if menu_item(ui, tl!("Add Selected Artwork as One Asset"), has_art, false) { collect(app, &ctx, None, true); } - if menu_item(ui, "Add Selected Artwork as Multiple Assets", has_art, false) { + if menu_item(ui, tl!("Add Selected Artwork as Multiple Assets"), has_art, false) { collect(app, &ctx, None, false); } ui.separator(); - if menu_item(ui, "Select All Assets", !all.is_empty(), false) { + if menu_item(ui, tl!("Select All Assets"), !all.is_empty(), false) { set_pstate(&ctx, SELECTED, all.clone()); } - if menu_item(ui, "Remove Selected Assets", !sel.is_empty(), false) { + if menu_item(ui, tl!("Remove Selected Assets"), !sel.is_empty(), false) { run(app, "assets.remove", json!({ "assets": sel })); set_pstate(&ctx, SELECTED, Vec::::new()); } ui.separator(); - if menu_item(ui, "Export for Screens…", true, false) { + if menu_item(ui, tl!("Export for Screens…"), true, false) { open_export_for_screens_assets(app, Some(&sel_or_all(&sel, &all))); } } diff --git a/crates/ui-egui/src/panels/attributes.rs b/crates/ui-egui/src/panels/attributes.rs index 2d93378b3..192397334 100644 --- a/crates/ui-egui/src/panels/attributes.rs +++ b/crates/ui-egui/src/panels/attributes.rs @@ -65,7 +65,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let has = !info.ids.is_empty(); ui.horizontal(|ui| { for (label, key, value) in - [("Overprint Fill", "overprintFill", info.overprint_fill), ("Overprint Stroke", "overprintStroke", info.overprint_stroke)] + [(tl!("Overprint Fill"), "overprintFill", info.overprint_fill), (tl!("Overprint Stroke"), "overprintStroke", info.overprint_stroke)] { if widgets::check3(ui, label, value, has) { set(app, json!({ key: value != Some(true) })); @@ -76,19 +76,19 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if !pstate::(ui.ctx(), HIDE_OPTIONS) { widgets::divider(ui); ui.horizontal(|ui| { - let center = [("dc-center-hide", "Don't Show Center", false), ("dc-center-show", "Show Center", true)]; + let center = [("dc-center-hide", tl!("Don't Show Center"), false), ("dc-center-show", tl!("Show Center"), true)]; if let Some(v) = pair(ui, has, info.show_center, center) { set(app, json!({ "showCenter": v })); } ui.add_space(10.0); - let dir = [("dc-dir-off", "Reverse Path Direction Off", false), ("dc-dir-on", "Reverse Path Direction On", true)]; + let dir = [("dc-dir-off", tl!("Reverse Path Direction Off"), false), ("dc-dir-on", tl!("Reverse Path Direction On"), true)]; if let Some(v) = pair(ui, paths, info.reversed, dir) { app.run("path.reverse", json!({ "reversed": v })).ok(); } ui.add_space(10.0); let rules = [ - ("dc-rule-nonzero", "Use Non-Zero Winding Fill Rule", FillRule::NonZero), - ("dc-rule-evenodd", "Use Even-Odd Fill Rule", FillRule::EvenOdd), + ("dc-rule-nonzero", tl!("Use Non-Zero Winding Fill Rule"), FillRule::NonZero), + ("dc-rule-evenodd", tl!("Use Even-Odd Fill Rule"), FillRule::EvenOdd), ]; if let Some(r) = pair(ui, paths, info.fill_rule, rules) { app.run("path.setFillRule", json!({ "rule": if r == FillRule::EvenOdd { "evenOdd" } else { "nonZero" } })).ok(); @@ -96,7 +96,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); ui.add_space(4.0); let url = info.url.clone().filter(|u| !u.is_empty()); - widgets::label_row(ui, "Image Map:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("Image Map:"), LABEL_W, |ui| { ui.add_enabled_ui(has, |ui| { let labels = ImageMap::ALL.map(ImageMap::label); if let Some(i) = widgets::dropdown(ui, "attr-map", info.image_map.map_or("", ImageMap::label), &labels, 100.0) { @@ -104,12 +104,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); ui.add_space(6.0); - let r = ui.add_enabled_ui(url.is_some(), |ui| widgets::flat_button(ui, "Browser", 64.0)).inner; - if r.on_hover_text("Open the URL in the web browser").clicked() { + let r = ui.add_enabled_ui(url.is_some(), |ui| widgets::flat_button(ui, tl!("Browser"), 64.0)).inner; + if r.on_hover_text(tl!("Open the URL in the web browser")).clicked() { app.run("attributes.openUrl", json!({})).ok(); } }); - widgets::label_row(ui, "URL:", LABEL_W, |ui| { + widgets::label_row(ui, tl!("URL:"), LABEL_W, |ui| { ui.add_enabled_ui(has, |ui| { let width = (ui.available_width() - 26.0).max(80.0); if let Some(u) = widgets::text_field(ui, "attr-url", info.url.as_deref(), width, 1) { @@ -121,7 +121,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } if !pstate::(ui.ctx(), HIDE_NOTE) { widgets::divider(ui); - widgets::subheader(ui, "Note:"); + widgets::subheader(ui, tl!("Note:")); ui.add_enabled_ui(has, |ui| { if let Some(n) = widgets::text_field(ui, "attr-note", info.note.as_deref(), ui.available_width(), 3) { set(app, json!({ "note": n })); @@ -129,13 +129,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); } if !has { - widgets::dim_label(ui, if selection_len(app) == 0 { "No Selection" } else { "" }); + widgets::dim_label(ui, if selection_len(app) == 0 { tl!("No Selection") } else { "" }); } } /// The recent URLs button: a menu of the URLs given lately; picking one sets it. fn recent_urls(app: &mut VectorcraftApp, ui: &mut Ui) { - let resp = widgets::icon_button_enabled(ui, "chevron-down", "Recent URLs", false, !app.session.recent_urls.is_empty(), 20.0); + let resp = widgets::icon_button_enabled(ui, "chevron-down", tl!("Recent URLs"), false, !app.session.recent_urls.is_empty(), 20.0); let mut chosen = None; egui::Popup::menu(&resp).show(|ui| { ui.set_min_width(220.0); @@ -151,7 +151,7 @@ fn recent_urls(app: &mut VectorcraftApp, ui: &mut Ui) { } pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { - for (key, hide, show) in [(HIDE_OPTIONS, "Hide Options", "Show Options"), (HIDE_NOTE, "Hide Note", "Show Note")] { + for (key, hide, show) in [(HIDE_OPTIONS, tl!("Hide Options"), tl!("Show Options")), (HIDE_NOTE, tl!("Hide Note"), tl!("Show Note"))] { let hidden: bool = pstate(ui.ctx(), key); if menu_item(ui, if hidden { show } else { hide }, true, false) { set_pstate(ui.ctx(), key, !hidden); diff --git a/crates/ui-egui/src/panels/brushes.rs b/crates/ui-egui/src/panels/brushes.rs index 66bc7da43..7976cff7c 100644 --- a/crates/ui-egui/src/panels/brushes.rs +++ b/crates/ui-egui/src/panels/brushes.rs @@ -63,7 +63,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.set_min_height(110.0); ui.set_width(ui.available_width()); if list.is_empty() { - super::empty_state(ui, "paintbrush", "No brushes", "Select art and use New Brush to make one."); + super::empty_state(ui, "paintbrush", tl!("No brushes"), tl!("Select art and use New Brush to make one.")); return; } for (ty, _) in KINDS { @@ -131,18 +131,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let target = sel_brush.clone().or(current.clone()); let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty()); widgets::bottom_bar(ui, |ui| { - widgets::icon_button_enabled(ui, "library", "Brush Libraries (on the roadmap)", false, false, 24.0); - if widgets::icon_button_enabled(ui, "dc-remove-brush", "Remove Brush Stroke", false, has_brush, 24.0).clicked() { + widgets::icon_button_enabled(ui, "library", tl!("Brush Libraries (on the roadmap)"), false, false, 24.0); + if widgets::icon_button_enabled(ui, "dc-remove-brush", tl!("Remove Brush Stroke"), false, has_brush, 24.0).clicked() { app.run("brush.remove", json!({})).ok(); } - if widgets::icon_button_enabled(ui, "dc-options", "Expand Brush Strokes", false, has_brush, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-options", tl!("Expand Brush Strokes"), false, has_brush, 24.0).clicked() { app.run("object.expandBrush", json!({})).ok(); } ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0)); - if widgets::icon_button_enabled(ui, "dc-new-item", "New Art Brush from Selection", false, has_sel, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-new-item", tl!("New Art Brush from Selection"), false, has_sel, 24.0).clicked() { app.run("brush.new", json!({"type": "art"})).ok(); } - if widgets::icon_button_enabled(ui, "trash-2", "Delete Brush", false, target.is_some(), 24.0).clicked() + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Brush"), false, target.is_some(), 24.0).clicked() && let Some(n) = &target { app.run("brush.delete", json!({"name": n})).ok(); @@ -155,39 +155,41 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let (_, current) = brushes(app); let target = sel.clone().or(current); let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty()); - for (label, ty) in [("New Calligraphic Brush", "calligraphic"), ("New Bristle Brush", "bristle")] { + for (label, ty) in [(tl!("New Calligraphic Brush"), "calligraphic"), (tl!("New Bristle Brush"), "bristle")] { if menu_item(ui, label, true, false) { app.run("brush.new", json!({"type": ty})).ok(); } } - for (label, ty) in - [("New Art Brush from Selection", "art"), ("New Scatter Brush from Selection", "scatter"), ("New Pattern Brush from Selection", "pattern")] - { + for (label, ty) in [ + (tl!("New Art Brush from Selection"), "art"), + (tl!("New Scatter Brush from Selection"), "scatter"), + (tl!("New Pattern Brush from Selection"), "pattern"), + ] { if menu_item(ui, label, has_sel, false) { app.run("brush.new", json!({"type": ty})).ok(); } } - if menu_item(ui, "Duplicate Brush", target.is_some(), false) + if menu_item(ui, tl!("Duplicate Brush"), target.is_some(), false) && let Some(n) = &target { app.run("brush.duplicate", json!({"name": n})).ok(); } - if menu_item(ui, "Delete Brush", target.is_some(), false) + if menu_item(ui, tl!("Delete Brush"), target.is_some(), false) && let Some(n) = &target { app.run("brush.delete", json!({"name": n})).ok(); } - if menu_item(ui, "Remove Brush Stroke", sel.is_some(), false) { + if menu_item(ui, tl!("Remove Brush Stroke"), sel.is_some(), false) { app.run("brush.remove", json!({})).ok(); } - if menu_item(ui, "Expand Brush Strokes", sel.is_some(), false) { + if menu_item(ui, tl!("Expand Brush Strokes"), sel.is_some(), false) { app.run("object.expandBrush", json!({})).ok(); } ui.separator(); let mut hidden: Vec = pstate(ui.ctx(), "br-hidden"); for (ty, label) in KINDS { let shown = !hidden.iter().any(|h| h == ty); - if menu_item(ui, &format!("Show {label} Brushes"), true, shown) { + if menu_item(ui, &crate::i18n::fmt(tl!("Show {kind} Brushes"), &[("kind", tl!(label))]), true, shown) { if shown { hidden.push(ty.to_string()); } else { @@ -198,10 +200,10 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.separator(); let list: bool = pstate(ui.ctx(), "br-list"); - if menu_item(ui, "Thumbnail View", true, !list) { + if menu_item(ui, tl!("Thumbnail View"), true, !list) { set_pstate(ui.ctx(), "br-list", false); } - if menu_item(ui, "List View", true, list) { + if menu_item(ui, tl!("List View"), true, list) { set_pstate(ui.ctx(), "br-list", true); } } diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index 829d3da22..8853f968f 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -230,14 +230,14 @@ pub(crate) fn route_type_input(app: &mut VectorcraftApp, ctx: &egui::Context) { fn cell(ui: &mut Ui, label: &str, tip: &str, add: impl FnOnce(&mut Ui)) { let t = Tokens::get(ui.ctx()); ui.horizontal(|ui| { - ui.add_sized(vec2(22.0, 24.0), egui::Label::new(egui::RichText::new(label).size(11.5).strong().color(t.text))).on_hover_text(tip); + ui.add_sized(vec2(22.0, 24.0), egui::Label::new(egui::RichText::new(label).size(11.5).strong().color(t.text))).on_hover_text(tl!(tip)); add(ui); }); } pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let Some((s, _)) = text_style(app) else { - super::empty_state(ui, "type", "No text selected", "Select a text object to edit its character attributes."); + super::empty_state(ui, "type", tl!("No text selected"), tl!("Select a text object to edit its character attributes.")); return; }; let w = ui.available_width(); @@ -253,47 +253,47 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let fw = ((w - 66.0) / 2.0).clamp(60.0, 100.0); let unit = app.session.type_unit(); egui::Grid::new("ch-grid").num_columns(2).spacing([6.0, 4.0]).show(ui, |ui| { - cell(ui, "T", "Font Size", |ui| { + cell(ui, "T", tl!("Font Size"), |ui| { if let Some(v) = widgets::spin_field(ui, "ch-size", Some(s.size), unit, fw, 1.0, 0.1, &SIZE_PRESETS) { style(app, json!({"size": v})); } }); - cell(ui, "A↕", "Leading", |ui| { + cell(ui, "A↕", tl!("Leading"), |ui| { let presets: Vec = SIZE_PRESETS.iter().map(|v| v * 1.2).collect(); if let Some(v) = widgets::spin_field(ui, "ch-lead", Some(s.effective_leading()), unit, fw, 1.0, 0.1, &presets) { style(app, json!({"leading": v})); } }); ui.end_row(); - cell(ui, "VA", "Kerning (0 = Auto)", |ui| { + cell(ui, "VA", tl!("Kerning (0 = Auto)"), |ui| { if let Some(v) = widgets::spin_plain(ui, "ch-kern", s.kerning.unwrap_or(0.0), "", 0, fw, 10.0, -1000.0, &TRACKING_PRESETS) { format(app, if v == 0.0 { json!({"kerning": "auto"}) } else { json!({"kerning": v}) }); } }); - cell(ui, "VA↔", "Tracking", |ui| { + cell(ui, "VA↔", tl!("Tracking"), |ui| { if let Some(v) = widgets::spin_plain(ui, "ch-track", s.tracking, "", 0, fw, 10.0, -1000.0, &TRACKING_PRESETS) { style(app, json!({"tracking": v})); } }); ui.end_row(); if !pstate::(ui.ctx(), "ch-hide-options") { - cell(ui, "IT", "Vertical Scale %", |ui| { + cell(ui, "IT", tl!("Vertical Scale %"), |ui| { if let Some(v) = widgets::spin_plain(ui, "ch-vs", s.v_scale, "%", 1, fw, 1.0, 1.0, &SCALE_PRESETS) { format(app, json!({"vScale": v})); } }); - cell(ui, "T↔", "Horizontal Scale %", |ui| { + cell(ui, "T↔", tl!("Horizontal Scale %"), |ui| { if let Some(v) = widgets::spin_plain(ui, "ch-hs", s.h_scale, "%", 1, fw, 1.0, 1.0, &SCALE_PRESETS) { format(app, json!({"hScale": v})); } }); ui.end_row(); - cell(ui, "Aª", "Baseline Shift", |ui| { + cell(ui, "Aª", tl!("Baseline Shift"), |ui| { if let Some(v) = widgets::spin_field(ui, "ch-bs", Some(s.baseline_shift), unit, fw, 1.0, -1296.0, &[]) { format(app, json!({"baselineShift": v})); } }); - cell(ui, "⟲T", "Character Rotation", |ui| { + cell(ui, "⟲T", tl!("Character Rotation"), |ui| { if let Some(v) = widgets::spin_plain(ui, "ch-rot", s.rotation, "°", 1, fw, 15.0, -360.0, &super::transform::ANGLE_PRESETS) { format(app, json!({"rotation": v})); } @@ -307,12 +307,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(4.0); ui.horizontal(|ui| { for (kind, tip, key, on, enabled) in [ - (Glyph::AllCaps, "All Caps", "allCaps", s.all_caps, true), - (Glyph::SmallCaps, "Small Caps (on the roadmap)", "", false, false), - (Glyph::Super, "Superscript (on the roadmap)", "", false, false), - (Glyph::Sub, "Subscript (on the roadmap)", "", false, false), - (Glyph::Underline, "Underline", "underline", s.underline, true), - (Glyph::Strike, "Strikethrough", "strikethrough", s.strikethrough, true), + (Glyph::AllCaps, tl!("All Caps"), "allCaps", s.all_caps, true), + (Glyph::SmallCaps, tl!("Small Caps (on the roadmap)"), "", false, false), + (Glyph::Super, tl!("Superscript (on the roadmap)"), "", false, false), + (Glyph::Sub, tl!("Subscript (on the roadmap)"), "", false, false), + (Glyph::Underline, tl!("Underline"), "underline", s.underline, true), + (Glyph::Strike, tl!("Strikethrough"), "strikethrough", s.strikethrough, true), ] { if style_toggle(ui, kind, tip, on, enabled) && !key.is_empty() { format(app, json!({key: !on})); @@ -371,7 +371,7 @@ fn style_toggle(ui: &mut Ui, g: Glyph, tip: &str, on: bool, enabled: bool) -> bo p.line_segment([c + vec2(-6.0, 1.0), c + vec2(6.0, 1.0)], egui::Stroke::new(1.2, col)); } } - let resp = resp.on_hover_text(tip); + let resp = resp.on_hover_text(tl!(tip)); enabled && resp.clicked() } @@ -379,36 +379,36 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let st = text_style(app); let has = st.is_some(); let hidden: bool = pstate(ui.ctx(), "ch-hide-options"); - if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), "ch-hide-options", !hidden); } ui.separator(); let s = st.map(|x| x.0); for (label, key, on) in [ - ("All Caps", "allCaps", s.as_ref().is_some_and(|s| s.all_caps)), - ("Underline", "underline", s.as_ref().is_some_and(|s| s.underline)), - ("Strikethrough", "strikethrough", s.as_ref().is_some_and(|s| s.strikethrough)), + (tl!("All Caps"), "allCaps", s.as_ref().is_some_and(|s| s.all_caps)), + (tl!("Underline"), "underline", s.as_ref().is_some_and(|s| s.underline)), + (tl!("Strikethrough"), "strikethrough", s.as_ref().is_some_and(|s| s.strikethrough)), ] { if menu_item(ui, label, has, on) { format(app, json!({key: !on})); } } for l in ["Small Caps", "Superscript", "Subscript"] { - menu_item(ui, l, false, false); + menu_item(ui, tl!(l), false, false); } ui.separator(); for l in ["Standard Vertical Roman Alignment", "Tate-chu-yoko", "Fractional Widths", "System Layout", "No Break"] { - menu_item(ui, l, false, l == "Fractional Widths"); + menu_item(ui, tl!(l), false, l == "Fractional Widths"); } ui.separator(); // The installed fonts are always listed; this picks up fonts installed since the app started. #[cfg(not(target_arch = "wasm32"))] - if menu_item(ui, "Refresh Font List", true, false) + if menu_item(ui, tl!("Refresh Font List"), true, false) && let Ok(r) = app.run("text.rescanFonts", json!({})) { app.status(format!("{} font families available", r["families"])); } - if menu_item(ui, "Reset Panel", has, false) { + if menu_item(ui, tl!("Reset Panel"), has, false) { style(app, json!({"tracking": 0, "leading": "auto"})); format( app, diff --git a/crates/ui-egui/src/panels/color.rs b/crates/ui-egui/src/panels/color.rs index ebfdfe5d2..e1a130271 100644 --- a/crates/ui-egui/src/panels/color.rs +++ b/crates/ui-egui/src/panels/color.rs @@ -209,10 +209,14 @@ pub(crate) fn warning_chip(ui: &mut Ui, (icon, warning): (&str, &str), fix: &Col let t = Tokens::get(ui.ctx()); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 3.0; - icons::icon(ui, icon, 16.0, t.icon).on_hover_text(warning); + icons::icon(ui, icon, 16.0, t.icon).on_hover_text(tl!(warning)); let (r, resp) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::click()); widgets::swatch_tile(ui, r, &Paint::solid(*fix), false, resp.hovered()); - resp.on_hover_text(format!("{warning}: click to correct to the closest color ({})", fix.to_hex())).clicked() + resp.on_hover_text(crate::i18n::fmt( + tl!("{warning}: click to correct to the closest color ({hex})"), + &[("warning", tl!(warning)), ("hex", &fix.to_hex())], + )) + .clicked() }) .inner } @@ -572,7 +576,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // None / Black / White chips and the hex field. ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; - for (p, tip) in [(Paint::None, "None"), (Paint::solid(Color::BLACK), "Black"), (Paint::solid(Color::WHITE), "White")] { + for (p, tip) in [(Paint::None, tl!("None")), (Paint::solid(Color::BLACK), tl!("Black")), (Paint::solid(Color::WHITE), tl!("White"))] { let (r, resp) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::click()); widgets::swatch_tile(ui, r, &p, false, resp.hovered()); if resp.on_hover_text(tip).clicked() { @@ -612,7 +616,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "color-hide-options"); - if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), "color-hide-options", !hidden); } ui.separator(); @@ -621,7 +625,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { // Tint mode checks no colour mode: picking one makes the tint a process colour. let cur = display_mode(pstate(ui.ctx(), "color-mode"), color.as_ref()); for m in Mode::ALL { - if menu_item(ui, m.label(), true, m == cur && tgt.tint().is_none()) { + if menu_item(ui, tl!(m.label()), true, m == cur && tgt.tint().is_none()) { set_mode(app, ui.ctx(), &tgt, m); } } @@ -630,8 +634,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { // its colour model); a gradient stop is recoloured in place. let recolor = color.is_some() || super::active_mixed(app, ui.ctx()); for (label, cmd, f) in [ - ("Invert", "paint.invert", Color::invert_keep_model as fn(&Color) -> Color), - ("Complement", "paint.complement", Color::complement_keep_model), + (tl!("Invert"), "paint.invert", Color::invert_keep_model as fn(&Color) -> Color), + (tl!("Complement"), "paint.complement", Color::complement_keep_model), ] { if menu_item(ui, label, recolor, false) { match (&tgt, color) { @@ -643,7 +647,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { } } ui.separator(); - if menu_item(ui, "Create New Swatch…", color.is_some(), false) + if menu_item(ui, tl!("Create New Swatch…"), color.is_some(), false) && let Some(c) = color { // A tint saves a tint swatch ("Name 40%"). @@ -653,7 +657,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { }; app.run("swatch.new", p).ok(); } - if menu_item(ui, "Copy Color Value (Hex)", color.is_some(), false) + if menu_item(ui, tl!("Copy Color Value (Hex)"), color.is_some(), false) && let Some(c) = color { ui.ctx().copy_text(c.to_hex()); diff --git a/crates/ui-egui/src/panels/color_guide.rs b/crates/ui-egui/src/panels/color_guide.rs index 449c403ad..89e4f5a18 100644 --- a/crates/ui-egui/src/panels/color_guide.rs +++ b/crates/ui-egui/src/panels/color_guide.rs @@ -97,7 +97,7 @@ fn limit_menu(app: &mut VectorcraftApp, ui: &mut Ui) { ui.set_min_width(200.0); let cur = app.ui.color_guide_limit.clone(); let mut chosen = None; - for (key, label) in [("", "None"), (swatchlib::DOCUMENT_SWATCHES, swatches::DOCUMENT_SWATCHES)] { + for (key, label) in [("", tl!("None")), (swatchlib::DOCUMENT_SWATCHES, swatches::DOCUMENT_SWATCHES)] { if menu_item(ui, label, true, cur == key) { chosen = Some(key.to_string()); } @@ -139,7 +139,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.horizontal(|ui| { let (r, resp) = ui.allocate_exact_size(vec2(26.0, 26.0), Sense::click()); widgets::swatch_tile(ui, r, &Paint::solid(base), false, resp.hovered()); - if resp.on_hover_text("Set base color to the current color").clicked() + if resp.on_hover_text(tl!("Set base color to the current color")).clicked() && let Some(c) = active_paint(app).color() { set_pstate(&ctx, "cg-base", Some(c)); @@ -211,8 +211,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); widgets::bottom_bar(ui, |ui| { let tip = match &limit { - Some(name) => format!("Limit colors to swatch library: {name}"), - None => "Limit colors to swatch library".into(), + Some(name) => crate::i18n::fmt(tl!("Limit colors to swatch library: {name}"), &[("name", name)]), + None => tl!("Limit colors to swatch library").into(), }; let lr = widgets::icon_button(ui, "library", &tip, limit.is_some(), 24.0); egui::Popup::menu(&lr).show(|ui| limit_menu(app, ui)); @@ -220,13 +220,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // Recolor Artwork with the harmony colours as the new colours: the selected art's, or // (without art) the colours themselves, which OK saves as a colour group. let doc = app.session.active().is_some(); - if widgets::icon_button_enabled(ui, "palette", "Edit or Apply Colors", false, doc, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "palette", tl!("Edit or Apply Colors"), false, doc, 24.0).clicked() { edit_or_apply(app, &g); } - if widgets::icon_button_enabled(ui, "dc-new-item", "Save selected colors as swatches", false, !sel.is_empty(), 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-new-item", tl!("Save selected colors as swatches"), false, !sel.is_empty(), 24.0).clicked() { save_selected(app, &ctx); } - if widgets::icon_button(ui, "dc-folder", "Save color group to Swatch panel", false, 24.0).clicked() { + if widgets::icon_button(ui, "dc-folder", tl!("Save color group to Swatch panel"), false, 24.0).clicked() { let cs: Vec<_> = g.colors.iter().map(color_json).collect(); app.run("swatch.newGroup", json!({"name": h.label(), "colors": cs})).ok(); } @@ -238,19 +238,21 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let mode = app.ui.color_guide.variation; - for (m, l) in - [(Variation::TintsShades, "Show Tints/Shades"), (Variation::WarmCool, "Show Warm/Cool"), (Variation::VividMuted, "Show Vivid/Muted")] - { + for (m, l) in [ + (Variation::TintsShades, tl!("Show Tints/Shades")), + (Variation::WarmCool, tl!("Show Warm/Cool")), + (Variation::VividMuted, tl!("Show Vivid/Muted")), + ] { if menu_item(ui, l, true, m == mode) { app.ui.color_guide.variation = m; } } ui.separator(); let ctx = ui.ctx().clone(); - if menu_item(ui, "Save Colors as Swatches", !selected(&ctx).is_empty(), false) { + if menu_item(ui, tl!("Save Colors as Swatches"), !selected(&ctx).is_empty(), false) { save_selected(app, &ctx); } - if menu_item(ui, "Color Guide Options…", true, false) { + if menu_item(ui, tl!("Color Guide Options…"), true, false) { app.run("ui.colorGuideOptions", json!({})).ok(); } } diff --git a/crates/ui-egui/src/panels/color_themes.rs b/crates/ui-egui/src/panels/color_themes.rs index 0cde331a8..29caf04fc 100644 --- a/crates/ui-egui/src/panels/color_themes.rs +++ b/crates/ui-egui/src/panels/color_themes.rs @@ -130,7 +130,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let ctx = ui.ctx().clone(); ui.horizontal(|ui| { let cur = tab(&ctx); - for (i, label) in ["Create", "My Themes"].into_iter().enumerate() { + for (i, label) in [tl!("Create"), tl!("My Themes")].into_iter().enumerate() { if ui.selectable_label(cur == i, egui::RichText::new(label).font(theme::semibold(12.5))).clicked() { set_pstate(&ctx, "ct-tab", i); } @@ -147,7 +147,7 @@ fn create(app: &mut VectorcraftApp, ui: &mut Ui) { let mut d = draft(app, &ctx); let before = d.clone(); ui.horizontal(|ui| { - ui.label(egui::RichText::new("Rule:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Rule:")).color(t.text_dim)); let labels: Vec<&str> = std::iter::once("Custom").chain(Harmony::ALL.iter().map(|h| h.label())).collect(); let cur = d.rule.map_or("Custom", Harmony::label); // A rule makes the theme from the selected colour; Custom frees the colours. @@ -190,7 +190,7 @@ fn create(app: &mut VectorcraftApp, ui: &mut Ui) { d.set_selected(c); } let current = active_paint(app).color(); - if widgets::icon_button_enabled(ui, "pipette", "Set the selected color to the current color", false, current.is_some(), 24.0).clicked() + if widgets::icon_button_enabled(ui, "pipette", tl!("Set the selected color to the current color"), false, current.is_some(), 24.0).clicked() && let Some(c) = current { d.set_selected(c); @@ -198,7 +198,7 @@ fn create(app: &mut VectorcraftApp, ui: &mut Ui) { }); ui.add_space(4.0); ui.horizontal(|ui| { - ui.label(egui::RichText::new("Name:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Name:")).color(t.text_dim)); if let Some(n) = widgets::text_field(ui, "ct-name", Some(&d.name), ui.available_width() - 4.0, 1) { d.name = n; } @@ -208,18 +208,18 @@ fn create(app: &mut VectorcraftApp, ui: &mut Ui) { } widgets::bottom_bar(ui, |ui| { let tip = match &d.editing { - Some(n) => format!("Save changes to {n}"), - None => "Save theme to My Themes".into(), + Some(n) => crate::i18n::fmt(tl!("Save changes to {name}"), &[("name", n)]), + None => tl!("Save theme to My Themes").into(), }; if widgets::icon_button(ui, "save", &tip, false, 24.0).clicked() { save(app, &ctx); } let doc = app.session.active().is_some(); - if widgets::icon_button_enabled(ui, "dc-folder", "Add to Swatches", false, doc, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-folder", tl!("Add to Swatches"), false, doc, 24.0).clicked() { add_draft_to_swatches(app, &ctx); } ui.add_space((ui.available_width() - 28.0).max(0.0)); - if widgets::icon_button(ui, "file-plus", "New theme from the current color", false, 24.0).clicked() { + if widgets::icon_button(ui, "file-plus", tl!("New theme from the current color"), false, 24.0).clicked() { let base = active_paint(app).color().unwrap_or(DEFAULT_BASE); set_draft(&ctx, Draft::new(base, d.rule.or(Some(DEFAULT_RULE)))); } @@ -238,7 +238,7 @@ fn my_themes(app: &mut VectorcraftApp, ui: &mut Ui) { let pick = picked(app, &ctx); let mut action = None; if app.session.prefs.color_themes.is_empty() { - empty_state(ui, "sun", "No saved themes", "Make a theme on Create and save it."); + empty_state(ui, "sun", tl!("No saved themes"), tl!("Make a theme on Create and save it.")); } else { widgets::list_box(ui, |ui| { egui::ScrollArea::vertical().id_salt("ct-list").max_height(300.0).auto_shrink([false, true]).show(ui, |ui| { @@ -261,7 +261,7 @@ fn my_themes(app: &mut VectorcraftApp, ui: &mut Ui) { ui.painter().rect_stroke(strip, 0.0, egui::Stroke::new(1.0, t.border), egui::StrokeKind::Outside); if resp.double_clicked() { action = Some(Action::Edit(th.name.clone())); - } else if resp.on_hover_text("Click to select, double-click to edit").clicked() { + } else if resp.on_hover_text(tl!("Click to select, double-click to edit")).clicked() { action = Some(Action::Pick(th.name.clone())); } } @@ -276,19 +276,19 @@ fn my_themes(app: &mut VectorcraftApp, ui: &mut Ui) { let pick = picked(app, &ctx); widgets::bottom_bar(ui, |ui| { let some = pick.is_some(); - if widgets::icon_button_enabled(ui, "pencil", "Edit theme", false, some, 24.0).clicked() + if widgets::icon_button_enabled(ui, "pencil", tl!("Edit theme"), false, some, 24.0).clicked() && let Some(n) = &pick { edit_saved(app, &ctx, n); } let doc = app.session.active().is_some(); - if widgets::icon_button_enabled(ui, "dc-folder", "Add to Swatches", false, some && doc, 24.0).clicked() + if widgets::icon_button_enabled(ui, "dc-folder", tl!("Add to Swatches"), false, some && doc, 24.0).clicked() && let Some(n) = &pick { app.run("colorTheme.addToSwatches", json!({ "name": n })).ok(); } ui.add_space((ui.available_width() - 28.0).max(0.0)); - if widgets::icon_button_enabled(ui, "trash-2", "Delete theme", false, some, 24.0).clicked() + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete theme"), false, some, 24.0).clicked() && let Some(n) = &pick { delete(app, &ctx, n); @@ -319,26 +319,26 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let doc = app.session.active().is_some(); if tab(&ctx) == 1 { let pick = picked(app, &ctx); - if menu_item(ui, "Edit Theme", pick.is_some(), false) + if menu_item(ui, tl!("Edit Theme"), pick.is_some(), false) && let Some(n) = &pick { edit_saved(app, &ctx, n); } - if menu_item(ui, "Add to Swatches", pick.is_some() && doc, false) + if menu_item(ui, tl!("Add to Swatches"), pick.is_some() && doc, false) && let Some(n) = &pick { app.run("colorTheme.addToSwatches", json!({ "name": n })).ok(); } - if menu_item(ui, "Delete Theme", pick.is_some(), false) + if menu_item(ui, tl!("Delete Theme"), pick.is_some(), false) && let Some(n) = &pick { delete(app, &ctx, n); } } else { - if menu_item(ui, "Save Theme", true, false) { + if menu_item(ui, tl!("Save Theme"), true, false) { save(app, &ctx); } - if menu_item(ui, "Add to Swatches", doc, false) { + if menu_item(ui, tl!("Add to Swatches"), doc, false) { add_draft_to_swatches(app, &ctx); } } diff --git a/crates/ui-egui/src/panels/css_properties.rs b/crates/ui-egui/src/panels/css_properties.rs index 274be815c..e6cd65818 100644 --- a/crates/ui-egui/src/panels/css_properties.rs +++ b/crates/ui-egui/src/panels/css_properties.rs @@ -105,7 +105,7 @@ fn sentence(w: &str) -> String { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let ctx = ui.ctx().clone(); let Some(selected) = app.session.active().map(|st| !st.selection.is_empty()) else { - widgets::dim_label(ui, "No document"); + widgets::dim_label(ui, tl!("No document")); return; }; let opts: CssOptions = pstate(&ctx, OPTIONS); @@ -118,18 +118,25 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { Some(job) => { ui.add(egui::Label::new(egui::WidgetText::LayoutJob(job.clone())).selectable(true).extend()); } - None if selected => super::empty_state(ui, "globe", "No CSS", "The selected objects have no CSS of their own (see below)."), - None => super::empty_state(ui, "globe", "No selection", "Select objects to see their CSS, or Generate CSS for the whole document."), + None if selected => super::empty_state(ui, "globe", tl!("No CSS"), tl!("The selected objects have no CSS of their own (see below).")), + None => super::empty_state( + ui, + "globe", + tl!("No selection"), + tl!("Select objects to see their CSS, or Generate CSS for the whole document."), + ), } }); }); let notes = (shown.more > 0) - .then(|| format!("{} more rules: Export writes every one.", shown.more)) + .then(|| crate::i18n::fmt(tl!("{count} more rules: Export writes every one."), &[("count", &shown.more.to_string())])) .into_iter() - .chain( - (shown.skipped > 0) - .then(|| format!("{} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu.", shown.skipped)), - ) + .chain((shown.skipped > 0).then(|| { + crate::i18n::fmt( + tl!("{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu."), + &[("count", &shown.skipped.to_string())], + ) + })) .chain(shown.warnings.iter().map(|w| sentence(w))); let dim = Tokens::get(&ctx).text_dim; for n in notes { @@ -139,13 +146,19 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let scope = if all { "all" } else { "selection" }; let (mut generate, mut copy, mut export) = (false, false, false); widgets::bottom_bar(ui, |ui| { - generate = - widgets::icon_button_enabled(ui, "sparkles", "Generate CSS: the whole document's CSS while nothing is selected", all, !selected, 24.0) - .clicked(); + generate = widgets::icon_button_enabled( + ui, + "sparkles", + tl!("Generate CSS: the whole document's CSS while nothing is selected"), + all, + !selected, + 24.0, + ) + .clicked(); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let tip = if all { "Export All…" } else { "Export Selected CSS…" }; + let tip = if all { tl!("Export All…") } else { tl!("Export Selected CSS…") }; export = widgets::icon_button_enabled(ui, "save", tip, false, has_css, 24.0).clicked(); - let tip = if all { "Copy All Styles" } else { "Copy Selected Style" }; + let tip = if all { tl!("Copy All Styles") } else { tl!("Copy Selected Style") }; copy = widgets::icon_button_enabled(ui, "copy", tip, false, has_css, 24.0).clicked(); }); }); @@ -199,22 +212,22 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(selected) = app.session.active().map(|st| !st.selection.is_empty()) else { return }; let mut opts: CssOptions = pstate(&ctx, OPTIONS); let all: bool = pstate(&ctx, ALL); - if menu_item(ui, "Copy Selected Style", selected, false) { + if menu_item(ui, tl!("Copy Selected Style"), selected, false) { crate::menus::invoke(app, "css.copy", params(&opts, "selection")); } - if menu_item(ui, "Export Selected CSS…", selected, false) { + if menu_item(ui, tl!("Export Selected CSS…"), selected, false) { crate::menus::invoke(app, "css.exportFile", params(&opts, "selection")); } - if menu_item(ui, "Export All…", true, false) { + if menu_item(ui, tl!("Export All…"), true, false) { crate::menus::invoke(app, "css.exportFile", params(&opts, "all")); } ui.separator(); - if menu_item(ui, "Generate CSS", true, all) { + if menu_item(ui, tl!("Generate CSS"), true, all) { set_pstate(&ctx, ALL, !all); } ui.separator(); let before = opts.clone(); - ui.menu_button(format!(" Units: {}", opts.units.name()), |ui| { + ui.menu_button(format!(" {}", crate::i18n::fmt(tl!("Units: {units}"), &[("units", opts.units.name())])), |ui| { for u in CssUnits::ALL { if menu_item(ui, u.name(), true, opts.units == u) { opts.units = u; @@ -222,10 +235,10 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { } }); for (label, on) in [ - ("Absolute Position", &mut opts.position), - ("Width and Height", &mut opts.dimensions), - ("Unnamed Objects", &mut opts.unnamed), - ("Rasterize Unsupported Art", &mut opts.rasterize), + (tl!("Absolute Position"), &mut opts.position), + (tl!("Width and Height"), &mut opts.dimensions), + (tl!("Unnamed Objects"), &mut opts.unnamed), + (tl!("Rasterize Unsupported Art"), &mut opts.rasterize), ] { if menu_item(ui, label, true, *on) { *on = !*on; diff --git a/crates/ui-egui/src/panels/doc_info.rs b/crates/ui-egui/src/panels/doc_info.rs index 7352868b5..9559ae008 100644 --- a/crates/ui-egui/src/panels/doc_info.rs +++ b/crates/ui-egui/src/panels/doc_info.rs @@ -21,7 +21,7 @@ struct Cache { pub(crate) fn row(ui: &mut Ui, label: &str, value: String) { let t = Tokens::get(ui.ctx()); ui.horizontal(|ui| { - ui.label(egui::RichText::new(label).size(12.0).color(t.text_dim)); + ui.label(egui::RichText::new(tl!(label)).size(12.0).color(t.text_dim)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Long values (paths) are cut to the panel's width; hovering shows them whole. ui.add(egui::Label::new(egui::RichText::new(value).size(12.0).color(t.text)).truncate()); @@ -46,7 +46,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let sel_only: bool = pstate(ui.ctx(), "docinfo-sel"); let category: String = pstate(ui.ctx(), "docinfo-category"); let Some(i) = info(app, ui.ctx(), sel_only) else { - widgets::dim_label(ui, "No document"); + widgets::dim_label(ui, tl!("No document")); return; }; egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| { @@ -60,12 +60,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let rows = s["rows"].as_array().map_or(&[][..], Vec::as_slice); // Lists show how many they hold; the selection scope shows on the first. let title = match s["id"].as_str() { - Some("document" | "objects") => title.to_string(), - _ => format!("{title} ({})", rows.len()), + Some("document" | "objects") => tl!(title).to_string(), + _ => format!("{} ({})", tl!(title), rows.len()), }; - widgets::subheader(ui, &if sel_only && n == 0 && category.is_empty() { format!("{title} (selection)") } else { title }); + widgets::subheader( + ui, + &if sel_only && n == 0 && category.is_empty() { crate::i18n::fmt(tl!("{title} (selection)"), &[("title", &title)]) } else { title }, + ); if rows.is_empty() { - widgets::dim_label(ui, "None"); + widgets::dim_label(ui, tl!("None")); } for r in rows { let (label, value) = (r[0].as_str().unwrap_or_default(), r[1].as_str().unwrap_or_default()); @@ -95,12 +98,12 @@ pub(crate) fn save_report(app: &mut VectorcraftApp, p: &Value) -> Result Value { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { - super::empty_state(ui, "eye", "No document", "Open a document to preview its flattening."); + super::empty_state(ui, "eye", tl!("No document"), tl!("Open a document to preview its flattening.")); return; }; let (index, revision) = (app.session.active_index().unwrap_or(0), st.revision); @@ -200,7 +200,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let mut set = app.ui.flattener_preview.clone(); let mut refresh_now = false; ui.horizontal(|ui| { - if widgets::flat_button(ui, "Refresh", 64.0).on_hover_text("Preview the document as it is now").clicked() { + if widgets::flat_button(ui, tl!("Refresh"), 64.0).on_hover_text(tl!("Preview the document as it is now")).clicked() { refresh_now = true; } let labels = Highlight::ALL.map(Highlight::label); @@ -210,13 +210,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); ui.add_space(4.0); egui::Grid::new("fp-settings").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { - widgets::dim_label(ui, "Overprints:"); + widgets::dim_label(ui, tl!("Overprints:")); let labels = Overprints::ALL.map(Overprints::label); if let Some(i) = widgets::dropdown(ui, "fp-overprints", set.overprints.label(), &labels, 150.0) { set.overprints = Overprints::ALL[i]; } ui.end_row(); - widgets::dim_label(ui, "Preset:"); + widgets::dim_label(ui, tl!("Preset:")); let presets = app.session.flattener_presets(); if let Some(p) = preset_dropdown(ui, "fp-preset", &presets, &set.preset, Some(&set.options), 150.0) { set.preset = p.name.clone(); @@ -238,11 +238,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { (count, s.is_some_and(|s| s.revision != revision || s.options != set.options || s.overprints != set.overprints)) }); if set.highlight != Highlight::None { - let what = if matches!(set.highlight, Highlight::RasterizedRegions | Highlight::AllRasterized) { "area" } else { "object" }; - widgets::dim_label(ui, &format!("{count} {what}{} highlighted", if count == 1 { "" } else { "s" })); + let areas = matches!(set.highlight, Highlight::RasterizedRegions | Highlight::AllRasterized); + let n = count as u64; + let msg = if areas { + crate::i18n::tn(n, "{n} area highlighted", "{n} areas highlighted") + } else { + crate::i18n::tn(n, "{n} object highlighted", "{n} objects highlighted") + }; + widgets::dim_label(ui, &msg); } if stale { - ui.label(egui::RichText::new("Changed since the last Refresh.").color(t.text_dim).size(11.5)); + ui.label(egui::RichText::new(tl!("Changed since the last Refresh.")).color(t.text_dim).size(11.5)); } if set != app.ui.flattener_preview { app.ui.flattener_preview = set; @@ -257,7 +263,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { fn preview_area(ui: &mut Ui, t: &Tokens, h: Highlight, overprints: Overprints) { let w = ui.available_width(); let (rect, resp) = ui.allocate_exact_size(vec2(w, (w * 0.8).clamp(140.0, 320.0)), Sense::click_and_drag()); - let resp = resp.on_hover_text("Click to zoom in, Alt-click to zoom out, drag to pan, double-click to fit"); + let resp = resp.on_hover_text(tl!("Click to zoom in, Alt-click to zoom out, drag to pan, double-click to fit")); ui.painter().rect_filled(rect, 0.0, t.pasteboard); let ppp = ui.ctx().pixels_per_point(); CACHE.with(|c| { @@ -339,19 +345,19 @@ fn render( } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { - if widgets::menu_item(ui, "Refresh", app.session.active().is_some(), false) + if widgets::menu_item(ui, tl!("Refresh"), app.session.active().is_some(), false) && let Err(e) = refresh(app) { app.status(e); } let shown = app.ui.flattener_preview.show_options; - if widgets::menu_item(ui, "Show Options", true, shown) { + if widgets::menu_item(ui, tl!("Show Options"), true, shown) { app.ui.flattener_preview.show_options = !shown; } - if widgets::menu_item(ui, "Fit in Window", true, false) { + if widgets::menu_item(ui, tl!("Fit in Window"), true, false) { CACHE.with(|c| c.borrow_mut().view = View::default()); } - if widgets::menu_item(ui, "Save Transparency Flattener Preset…", true, false) { + if widgets::menu_item(ui, tl!("Save Transparency Flattener Preset…"), true, false) { // Saved under a new name, then shown in the presets manager to name it. match app.run("flattener.presets.save", json!({ "options": app.ui.flattener_preview.options })) { Ok(r) => { diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index 9cbbca69f..81fc527c4 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -142,7 +142,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let picked: Option<(String, String)> = pstate(ui.ctx(), "gl-font"); let (family, style) = picked.or(current).unwrap_or_else(|| (vectorcraft_text::FALLBACK_FAMILY.to_string(), "Regular".to_string())); let Some(face) = db.face(&family, &style) else { - ui.label(egui::RichText::new("No fonts are available.").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("No fonts are available.")).color(t.text_dim)); return; }; let subset: usize = pstate(ui.ctx(), "gl-subset"); @@ -150,7 +150,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let w = ui.available_width(); let names: Vec<&str> = SUBSETS.iter().map(|s| s.0).collect(); ui.horizontal(|ui| { - ui.label(egui::RichText::new("Show:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Show:")).color(t.text_dim)); if let Some(i) = widgets::dropdown(ui, "gl-subset", names[subset.min(names.len() - 1)], &names, (w - 50.0).max(80.0)) { set_pstate(ui.ctx(), "gl-subset", i); } @@ -175,7 +175,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.painter().rect_stroke(rect.shrink(0.5), 0, egui::Stroke::new(0.5, t.border), egui::StrokeKind::Inside); let tex = glyph_texture(ui.ctx(), &face, gid, px); ui.painter().image(tex.id(), rect, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), t.text_strong); - let resp = resp.on_hover_text(format!("U+{:04X} {c} (glyph {gid})\nDouble-click to insert", c as u32)); + let resp = resp.on_hover_text(format!("U+{:04X} {c} (glyph {gid})\n{}", c as u32, tl!("Double-click to insert"))); if resp.double_clicked() { insert(app, c); } @@ -187,8 +187,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let info = ui.ctx().data(|d| d.get_temp::<(char, u32)>(hover_id)); ui.label( egui::RichText::new(match info { - Some((c, g)) => format!("U+{:04X} glyph {g} · {} glyphs", c as u32, chars.len()), - None => format!("{} glyphs", chars.len()), + Some((c, g)) => format!( + "U+{:04X} {} · {}", + c as u32, + crate::i18n::fmt(tl!("glyph {id}"), &[("id", &g.to_string())]), + crate::i18n::tn(chars.len() as u64, "{n} glyph", "{n} glyphs") + ), + None => crate::i18n::tn(chars.len() as u64, "{n} glyph", "{n} glyphs"), }) .size(11.0) .color(t.text_dim), @@ -207,13 +212,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { - if menu_item(ui, "Use the Selected Text's Font", true, false) { + if menu_item(ui, tl!("Use the Selected Text's Font"), true, false) { set_pstate::>(ui.ctx(), "gl-font", None); } ui.separator(); for (i, (name, _, _)) in SUBSETS.iter().enumerate() { let cur: usize = pstate(ui.ctx(), "gl-subset"); - if menu_item(ui, name, true, cur == i) { + if menu_item(ui, tl!(name), true, cur == i) { set_pstate(ui.ctx(), "gl-subset", i); } } diff --git a/crates/ui-egui/src/panels/gradient.rs b/crates/ui-egui/src/panels/gradient.rs index dd071b114..b644af0f1 100644 --- a/crates/ui-egui/src/panels/gradient.rs +++ b/crates/ui-egui/src/panels/gradient.rs @@ -185,18 +185,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(4.0); ui.vertical(|ui| { ui.horizontal(|ui| { - widgets::dim_label(ui, "Type:"); + widgets::dim_label(ui, tl!("Type:")); for (k, icon, tip) in [ - (GradientKind::Linear, "dc-grad-linear", "Linear Gradient"), - (GradientKind::Radial, "dc-grad-radial", "Radial Gradient"), - (GradientKind::Freeform, "dc-grad-freeform", "Freeform Gradient"), + (GradientKind::Linear, "dc-grad-linear", tl!("Linear Gradient")), + (GradientKind::Radial, "dc-grad-radial", tl!("Radial Gradient")), + (GradientKind::Freeform, "dc-grad-freeform", tl!("Freeform Gradient")), ] { if widgets::icon_button(ui, icon, tip, is_grad && kind == k, 24.0).clicked() { edit(app, json!({"kind": k.label().to_lowercase()}), Live::Released); } } }); - if widgets::flat_button(ui, "Edit Gradient", 96.0).clicked() { + if widgets::flat_button(ui, tl!("Edit Gradient"), 96.0).clicked() { app.select_tool("gradient"); } }); @@ -213,11 +213,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let stroke_mode = (!app.session.fill_active && is_grad && !freeform).then(|| app.session.shown_stroke().map(|st| st.gradient_mode)).flatten(); ui.add_space(4.0); ui.horizontal(|ui| { - widgets::dim_label(ui, "Stroke:"); + widgets::dim_label(ui, tl!("Stroke:")); for (m, icon, tip) in [ - (StrokeGradientMode::Within, "dc-grad-stroke-within", "Gradient within stroke"), - (StrokeGradientMode::Along, "dc-grad-stroke-along", "Gradient along stroke"), - (StrokeGradientMode::Across, "dc-grad-stroke-across", "Gradient across stroke"), + (StrokeGradientMode::Within, "dc-grad-stroke-within", tl!("Gradient within stroke")), + (StrokeGradientMode::Along, "dc-grad-stroke-along", tl!("Gradient along stroke")), + (StrokeGradientMode::Across, "dc-grad-stroke-across", tl!("Gradient across stroke")), ] { let on = stroke_mode == Some(m); if widgets::icon_button_enabled(ui, icon, tip, on, stroke_mode.is_some(), 22.0).clicked() && !on { @@ -237,7 +237,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(4.0); ui.vertical(|ui| { ui.horizontal(|ui| { - icons::icon(ui, "rotate-ccw", 15.0, if placed { t.icon } else { t.text_disabled }).on_hover_text("Angle"); + icons::icon(ui, "rotate-ccw", 15.0, if placed { t.icon } else { t.text_disabled }).on_hover_text(tl!("Angle")); let angle = g.geom.map_or(g.angle, |x| x.angle_deg()); let set = ui .add_enabled_ui(placed, |ui| { @@ -247,13 +247,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if let Some(a) = set { edit(app, json!({"angle": a}), Live::Released); } - if widgets::icon_button_enabled(ui, "dc-reverse", "Reverse Gradient", false, is_grad, 22.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-reverse", tl!("Reverse Gradient"), false, is_grad, 22.0).clicked() { edit(app, json!({"reverse": true}), Live::Released); } }); let radial = kind == GradientKind::Radial && placed; ui.horizontal(|ui| { - icons::icon(ui, "scaling", 15.0, if radial { t.icon } else { t.text_disabled }).on_hover_text("Aspect Ratio"); + icons::icon(ui, "scaling", 15.0, if radial { t.icon } else { t.text_disabled }).on_hover_text(tl!("Aspect Ratio")); let asp = g.geom.map_or(100.0, |x| (x.aspect * 1000.0).round() / 10.0); let set = ui.add_enabled_ui(radial, |ui| widgets::spin_plain(ui, "grad-aspect", asp, "%", 1, 104.0, 1.0, 0.5, &ASPECT_PRESETS)).inner; if let Some(a) = set { @@ -267,7 +267,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(4.0); stop_fields(app, ui, &g.gradient, is_grad); if !is_grad { - widgets::dim_label(ui, "Click the ramp or a type button to apply a gradient."); + widgets::dim_label(ui, tl!("Click the ramp or a type button to apply a gradient.")); } } @@ -291,10 +291,10 @@ fn freeform_section(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint) { ui.horizontal(|ui| { super::proxy(app, ui, 36.0); ui.add_space(4.0); - widgets::dim_label(ui, "Draw:"); + widgets::dim_label(ui, tl!("Draw:")); for (m, icon, tip) in [ - (FreeformMode::Points, "circle", "Points: clicks add free points"), - (FreeformMode::Lines, "spline", "Lines: clicks add points joined by a line"), + (FreeformMode::Points, "circle", tl!("Points: clicks add free points")), + (FreeformMode::Lines, "spline", tl!("Lines: clicks add points joined by a line")), ] { if widgets::icon_button(ui, icon, tip, mode == m, 24.0).clicked() && mode != m { edit(app, json!({ "mode": m.label().to_lowercase() }), Live::Released); @@ -305,18 +305,18 @@ fn freeform_section(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint) { let sel = app.session.selected_freeform_point().filter(|i| *i < f.points.len()); let point = sel.map(|i| f.points[i]); ui.horizontal(|ui| { - widgets::dim_label(ui, "Color:"); + widgets::dim_label(ui, tl!("Color:")); let (r, resp) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover()); let paint = point.map_or(Paint::None, |p| Paint::solid(p.color)); widgets::swatch_tile(ui, r, &paint, false, resp.hovered()); - resp.on_hover_text("Edit the selected point's colour in the Color panel"); + resp.on_hover_text(tl!("Edit the selected point's colour in the Color panel")); if let Some(p) = point { widgets::dim_label(ui, &p.color.to_hex().to_uppercase()); } }); ui.horizontal(|ui| { ui.add_enabled_ui(sel.is_some(), |ui| { - for (label, key, value) in [("Opacity:", "opacity", point.map(|p| p.opacity)), ("Spread:", "spread", point.map(|p| p.spread))] { + for (label, key, value) in [(tl!("Opacity:"), "opacity", point.map(|p| p.opacity)), (tl!("Spread:"), "spread", point.map(|p| p.spread))] { widgets::dim_label(ui, label); // Blank while no point is selected. let shown = value.map(|v| (v as f64 * 1000.0).round() / 10.0); @@ -328,14 +328,14 @@ fn freeform_section(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint) { } }); let can_del = sel.is_some() && f.points.len() > 1; - if widgets::icon_button_enabled(ui, "trash-2", "Delete Point", false, can_del, 22.0).clicked() + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Point"), false, can_del, 22.0).clicked() && let Some(i) = sel { edit_point(app, "paint.freeform.deletePoint", json!({ "index": i })); } }); if sel.is_none() { - widgets::dim_label(ui, "Click the art with the Gradient tool to add or select points."); + widgets::dim_label(ui, tl!("Click the art with the Gradient tool to add or select points.")); } } @@ -346,7 +346,7 @@ fn thumbnail(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint, is_grad: let (r, resp) = ui.allocate_exact_size(vec2(40.0, 40.0), Sense::click_and_drag()); widgets::gradient_chip(ui, r, &g.gradient); ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, t.border), StrokeKind::Inside); - let resp = resp.on_hover_text("Gradient Fill: click to apply, drag onto art"); + let resp = resp.on_hover_text(tl!("Gradient Fill: click to apply, drag onto art")); if resp.clicked() && !is_grad { edit(app, json!({}), Live::Released); } @@ -356,13 +356,13 @@ fn thumbnail(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint, is_grad: ui.advance_cursor_after_rect(dr); let dresp = ui.interact(dr, ui.id().with("grad-swatches"), Sense::click()); icons::paint(ui, "chevron-down", Rect::from_center_size(dr.center(), vec2(12.0, 12.0)), if dresp.hovered() { t.text_strong } else { t.icon }); - let dresp = dresp.on_hover_text("Gradient swatches"); + let dresp = dresp.on_hover_text(tl!("Gradient swatches")); egui::Popup::menu(&dresp).show(|ui| { ui.set_min_width(200.0); let mut chosen = None; let swatches = gradient_swatches(app); if swatches.is_empty() { - widgets::dim_label(ui, "No gradient swatches"); + widgets::dim_label(ui, tl!("No gradient swatches")); } for (name, grad) in &swatches { let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click()); @@ -381,7 +381,7 @@ fn thumbnail(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint, is_grad: } } ui.separator(); - if menu_item(ui, "Save to Swatches", is_grad, false) { + if menu_item(ui, tl!("Save to Swatches"), is_grad, false) { save_to_swatches(app, g); } if let Some(name) = chosen { @@ -400,7 +400,7 @@ fn stop_fields(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: boo let stop = sel.map(|i| &stops[i]); if let (Some(i), Some(s)) = (sel, stop) { ui.horizontal(|ui| { - widgets::dim_label(ui, "Color:"); + widgets::dim_label(ui, tl!("Color:")); let (r, resp) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::click()); widgets::swatch_tile(ui, r, &Paint::solid(s.color), false, resp.hovered()); // A linked stop names its swatch and tint. @@ -409,18 +409,18 @@ fn stop_fields(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: boo None => s.color.to_hex().to_uppercase(), }; widgets::dim_label(ui, &label); - if resp.on_hover_text("Edit the stop").clicked() { + if resp.on_hover_text(tl!("Edit the stop")).clicked() { open_popover(app, i, r.left_bottom() + vec2(0.0, 4.0)); } ui.add_space(ui.available_width() - 22.0); - if widgets::icon_button(ui, "pipette", "Eyedropper: click the art to sample the stop's colour", false, 22.0).clicked() { + if widgets::icon_button(ui, "pipette", tl!("Eyedropper: click the art to sample the stop's colour"), false, 22.0).clicked() { stop_eyedropper(app); } }); } ui.horizontal(|ui| { ui.add_enabled_ui(sel.is_some(), |ui| { - widgets::dim_label(ui, "Opacity:"); + widgets::dim_label(ui, tl!("Opacity:")); if let Some(v) = widgets::plain_field(ui, "grad-op", stop.map_or(100.0, |s| s.opacity as f64 * 100.0), "%", 0, 54.0) && let Some(i) = sel { @@ -430,7 +430,7 @@ fn stop_fields(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: boo } }); ui.add_enabled_ui(sel.is_some() || mid.is_some(), |ui| { - widgets::dim_label(ui, "Location:"); + widgets::dim_label(ui, tl!("Location:")); let loc = match (mid, stop) { (Some(m), _) => stops[m].midpoint as f64 * 100.0, (None, Some(s)) => s.offset as f64 * 100.0, @@ -447,7 +447,7 @@ fn stop_fields(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: boo } }); let can_del = sel.is_some() && stops.len() > MIN_STOPS; - if widgets::icon_button_enabled(ui, "trash-2", "Delete Stop", false, can_del, 22.0).clicked() + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Stop"), false, can_del, 22.0).clicked() && let Some(i) = sel && let Some(v) = remove_stop(stops, i) { @@ -668,19 +668,19 @@ fn ramp(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let g = current(app); let hidden: bool = pstate(ui.ctx(), HIDE_OPTIONS); - if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), HIDE_OPTIONS, !hidden); } ui.separator(); - if menu_item(ui, "Add to Swatches", g.is_some(), false) + if menu_item(ui, tl!("Add to Swatches"), g.is_some(), false) && let Some(g) = &g { save_to_swatches(app, g); } - if menu_item(ui, "Reverse Gradient", g.is_some(), false) { + if menu_item(ui, tl!("Reverse Gradient"), g.is_some(), false) { edit(app, json!({"reverse": true}), Live::Released); } - if menu_item(ui, "Reset to White, Black", true, false) { + if menu_item(ui, tl!("Reset to White, Black"), true, false) { set_stops(app, &Gradient::default().stops, None, Live::Released); } } diff --git a/crates/ui-egui/src/panels/graphic_styles.rs b/crates/ui-egui/src/panels/graphic_styles.rs index d8adbc495..d93729720 100644 --- a/crates/ui-egui/src/panels/graphic_styles.rs +++ b/crates/ui-egui/src/panels/graphic_styles.rs @@ -455,7 +455,7 @@ fn selected(ctx: &egui::Context, d: &Document) -> Vec { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // The document is shared (an `Arc`): holding it while drawing copies nothing. let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { - super::empty_state(ui, "dc-graphic-styles", "No document", "Open a document to see its graphic styles."); + super::empty_state(ui, "dc-graphic-styles", tl!("No document"), tl!("Open a document to see its graphic styles.")); return; }; let looks = look_hashes(ui.ctx(), app); @@ -468,7 +468,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.set_min_height(120.0); ui.set_width(ui.available_width()); if doc.graphic_styles.is_empty() { - super::empty_state(ui, "dc-graphic-styles", "No graphic styles", "Select styled art and click New Graphic Style."); + super::empty_state(ui, "dc-graphic-styles", tl!("No graphic styles"), tl!("Select styled art and click New Graphic Style.")); return; } match view.row() { @@ -490,14 +490,16 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.set_min_width(200.0); library_panel::library_menu::(app, ui); }); - if widgets::icon_button_enabled(ui, "link-2-off", "Break Link to Graphic Style", false, linked, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "link-2-off", tl!("Break Link to Graphic Style"), false, linked, 24.0).clicked() { app.run("graphicStyle.breakLink", json!({})).ok(); } ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0)); - if widgets::icon_button_enabled(ui, "dc-new-item", "New Graphic Style (Alt-click to name it)", false, has_sel, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-new-item", tl!("New Graphic Style (Alt-click to name it)"), false, has_sel, 24.0).clicked() { new_style(app, alt_held(ui)); } - if widgets::icon_button_enabled(ui, "trash-2", "Delete Graphic Style (Alt-click: without asking)", false, !sel.is_empty(), 24.0).clicked() { + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Graphic Style (Alt-click: without asking)"), false, !sel.is_empty(), 24.0) + .clicked() + { delete(app, &sel, alt_held(ui)); } }); @@ -548,10 +550,10 @@ fn delete(app: &mut VectorcraftApp, names: &[String], now: bool) { return; } let message = match names { - [n] => format!("Delete the graphic style \u{201c}{n}\u{201d}?"), - _ => format!("Delete these {} graphic styles?", names.len()), + [n] => crate::i18n::fmt(tl!("Delete the graphic style “{name}”?"), &[("name", n)]), + _ => crate::i18n::tn(names.len() as u64, "Delete this {n} graphic style?", "Delete these {n} graphic styles?"), }; - crate::dialogs::confirm::ask(app, &message, "Objects using them keep their look but are no longer linked.", "graphicStyle.delete", params); + crate::dialogs::confirm::ask(app, &message, tl!("Objects using them keep their look but are no longer linked."), "graphicStyle.delete", params); } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -559,15 +561,15 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let sel = doc.as_ref().map(|d| selected(ui.ctx(), d)).unwrap_or_default(); let one = (sel.len() == 1).then(|| sel[0].clone()); let has_doc = doc.is_some(); - if menu_item(ui, "New Graphic Style…", selection_len(app) > 0, false) { + if menu_item(ui, tl!("New Graphic Style…"), selection_len(app) > 0, false) { new_style(app, true); } - if menu_item(ui, "Duplicate Graphic Style", one.is_some(), false) + if menu_item(ui, tl!("Duplicate Graphic Style"), one.is_some(), false) && let Some(n) = &one { app.run("graphicStyle.duplicate", json!({ "name": n })).ok(); } - if menu_item(ui, "Merge Graphic Styles", sel.len() > 1, false) + if menu_item(ui, tl!("Merge Graphic Styles"), sel.len() > 1, false) && let Some(d) = &doc { // Merged in panel order. @@ -575,48 +577,48 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { names.sort_by_key(|n| d.graphic_style_index(n)); run(app, "ui.mergeGraphicStyles", json!({ "names": names })); } - if menu_item(ui, "Delete Graphic Style", !sel.is_empty(), false) { + if menu_item(ui, tl!("Delete Graphic Style"), !sel.is_empty(), false) { delete(app, &sel, false); } - if menu_item(ui, "Break Link to Graphic Style", app.session.selection_graphic_style().is_some(), false) { + if menu_item(ui, tl!("Break Link to Graphic Style"), app.session.selection_graphic_style().is_some(), false) { app.run("graphicStyle.breakLink", json!({})).ok(); } ui.separator(); - if menu_item(ui, "Select All Unused", has_doc, false) + if menu_item(ui, tl!("Select All Unused"), has_doc, false) && let Ok(r) = app.run("graphicStyle.unused", json!({})) { let names: Vec = serde_json::from_value(r["names"].clone()).unwrap_or_default(); set_pstate(ui.ctx(), "gs-sel", names); } - if menu_item(ui, "Sort by Name", has_doc, false) { + if menu_item(ui, tl!("Sort by Name"), has_doc, false) { app.run("graphicStyle.sortByName", json!({})).ok(); } ui.separator(); let view: View = pstate(ui.ctx(), "gs-view"); for (v, label) in View::ALL { - if menu_item(ui, label, true, view == v) { + if menu_item(ui, tl!(label), true, view == v) { set_pstate(ui.ctx(), "gs-view", v); } } ui.separator(); let preview: Preview = pstate(ui.ctx(), "gs-preview"); - for (p, label) in [(Preview::Square, "Use Square for Previews"), (Preview::Text, "Use Text for Previews")] { + for (p, label) in [(Preview::Square, tl!("Use Square for Previews")), (Preview::Text, tl!("Use Text for Previews"))] { if menu_item(ui, label, true, preview == p) { set_pstate(ui.ctx(), "gs-preview", p); } } let over = app.session.prefs.override_char_color; - if menu_item(ui, "Override Character Color", true, over) { + if menu_item(ui, tl!("Override Character Color"), true, over) { app.run("graphicStyle.setOptions", json!({ "overrideCharColor": !over })).ok(); } ui.separator(); - if menu_item(ui, "Graphic Style Options…", one.is_some(), false) + if menu_item(ui, tl!("Graphic Style Options…"), one.is_some(), false) && let Some(n) = &one { app.run("ui.graphicStyleOptions", json!({ "name": n })).ok(); } - ui.menu_button("Open Graphic Style Library", |ui| library_panel::library_menu::(app, ui)); - if menu_item(ui, "Save Graphic Style Library…", has_doc, false) { + ui.menu_button(tl!("Open Graphic Style Library"), |ui| library_panel::library_menu::(app, ui)); + if menu_item(ui, tl!("Save Graphic Style Library…"), has_doc, false) { run(app, "ui.saveGraphicStyleLibrary", json!({ "names": sel })); } } @@ -667,19 +669,19 @@ fn shown_library(lib: Arc) -> LibStyles { /// transparency. fn describe(g: &GraphicStyle) -> String { let ap = &g.appearance; - let count = |fill: bool, one: &str| match ap.items.iter().filter(|i| i.is_fill() == fill).count() { + let count = |fill: bool| match ap.items.iter().filter(|i| i.is_fill() == fill).count() { 0 => None, - 1 => Some(format!("1 {one}")), - n => Some(format!("{n} {one}s")), + n if fill => Some(crate::i18n::tn(n as u64, "{n} fill", "{n} fills")), + n => Some(crate::i18n::tn(n as u64, "{n} stroke", "{n} strokes")), }; - let mut parts: Vec = [count(true, "fill"), count(false, "stroke")].into_iter().flatten().collect(); + let mut parts: Vec = [count(true), count(false)].into_iter().flatten().collect(); let effects = ap.effects.iter().filter_map(|e| vectorcraft_render::effects::effect_info(&e.id)); parts.extend(effects.map(|e| e.label.trim_end_matches('…').to_string())); if g.opacity < 1.0 { - parts.push(format!("{}% opacity", (g.opacity * 100.0).round())); + parts.push(crate::i18n::fmt(tl!("{percent}% opacity"), &[("percent", &format!("{}", (g.opacity * 100.0).round()))])); } if g.blend != BlendMode::Normal { - parts.push(g.blend.label().to_string()); + parts.push(tl!(g.blend.label()).to_string()); } parts.join(", ") } @@ -728,13 +730,13 @@ impl LibraryKind for GraphicStyleLibraries { } fn menu_tail(app: &mut VectorcraftApp, ui: &mut Ui) { ui.separator(); - if menu_item(ui, "Other Library…", true, false) + if menu_item(ui, tl!("Other Library…"), true, false) && let Err(e) = other_library(app, None) { app.status(e); } let doc = app.session.active().map(|st| st.doc.clone()); - if menu_item(ui, "Save Graphic Style Library…", doc.is_some(), false) { + if menu_item(ui, tl!("Save Graphic Style Library…"), doc.is_some(), false) { let names = doc.map(|d| selected(ui.ctx(), &d)).unwrap_or_default(); run(app, "ui.saveGraphicStyleLibrary", json!({ "names": names })); } diff --git a/crates/ui-egui/src/panels/history.rs b/crates/ui-egui/src/panels/history.rs index 007e944f7..75199d7d2 100644 --- a/crates/ui-egui/src/panels/history.rs +++ b/crates/ui-egui/src/panels/history.rs @@ -16,7 +16,7 @@ pub fn steps_to(current: usize, target: usize) -> i64 { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { - super::empty_state(ui, "history", "No document", "Open a document to see its history."); + super::empty_state(ui, "history", tl!("No document"), tl!("Open a document to see its history.")); return; }; let mut rows: Vec<(String, bool)> = vec![("Open".to_string(), false)]; @@ -42,7 +42,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if *future { t.text_disabled } else { t.icon }, ); let col = if *future { t.text_disabled } else { t.text }; - ui.painter().text(r.left_center() + vec2(30.0, 0.0), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(12.5), col); + ui.painter().text(r.left_center() + vec2(30.0, 0.0), egui::Align2::LEFT_CENTER, tl!(label), egui::FontId::proportional(12.5), col); if resp.clicked() && i != current { steps = Some(steps_to(current, i)); } @@ -60,7 +60,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { - menu_item(ui, "History Options…", false, false); + menu_item(ui, tl!("History Options…"), false, false); } #[cfg(test)] diff --git a/crates/ui-egui/src/panels/image_trace.rs b/crates/ui-egui/src/panels/image_trace.rs index 948c49754..07e01595c 100644 --- a/crates/ui-egui/src/panels/image_trace.rs +++ b/crates/ui-egui/src/panels/image_trace.rs @@ -49,7 +49,7 @@ fn trace(app: &mut VectorcraftApp, st: &mut TraceUi) { fn slider(ui: &mut Ui, label: &str, v: &mut f64, range: std::ops::RangeInclusive, suffix: &str) -> (bool, bool) { let mut out = (false, false); ui.horizontal(|ui| { - ui.add_sized([74.0, 22.0], egui::Label::new(egui::RichText::new(label).size(12.0))); + ui.add_sized([74.0, 22.0], egui::Label::new(egui::RichText::new(tl!(label)).size(12.0))); // The theme's widget fill matches the panel, which would hide the rail. let t = crate::theme::Tokens::get(ui.ctx()); ui.visuals_mut().widgets.inactive.bg_fill = t.input_border; @@ -76,7 +76,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let mut retrace = false; ui.horizontal(|ui| { - widgets::dim_label(ui, "Preset:"); + widgets::dim_label(ui, tl!("Preset:")); let names: Vec<&str> = all.iter().map(|p| p.0.as_str()).collect(); if let Some(i) = widgets::dropdown(ui, "it-preset", &st.preset, &names, 170.0) { st.preset = all[i].0.clone(); @@ -85,7 +85,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); ui.horizontal(|ui| { - widgets::dim_label(ui, "Mode:"); + widgets::dim_label(ui, tl!("Mode:")); let mode = st.params["mode"].as_str().unwrap_or("blackAndWhite").to_string(); let label = MODES.iter().find(|m| m.0 == mode).map_or("Black and White", |m| m.1); let labels: Vec<&str> = MODES.iter().map(|m| m.1).collect(); @@ -97,9 +97,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); let bw = st.params["mode"].as_str() == Some("blackAndWhite"); let (key, label, range) = if bw { - ("threshold", "Threshold", 0.0..=255.0) + ("threshold", tl!("Threshold"), 0.0..=255.0) } else { - ("colors", if st.params["mode"] == "grayscale" { "Grays" } else { "Colors" }, 2.0..=256.0) + ("colors", if st.params["mode"] == "grayscale" { tl!("Grays") } else { tl!("Colors") }, 2.0..=256.0) }; let mut v = st.params[key].as_f64().unwrap_or(0.0); let (changed, release) = slider(ui, label, &mut v, range, ""); @@ -110,12 +110,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { retrace |= release; let open: bool = !pstate::(ui.ctx(), "it-advanced-closed"); - if ui.add(egui::Button::new(format!("{} Advanced", if open { "▾" } else { "▸" })).frame(false)).clicked() { + if ui.add(egui::Button::new(format!("{} {}", if open { "▾" } else { "▸" }, tl!("Advanced"))).frame(false)).clicked() { set_pstate(ui.ctx(), "it-advanced-closed", open); } if open { for (key, label, range, suffix) in - [("paths", "Paths", 0.0..=100.0, "%"), ("corners", "Corners", 0.0..=100.0, "%"), ("noise", "Noise", 1.0..=100.0, " px")] + [("paths", tl!("Paths"), 0.0..=100.0, "%"), ("corners", tl!("Corners"), 0.0..=100.0, "%"), ("noise", tl!("Noise"), 1.0..=100.0, " px")] { let mut v = st.params[key].as_f64().unwrap_or(0.0); let (changed, release) = slider(ui, label, &mut v, range, suffix); @@ -126,8 +126,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { retrace |= release; } ui.horizontal(|ui| { - widgets::dim_label(ui, "Method:"); - for (m, l) in [("abutting", "Abutting"), ("overlapping", "Overlapping")] { + widgets::dim_label(ui, tl!("Method:")); + for (m, l) in [("abutting", tl!("Abutting")), ("overlapping", tl!("Overlapping"))] { if ui.selectable_label(st.params["method"] == m, l).clicked() && st.params["method"] != m { st.params["method"] = json!(m); st.preset = "Custom".into(); @@ -135,7 +135,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } }); - for (key, label) in [("snapCurvesToLines", "Snap Curves To Lines"), ("ignoreWhite", "Ignore White")] { + for (key, label) in [("snapCurvesToLines", tl!("Snap Curves To Lines")), ("ignoreWhite", tl!("Ignore White"))] { let on = st.params[key].as_bool().unwrap_or(false); if widgets::check(ui, label, on, true) { st.params[key] = json!(!on); @@ -146,14 +146,20 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } widgets::divider(ui); if let Some((p, a, c)) = st.info { - widgets::dim_label(ui, &format!("Paths: {p} Anchors: {a} Colors: {c}")); + widgets::dim_label( + ui, + &crate::i18n::fmt( + tl!("Paths: {paths} Anchors: {anchors} Colors: {colors}"), + &[("paths", &p.to_string()), ("anchors", &a.to_string()), ("colors", &c.to_string())], + ), + ); } ui.horizontal(|ui| { - let r = ui.add_enabled_ui(is_trace || is_image, |ui| widgets::flat_button(ui, "Trace", 80.0)).inner; - if r.on_disabled_hover_text("Select an image to trace").clicked() { + let r = ui.add_enabled_ui(is_trace || is_image, |ui| widgets::flat_button(ui, tl!("Trace"), 80.0)).inner; + if r.on_disabled_hover_text(tl!("Select an image to trace")).clicked() { trace(app, &mut st); } - if ui.add_enabled_ui(is_trace, |ui| widgets::flat_button(ui, "Expand", 80.0)).inner.clicked() { + if ui.add_enabled_ui(is_trace, |ui| widgets::flat_button(ui, tl!("Expand"), 80.0)).inner.clicked() { app.run("imageTrace.expand", json!({})).ok(); } }); @@ -165,14 +171,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let (is_trace, _, _) = target(app); - if menu_item(ui, "Release", is_trace, false) { + if menu_item(ui, tl!("Release"), is_trace, false) { app.run("imageTrace.release", json!({})).ok(); } - if menu_item(ui, "Expand", is_trace, false) { + if menu_item(ui, tl!("Expand"), is_trace, false) { app.run("imageTrace.expand", json!({})).ok(); } ui.separator(); - if menu_item(ui, "Reset to Default", true, false) { + if menu_item(ui, tl!("Reset to Default"), true, false) { set_pstate(ui.ctx(), "image-trace", TraceUi::default()); } } diff --git a/crates/ui-egui/src/panels/info.rs b/crates/ui-egui/src/panels/info.rs index dcb5eb04c..a23678f9f 100644 --- a/crates/ui-egui/src/panels/info.rs +++ b/crates/ui-egui/src/panels/info.rs @@ -80,13 +80,19 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { widgets::divider(ui); let st = tx.first_style(); mono(ui, format!("{} {}", st.font_family, st.font_style)); - mono(ui, format!("Size: {} Tracking: {:.0}", app.session.type_unit().format(st.size), st.tracking)); + mono( + ui, + crate::i18n::fmt( + tl!("Size: {size} Tracking: {tracking}"), + &[("size", &app.session.type_unit().format(st.size)), ("tracking", &format!("{:.0}", st.tracking))], + ), + ); } } pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "info-hide-options"); - if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), "info-hide-options", !hidden); } } diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 81985c7b5..2e7925866 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -22,6 +22,16 @@ fn expanded_id() -> egui::Id { egui::Id::new("layers-expanded") } +/// The name painted for a row: a generated `` name is translated, anything else is user data. +fn painted_name(n: &Node, name: &str) -> String { + if n.name.is_none() + && let Some(inner) = name.strip_prefix('<').and_then(|s| s.strip_suffix('>')) + { + return format!("<{}>", crate::i18n::t(inner)); + } + name.to_string() +} + pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { return }; @@ -46,7 +56,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let mut expanded: HashSet = ui.data(|d| d.get_temp(expanded_id())).unwrap_or_else(|| doc.layers.iter().map(|l| l.id.0).collect()); let mut actions: Vec<(String, serde_json::Value)> = vec![]; // Search field ("Search All"). - crate::widgets::search_field(ui, egui::Id::new("layers-search"), "Search All"); + crate::widgets::search_field(ui, egui::Id::new("layers-search"), tl!("Search All")); ui.add_space(6.0); let h = ui.available_height() - 34.0; egui::ScrollArea::vertical().max_height(h).auto_shrink([false, false]).show(ui, |ui| { @@ -66,12 +76,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.painter().text( bar.left_center() + vec2(4.0, 0.0), egui::Align2::LEFT_CENTER, - format!("{n} Layer{}", if n == 1 { "" } else { "s" }), + crate::i18n::tn(n as u64, "{n} Layer", "{n} Layers"), egui::FontId::proportional(11.5), t.text_dim, ); let mut child = ui.new_child(egui::UiBuilder::new().max_rect(bar).layout(egui::Layout::right_to_left(egui::Align::Center))); - let trash = widgets::icon_button(&mut child, "trash-2", "Delete Selection", false, 24.0); + let trash = widgets::icon_button(&mut child, "trash-2", tl!("Delete Selection"), false, 24.0); // A target circle dropped on the trash clears that appearance. if let Some(d) = trash.dnd_release_payload::() && let PanelDrag::Appearance(id) = *d @@ -84,16 +94,16 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { actions.push(("layer.delete".into(), json!({}))); } } - if widgets::icon_button(&mut child, "file-plus", "Create New Layer", false, 24.0).clicked() { + if widgets::icon_button(&mut child, "file-plus", tl!("Create New Layer"), false, 24.0).clicked() { actions.push(("layer.new".into(), json!({}))); } - if widgets::icon_button(&mut child, "plus", "Create New Sublayer", false, 24.0).clicked() { + if widgets::icon_button(&mut child, "plus", tl!("Create New Sublayer"), false, 24.0).clicked() { actions.push(("layer.newSublayer".into(), json!({}))); } - if widgets::icon_button(&mut child, "frame", "Make/Release Clipping Mask", false, 24.0).clicked() { + if widgets::icon_button(&mut child, "frame", tl!("Make/Release Clipping Mask"), false, 24.0).clicked() { actions.push(("layer.clippingMask.toggle".into(), json!({}))); } - if widgets::icon_button(&mut child, "search", "Locate Object", false, 24.0).clicked() { + if widgets::icon_button(&mut child, "search", tl!("Locate Object"), false, 24.0).clicked() { // Expand ancestors of the selection. if let Some(st) = app.session.active() { let mut ex: HashSet = ui.data(|d| d.get_temp(expanded_id())).unwrap_or_default(); @@ -233,7 +243,9 @@ fn row( } _ => { let painter = ui.painter().with_clip_rect(name_rect); - let text = painter.text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, name.clone(), font, t.text); + // Generated names ("", "") are translated where painted; the stored name stays English. + let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { tl!("").to_string() } else { painted_name(n, &name) }; + let text = painter.text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, shown, font, t.text); // A clipping path's name is underlined, a masked object's with a dashed line. if clip_path { painter.line_segment([text.left_bottom(), text.right_bottom()], Stroke::new(1.0, t.text)); @@ -427,13 +439,13 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if i == 4 { ui.separator(); } - if widgets::menu_item(ui, label, crate::menus::enabled(app, cmd), false) { + if widgets::menu_item(ui, tl!(label), crate::menus::enabled(app, cmd), false) { app.run(cmd, json!({})).ok(); } } ui.separator(); let remembers = app.session.active().is_some_and(|d| d.doc.paste_remembers_layers); - if widgets::menu_item(ui, "Paste Remembers Layers", app.session.active().is_some(), remembers) { + if widgets::menu_item(ui, tl!("Paste Remembers Layers"), app.session.active().is_some(), remembers) { app.run("layer.pasteRemembersLayers", json!({"on": !remembers})).ok(); } } diff --git a/crates/ui-egui/src/panels/library_panel.rs b/crates/ui-egui/src/panels/library_panel.rs index 2affee132..0812a358c 100644 --- a/crates/ui-egui/src/panels/library_panel.rs +++ b/crates/ui-egui/src/panels/library_panel.rs @@ -174,17 +174,17 @@ fn window(app: &mut VectorcraftApp, ctx: &egui::Context, id: &st ui.set_width(256.0); let (strip, _) = ui.allocate_exact_size(vec2(256.0, 26.0), Sense::hover()); ui.painter().rect_filled(strip, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.panel_darker); - let label = egui::RichText::new(&name).font(theme::semibold(12.0)); - let galley = ui.painter().layout_no_wrap(name.clone(), theme::semibold(12.0), t.text); + let label = egui::RichText::new(tl!(&name)).font(theme::semibold(12.0)); + let galley = ui.painter().layout_no_wrap(tl!(&name).to_string(), theme::semibold(12.0), t.text); let tab = Rect::from_min_size(strip.min, vec2((galley.size().x + 24.0).min(190.0), 26.0)); ui.painter().rect_filled(tab, CornerRadius { nw: 4, ne: 0, sw: 0, se: 0 }, t.panel); ui.put(tab.shrink2(vec2(12.0, 0.0)), egui::Label::new(label.color(t.text)).truncate().selectable(false)); let close = Rect::from_center_size(strip.right_center() - vec2(13.0, 0.0), vec2(14.0, 14.0)); - let cr = ui.interact(close, ui.id().with("close-library"), Sense::click()).on_hover_text("Close"); + let cr = ui.interact(close, ui.id().with("close-library"), Sense::click()).on_hover_text(tl!("Close")); icons::paint(ui, "x", close, if cr.hovered() { t.text } else { t.text_dim }); open = !cr.clicked(); let menu = Rect::from_center_size(strip.right_center() - vec2(34.0, 0.0), vec2(16.0, 16.0)); - let mr = ui.interact(menu, ui.id().with("library-menu"), Sense::click()).on_hover_text("Panel menu"); + let mr = ui.interact(menu, ui.id().with("library-menu"), Sense::click()).on_hover_text(tl!("Panel menu")); icons::paint(ui, "menu", menu.shrink(1.0), if mr.hovered() { t.text_strong } else { t.text_dim }); egui::Popup::menu(&mr).show(|ui| { ui.set_min_width(200.0); @@ -217,7 +217,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, lib: &K let query = if pstate::(ui.ctx(), &key::("hide-find")) { String::new() } else { - widgets::search_field(ui, egui::Id::new(key::("find")), "Find") + widgets::search_field(ui, egui::Id::new(key::("find")), tl!("Find")) }; ui.add_space(4.0); let rows = K::rows(lib, &query.trim().to_lowercase()); @@ -234,7 +234,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, lib: &K egui::ScrollArea::vertical().id_salt(key::("scroll")).max_height(max_height).show(ui, |ui| { ui.set_width(ui.available_width()); if rows.is_empty() { - widgets::dim_label(ui, if query.trim().is_empty() { "This library is empty." } else { "No matches." }); + widgets::dim_label(ui, if query.trim().is_empty() { tl!("This library is empty.") } else { tl!("No matches.") }); } else if view.is_list() { for row in &rows { let (r, resp, chip) = list_row(ui, tile_id::(row.name), view, is_sel(row.name)); @@ -298,7 +298,7 @@ fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) { ui.set_min_width(200.0); library_menu::(app, ui); }); - for (icon, tip, step) in [("chevron-left", "Previous Library", -1), ("chevron-right", "Next Library", 1)] { + for (icon, tip, step) in [("chevron-left", tl!("Previous Library"), -1), ("chevron-right", tl!("Next Library"), 1)] { if widgets::icon_button(ui, icon, tip, false, 24.0).clicked() && let Some(next) = neighbour::(app, id, step) { @@ -327,18 +327,18 @@ fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) { } ui.separator(); let hidden: bool = pstate(ui.ctx(), &key::("hide-find")); - if menu_item(ui, "Show Find Field", true, !hidden) { + if menu_item(ui, tl!("Show Find Field"), true, !hidden) { set_pstate(ui.ctx(), &key::("hide-find"), !hidden); } ui.separator(); - for (label, step) in [("Previous Library", -1), ("Next Library", 1)] { + for (label, step) in [(tl!("Previous Library"), -1), (tl!("Next Library"), 1)] { if menu_item(ui, label, true, false) && let Some(next) = neighbour::(app, id, step) { open::(app, &next); } } - if menu_item(ui, "Close Library", true, false) { + if menu_item(ui, tl!("Close Library"), true, false) { app.ui.library_panel = None; } } @@ -373,7 +373,7 @@ pub(crate) fn library_items(ui: &mut Ui, libs: &[LibraryRef], current: Option<&s } } for s in subs { - ui.menu_button(s, |ui| { + ui.menu_button(tl!(s), |ui| { crate::widgets::menu_scroll(ui, |ui| { for l in libs.iter().filter(|l| l.submenu == Some(s)) { item(ui, l); diff --git a/crates/ui-egui/src/panels/links.rs b/crates/ui-egui/src/panels/links.rs index 0bd233475..124b215fa 100644 --- a/crates/ui-egui/src/panels/links.rs +++ b/crates/ui-egui/src/panels/links.rs @@ -198,9 +198,9 @@ fn badge(ui: &Ui, r: egui::Rect, status: &str) { fn status_tip(r: &Value) -> String { let path = r["found"].as_str().or(r["path"].as_str()).unwrap_or_default(); match r["status"].as_str().unwrap_or_default() { - "missing" => format!("Missing: {path}"), - "modified" => format!("Modified since it was read: {path}"), - "embedded" => "Embedded".into(), + "missing" => crate::i18n::fmt(tl!("Missing: {path}"), &[("path", path)]), + "modified" => crate::i18n::fmt(tl!("Modified since it was read: {path}"), &[("path", path)]), + "embedded" => tl!("Embedded").into(), _ => path.to_string(), } } @@ -209,7 +209,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let o = options(ui.ctx()); let Some(cache) = cached(app, ui.ctx(), &o) else { - widgets::dim_label(ui, "No document"); + widgets::dim_label(ui, tl!("No document")); return; }; let Some(doc) = app.session.active().map(|st| st.doc.clone()) else { return }; @@ -220,9 +220,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.set_min_height(110.0); ui.set_width(ui.available_width()); if rows.is_empty() { - let detail = - if o.show == "all" { "File › Place… brings in an image, linked or embedded." } else { "Nothing matches the Show filter." }; - super::empty_state(ui, "link", "No images", detail); + let detail = if o.show == "all" { + tl!("File › Place… brings in an image, linked or embedded.") + } else { + tl!("Nothing matches the Show filter.") + }; + super::empty_state(ui, "link", tl!("No images"), detail); return; } egui::ScrollArea::vertical().max_height(260.0).auto_shrink([false, true]).show(ui, |ui| { @@ -297,23 +300,23 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let can_edit = sel.iter().any(|r| r["linked"] == true && !status_is(r, "missing")); widgets::bottom_bar(ui, |ui| { let info = !o.info; - if widgets::icon_button(ui, if o.info { "chevron-down" } else { "chevron-right" }, "Show Link Info", false, 24.0).clicked() { + if widgets::icon_button(ui, if o.info { "chevron-down" } else { "chevron-right" }, tl!("Show Link Info"), false, 24.0).clicked() { set_options(ui.ctx(), |o| o.info = info); } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if widgets::icon_button_enabled(ui, "pencil", "Edit Original", false, can_edit, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "pencil", tl!("Edit Original"), false, can_edit, 24.0).clicked() { crate::menus::invoke(app, "links.editOriginal", json!({})); } - if widgets::icon_button_enabled(ui, "rotate-cw", "Update Link", false, !modified.is_empty(), 24.0).clicked() { + if widgets::icon_button_enabled(ui, "rotate-cw", tl!("Update Link"), false, !modified.is_empty(), 24.0).clicked() { let r = app.run("links.update", json!({ "ids": modified })); report(app, r, |v| format!("Updated {} image(s)", v["updated"].as_array().map_or(0, Vec::len))); } - if widgets::icon_button_enabled(ui, "zoom-in", "Go To Link", false, sel.len() == 1, 24.0).clicked() + if widgets::icon_button_enabled(ui, "zoom-in", tl!("Go To Link"), false, sel.len() == 1, 24.0).clicked() && let Some(id) = sel.first().and_then(|r| r["id"].as_u64()) { go_to(app, id); } - if widgets::icon_button_enabled(ui, "link", "Relink…", false, !sel.is_empty() && can_pick(app), 24.0).clicked() { + if widgets::icon_button_enabled(ui, "link", tl!("Relink…"), false, !sel.is_empty() && can_pick(app), 24.0).clicked() { relink(app, ids(&sel, |_| true)); } }); @@ -344,7 +347,7 @@ pub(crate) fn date_label(ms: u64) -> String { fn link_info(ui: &mut Ui, info: Option<&Value>) { widgets::divider(ui); let Some(i) = info else { - widgets::dim_label(ui, "Select an image to see its Link Info."); + widgets::dim_label(ui, tl!("Select an image to see its Link Info.")); return; }; let pair = |k: &str| { @@ -356,38 +359,38 @@ fn link_info(ui: &mut Ui, info: Option<&Value>) { } }; let row = super::doc_info::row; - row(ui, "Name", i["fileName"].as_str().or(i["name"].as_str()).unwrap_or_default().into()); - row(ui, "Format", i["format"].as_str().unwrap_or_default().into()); - row(ui, "Color Space", i["colorMode"].as_str().unwrap_or_default().into()); + row(ui, tl!("Name"), i["fileName"].as_str().or(i["name"].as_str()).unwrap_or_default().into()); + row(ui, tl!("Format"), i["format"].as_str().unwrap_or_default().into()); + row(ui, tl!("Color Space"), i["colorMode"].as_str().unwrap_or_default().into()); if let Some(l) = i["location"].as_str() { - row(ui, "Location", l.into()); + row(ui, tl!("Location"), l.into()); } - row(ui, "PPI", pair("ppi")); - row(ui, "Effective PPI", pair("effectivePpi")); - row(ui, "Dimensions", format!("{} × {} px", i["pixelWidth"], i["pixelHeight"])); + row(ui, tl!("PPI"), pair("ppi")); + row(ui, tl!("Effective PPI"), pair("effectivePpi")); + row(ui, tl!("Dimensions"), format!("{} × {} px", i["pixelWidth"], i["pixelHeight"])); let scale: Vec = i["scale"].as_array().into_iter().flatten().filter_map(Value::as_f64).collect(); if let [x, y] = scale[..] { - row(ui, "Scale", format!("H: {x:.1}% V: {y:.1}%")); + row(ui, tl!("Scale"), format!("H: {x:.1}% V: {y:.1}%")); } - row(ui, "Rotation", format!("{:.1}°", i["rotation"].as_f64().unwrap_or(0.0))); + row(ui, tl!("Rotation"), format!("{:.1}°", i["rotation"].as_f64().unwrap_or(0.0))); if let Some(b) = i["fileSize"].as_u64() { - row(ui, "Size", size_label(b)); + row(ui, tl!("Size"), size_label(b)); } - for (k, label) in [("created", "Created"), ("modified", "Modified")] { + for (k, label) in [("created", tl!("Created")), ("modified", tl!("Modified"))] { if let Some(ms) = i[k].as_u64() { row(ui, label, date_label(ms)); } } if let Some(p) = i["page"].as_u64() { - row(ui, "Page", p.to_string()); + row(ui, tl!("Page"), p.to_string()); } let status = match i["status"].as_str().unwrap_or_default() { - "ok" => "Linked", - "missing" => "Missing", - "modified" => "Modified", - _ => "Embedded", + "ok" => tl!("Linked"), + "missing" => tl!("Missing"), + "modified" => tl!("Modified"), + _ => tl!("Embedded"), }; - row(ui, "Status", status.into()); + row(ui, tl!("Status"), status.into()); } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -400,26 +403,26 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let embedded: Vec<&Value> = sel.iter().copied().filter(|r| status_is(r, "embedded")).collect(); let any = !sel.is_empty(); let missing_any = list["missing"].as_u64().unwrap_or(0) > 0; - if menu_item(ui, "Relink…", any && can_pick(app), false) { + if menu_item(ui, tl!("Relink…"), any && can_pick(app), false) { relink(app, ids(&sel, |_| true)); } - if menu_item(ui, "Relink to Folder…", (any || missing_any) && app.services.pick_folder.is_some(), false) { + if menu_item(ui, tl!("Relink to Folder…"), (any || missing_any) && app.services.pick_folder.is_some(), false) { relink_to_folder(app, ids(&sel, |_| true)); } - if menu_item(ui, "Go To Link", sel.len() == 1, false) + if menu_item(ui, tl!("Go To Link"), sel.len() == 1, false) && let Some(id) = sel.first().and_then(|r| r["id"].as_u64()) { go_to(app, id); } - if menu_item(ui, "Update Link", sel.iter().any(|r| status_is(r, "modified")), false) { + if menu_item(ui, tl!("Update Link"), sel.iter().any(|r| status_is(r, "modified")), false) { let r = app.run("links.update", json!({ "ids": ids(&sel, |r| status_is(r, "modified")) })); report(app, r, |v| format!("Updated {} image(s)", v["updated"].as_array().map_or(0, Vec::len))); } - if menu_item(ui, "Edit Original", sel.iter().any(|r| r["linked"] == true && !status_is(r, "missing")), false) { + if menu_item(ui, tl!("Edit Original"), sel.iter().any(|r| r["linked"] == true && !status_is(r, "missing")), false) { crate::menus::invoke(app, "links.editOriginal", json!({})); } ui.separator(); - if menu_item(ui, "Embed Image(s)", !linked.is_empty(), false) { + if menu_item(ui, tl!("Embed Image(s)"), !linked.is_empty(), false) { let r = app.run("links.embed", json!({ "ids": linked })); report(app, r, |v| { let missing = v["missing"].as_array().map_or(0, Vec::len); @@ -427,38 +430,38 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if missing == 0 { format!("Embedded {n} image(s)") } else { format!("Embedded {n} image(s); {missing} missing: relink them first") } }); } - if menu_item(ui, "Unembed…", embedded.len() == 1, false) + if menu_item(ui, tl!("Unembed…"), embedded.len() == 1, false) && let Some(r) = embedded.first() && let Some(id) = r["id"].as_u64() { let name = r["name"].as_str().unwrap_or("Image").to_string(); unembed(app, id, &name); } - if menu_item(ui, "Placement Options…", any, false) { + if menu_item(ui, tl!("Placement Options…"), any, false) { crate::menus::invoke(app, "ui.placementOptionsDialog", json!({ "ids": ids(&sel, |_| true) })); } - if menu_item(ui, "Show in Folder", sel.iter().any(|r| r["linked"] == true && !status_is(r, "missing")), false) { + if menu_item(ui, tl!("Show in Folder"), sel.iter().any(|r| r["linked"] == true && !status_is(r, "missing")), false) { crate::menus::invoke(app, "links.reveal", json!({})); } ui.separator(); for (id, label) in SHOW { - if menu_item(ui, label, true, o.show == id) { + if menu_item(ui, tl!(label), true, o.show == id) { set_options(ui.ctx(), |o| o.show = id); } } ui.separator(); for (id, label) in SORT { - if menu_item(ui, label, true, o.sort == Some(id)) { + if menu_item(ui, tl!(label), true, o.sort == Some(id)) { set_options(ui.ctx(), |o| o.sort = if o.sort == Some(id) { None } else { Some(id) }); } } ui.separator(); for (px, label) in THUMBS { - if menu_item(ui, label, true, o.thumb == px) { + if menu_item(ui, tl!(label), true, o.thumb == px) { set_options(ui.ctx(), |o| o.thumb = px); } } - if menu_item(ui, "Show Link Info", true, o.info) { + if menu_item(ui, tl!("Show Link Info"), true, o.info) { set_options(ui.ctx(), |o| o.info = !o.info); } } diff --git a/crates/ui-egui/src/panels/magic_wand.rs b/crates/ui-egui/src/panels/magic_wand.rs index 6e9c18c86..c2895e19f 100644 --- a/crates/ui-egui/src/panels/magic_wand.rs +++ b/crates/ui-egui/src/panels/magic_wand.rs @@ -25,7 +25,7 @@ fn row(app: &mut VectorcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str let on = o[flag].as_bool().unwrap_or(false); ui.horizontal(|ui| { ui.set_min_height(26.0); - if widgets::check(ui, label, on, true) { + if widgets::check(ui, tl!(label), on, true) { app.run("magicWand.set", json!({ flag: !on })).ok(); } if let Some((key, kind, max)) = tol { @@ -41,7 +41,7 @@ fn row(app: &mut VectorcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str app.run("magicWand.set", json!({ key: n.clamp(0.0, max) })).ok(); } }); - widgets::dim_label(ui, "Tolerance:"); + widgets::dim_label(ui, tl!("Tolerance:")); }); } }); @@ -49,28 +49,28 @@ fn row(app: &mut VectorcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let o = options(app); - row(app, ui, &o, "Fill Color", "fillColor", Some(("fillTolerance", Tol::Plain(""), 255.0))); + row(app, ui, &o, tl!("Fill Color"), "fillColor", Some(("fillTolerance", Tol::Plain(""), 255.0))); if !pstate::(ui.ctx(), "wand-hide-stroke") { widgets::divider(ui); - row(app, ui, &o, "Stroke Color", "strokeColor", Some(("strokeTolerance", Tol::Plain(""), 255.0))); - row(app, ui, &o, "Stroke Weight", "strokeWeight", Some(("weightTolerance", Tol::Weight, 1000.0))); + row(app, ui, &o, tl!("Stroke Color"), "strokeColor", Some(("strokeTolerance", Tol::Plain(""), 255.0))); + row(app, ui, &o, tl!("Stroke Weight"), "strokeWeight", Some(("weightTolerance", Tol::Weight, 1000.0))); } if !pstate::(ui.ctx(), "wand-hide-transparency") { widgets::divider(ui); - row(app, ui, &o, "Opacity", "opacity", Some(("opacityTolerance", Tol::Plain("%"), 100.0))); - row(app, ui, &o, "Blending Mode", "blendingMode", None); + row(app, ui, &o, tl!("Opacity"), "opacity", Some(("opacityTolerance", Tol::Plain("%"), 100.0))); + row(app, ui, &o, tl!("Blending Mode"), "blendingMode", None); } } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { - for (label, key) in [("Stroke Options", "wand-hide-stroke"), ("Transparency Options", "wand-hide-transparency")] { + for (label, key) in [(tl!("Stroke Options"), "wand-hide-stroke"), (tl!("Transparency Options"), "wand-hide-transparency")] { let hidden: bool = pstate(ui.ctx(), key); - if menu_item(ui, &format!("{} {label}", if hidden { "Show" } else { "Hide" }), true, false) { + if menu_item(ui, &format!("{} {label}", if hidden { tl!("Show") } else { tl!("Hide") }), true, false) { set_pstate(ui.ctx(), key, !hidden); } } ui.separator(); - if menu_item(ui, "Reset", true, false) { + if menu_item(ui, tl!("Reset"), true, false) { app.run("magicWand.set", json!({ "reset": true })).ok(); } } diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index edf4467e4..1dcef79d1 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -101,7 +101,7 @@ pub fn show_icon_panel(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) { asset_export::ID => asset_export::show(app, ui), css_properties::ID => css_properties::show(app, ui), _ => { - dim_label(ui, "This panel is on the roadmap (see the parity plan)."); + dim_label(ui, tl!("This panel is on the roadmap (see the parity plan).")); } } } @@ -146,7 +146,7 @@ pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) { asset_export::ID => asset_export::menu(app, ui), css_properties::ID => css_properties::menu(app, ui), _ => { - ui.add_enabled(false, egui::Button::new("No options").frame(false)); + ui.add_enabled(false, egui::Button::new(tl!("No options")).frame(false)); } } } @@ -156,7 +156,7 @@ pub fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, rect: Rect) { let t = Tokens::get(ui.ctx()); let resp = ui.interact(rect, ui.id().with(("panel-menu", id)), Sense::click()); icons::paint(ui, "menu", rect.shrink(1.0), if resp.hovered() { t.text_strong } else { t.text_dim }); - let resp = resp.on_hover_text("Panel menu"); + let resp = resp.on_hover_text(tl!("Panel menu")); egui::Popup::menu(&resp).show(|ui| { crate::widgets::menu_scroll(ui, |ui| { ui.set_min_width(220.0); @@ -171,7 +171,7 @@ pub fn libraries(_app: &mut VectorcraftApp, ui: &mut Ui) { ui.vertical_centered(|ui| { icons::icon(ui, "library", 40.0, t.text_dim); ui.add_space(8.0); - ui.label(egui::RichText::new("Local Libraries").size(14.0).color(t.text)); + ui.label(egui::RichText::new(tl!("Local Libraries")).size(14.0).color(t.text)); dim_label( ui, "Drag art, colors and text styles here to reuse them across documents. Libraries are stored on this machine — no account required.", @@ -425,8 +425,8 @@ pub(crate) fn empty_state(ui: &mut Ui, icon: &str, title: &str, body: &str) { ui.vertical_centered(|ui| { icons::icon(ui, icon, 28.0, t.text_disabled); ui.add_space(4.0); - ui.label(egui::RichText::new(title).size(12.5).color(t.text)); - ui.label(egui::RichText::new(body).size(11.5).color(t.text_dim)); + ui.label(egui::RichText::new(tl!(title)).size(12.5).color(t.text)); + ui.label(egui::RichText::new(tl!(body)).size(11.5).color(t.text_dim)); }); ui.add_space(12.0); } diff --git a/crates/ui-egui/src/panels/navigator.rs b/crates/ui-egui/src/panels/navigator.rs index 111a16944..921b48bfb 100644 --- a/crates/ui-egui/src/panels/navigator.rs +++ b/crates/ui-egui/src/panels/navigator.rs @@ -50,7 +50,7 @@ pub fn slider_to_zoom(t: f64) -> f64 { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { - super::empty_state(ui, "map", "No document", "Open a document to navigate it."); + super::empty_state(ui, "map", tl!("No document"), tl!("Open a document to navigate it.")); return; }; let artboard_only: bool = pstate(ui.ctx(), "nav-ab-only"); @@ -118,7 +118,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if let Some(v) = widgets::plain_field(ui, "nav-zoom", zoom, "%", 2, 64.0) { z = Some(v); } - if widgets::icon_button(ui, "dc-zoom-small", "Zoom Out", false, 22.0).clicked() { + if widgets::icon_button(ui, "dc-zoom-small", tl!("Zoom Out"), false, 22.0).clicked() { app.run("view.zoomOut", json!({})).ok(); } let sw = (ui.available_width() - 30.0).max(40.0); @@ -128,7 +128,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { { z = Some(slider_to_zoom(v as f64)); } - if widgets::icon_button(ui, "dc-zoom-large", "Zoom In", false, 22.0).clicked() { + if widgets::icon_button(ui, "dc-zoom-large", tl!("Zoom In"), false, 22.0).clicked() { app.run("view.zoomIn", json!({})).ok(); } if let Some(z) = z { @@ -139,10 +139,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { let ab: bool = pstate(ui.ctx(), "nav-ab-only"); - if menu_item(ui, "View Artboard Only", true, ab) { + if menu_item(ui, tl!("View Artboard Only"), true, ab) { set_pstate(ui.ctx(), "nav-ab-only", !ab); } - menu_item(ui, "Panel Options…", false, false); + menu_item(ui, tl!("Panel Options…"), false, false); } #[cfg(test)] diff --git a/crates/ui-egui/src/panels/opentype.rs b/crates/ui-egui/src/panels/opentype.rs index a73c2e9e8..9f737233f 100644 --- a/crates/ui-egui/src/panels/opentype.rs +++ b/crates/ui-egui/src/panels/opentype.rs @@ -29,13 +29,13 @@ fn set(app: &mut VectorcraftApp, f: OtFeatures) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let Some((st, _)) = text_style(app) else { - widgets::dim_label(ui, "Select text to set its OpenType features."); + widgets::dim_label(ui, tl!("Select text to set its OpenType features.")); return; }; let cur = OtFeatures::from_tags(st.features.iter().map(String::as_str)); let mut next = cur; ui.horizontal(|ui| { - widgets::dim_label(ui, "Figure:"); + widgets::dim_label(ui, tl!("Figure:")); let fig = match (cur.tabular_figures, cur.oldstyle_figures) { (false, false) => 0, (true, false) => 1, @@ -49,13 +49,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); widgets::divider(ui); let rows: [Toggle; 7] = [ - ("Standard Ligatures", |f| &mut f.ligatures), - ("Contextual Alternates", |f| &mut f.contextual), - ("Discretionary Ligatures", |f| &mut f.discretionary_ligatures), - ("Swash", |f| &mut f.swash), - ("Small Caps", |f| &mut f.small_caps), - ("Ordinals", |f| &mut f.ordinals), - ("Fractions", |f| &mut f.fractions), + (tl!("Standard Ligatures"), |f| &mut f.ligatures), + (tl!("Contextual Alternates"), |f| &mut f.contextual), + (tl!("Discretionary Ligatures"), |f| &mut f.discretionary_ligatures), + (tl!("Swash"), |f| &mut f.swash), + (tl!("Small Caps"), |f| &mut f.small_caps), + (tl!("Ordinals"), |f| &mut f.ordinals), + (tl!("Fractions"), |f| &mut f.fractions), ]; for (label, field) in rows { let on = *field(&mut next.clone()); @@ -64,7 +64,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } if st.tracking.abs() > 1e-9 && cur.ligatures { - widgets::dim_label(ui, "Ligatures are off while the text is tracked."); + widgets::dim_label(ui, tl!("Ligatures are off while the text is tracked.")); } if next != cur { set(app, next); @@ -72,7 +72,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { - if menu_item(ui, "Reset OpenType Features", text_style(app).is_some(), false) { + if menu_item(ui, tl!("Reset OpenType Features"), text_style(app).is_some(), false) { set(app, OtFeatures::default()); } } diff --git a/crates/ui-egui/src/panels/paragraph.rs b/crates/ui-egui/src/panels/paragraph.rs index c3097e99e..2257e43ef 100644 --- a/crates/ui-egui/src/panels/paragraph.rs +++ b/crates/ui-egui/src/panels/paragraph.rs @@ -36,7 +36,7 @@ fn para_cmd(app: &mut VectorcraftApp, cmd: &str, mut p: Value) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some((_, para)) = text_style(app) else { - super::empty_state(ui, "pilcrow", "No text selected", "Select a text object to edit its paragraph attributes."); + super::empty_state(ui, "pilcrow", tl!("No text selected"), tl!("Select a text object to edit its paragraph attributes.")); return; }; ui.horizontal(|ui| { @@ -55,34 +55,34 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_sized(vec2(22.0, 24.0), egui::Label::new(egui::RichText::new(s).size(11.5).strong().color(t.text))).on_hover_text(tip); }; egui::Grid::new("para-grid").num_columns(4).spacing([4.0, 4.0]).show(ui, |ui| { - label(ui, "→|", "Left Indent"); + label(ui, "→|", tl!("Left Indent")); if let Some(v) = widgets::spin_field(ui, "pa-li", Some(para.left_indent), unit, fw, 1.0, -1296.0, &[]) { format(app, json!({"leftIndent": v})); } - label(ui, "|←", "Right Indent"); + label(ui, "|←", tl!("Right Indent")); if let Some(v) = widgets::spin_field(ui, "pa-ri", Some(para.right_indent), unit, fw, 1.0, -1296.0, &[]) { format(app, json!({"rightIndent": v})); } ui.end_row(); - label(ui, "1→", "First-line Left Indent"); + label(ui, "1→", tl!("First-line Left Indent")); if let Some(v) = widgets::spin_field(ui, "pa-fi", Some(para.first_line_indent), unit, fw, 1.0, -1296.0, &[]) { format(app, json!({"firstLineIndent": v})); } ui.label(""); ui.label(""); ui.end_row(); - label(ui, "↑¶", "Space Before Paragraph"); + label(ui, "↑¶", tl!("Space Before Paragraph")); if let Some(v) = widgets::spin_field(ui, "pa-sb", Some(para.space_before), unit, fw, 1.0, 0.0, &[]) { format(app, json!({"spaceBefore": v})); } - label(ui, "↓¶", "Space After Paragraph"); + label(ui, "↓¶", tl!("Space After Paragraph")); if let Some(v) = widgets::spin_field(ui, "pa-sa", Some(para.space_after), unit, fw, 1.0, 0.0, &[]) { format(app, json!({"spaceAfter": v})); } ui.end_row(); }); ui.add_space(4.0); - if !pstate::(ui.ctx(), "pa-hide-options") && widgets::check(ui, "Hyphenate", para.hyphenate, true) { + if !pstate::(ui.ctx(), "pa-hide-options") && widgets::check(ui, tl!("Hyphenate"), para.hyphenate, true) { format(app, json!({"hyphenate": !para.hyphenate})); } } @@ -90,18 +90,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let has = text_style(app).is_some(); let hidden: bool = pstate(ui.ctx(), "pa-hide-options"); - if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), "pa-hide-options", !hidden); } ui.separator(); - for l in ["Roman Hanging Punctuation", "Justification…", "Hyphenation…"] { + for l in [tl!("Roman Hanging Punctuation"), tl!("Justification…"), tl!("Hyphenation…")] { menu_item(ui, l, false, false); } ui.separator(); - menu_item(ui, "Single-line Composer", false, false); - menu_item(ui, "Every-line Composer", false, true); + menu_item(ui, tl!("Single-line Composer"), false, false); + menu_item(ui, tl!("Every-line Composer"), false, true); ui.separator(); - if menu_item(ui, "Reset Panel", has, false) { + if menu_item(ui, tl!("Reset Panel"), has, false) { para_cmd(app, "text.setStyle", json!({"justify": "left"})); format(app, json!({"leftIndent": 0, "rightIndent": 0, "firstLineIndent": 0, "spaceBefore": 0, "spaceAfter": 0, "hyphenate": false})); } diff --git a/crates/ui-egui/src/panels/pathfinder.rs b/crates/ui-egui/src/panels/pathfinder.rs index eab9b1d72..7f25d5e15 100644 --- a/crates/ui-egui/src/panels/pathfinder.rs +++ b/crates/ui-egui/src/panels/pathfinder.rs @@ -32,7 +32,7 @@ fn run(app: &mut VectorcraftApp, ui: &Ui, op: &str, label: &str) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let n = selection_len(app); let t = crate::theme::Tokens::get(ui.ctx()); - widgets::subheader(ui, "Shape Modes:"); + widgets::subheader(ui, tl!("Shape Modes:")); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 5.0; for (icon, tip, op) in SHAPE_MODES { @@ -43,12 +43,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_enabled_ui(false, |ui| { let (r, _) = ui.allocate_exact_size(egui::vec2(ui.available_width().min(90.0), 26.0), egui::Sense::hover()); ui.painter().rect_filled(r, 2, t.hover.gamma_multiply(0.5)); - ui.painter().text(r.center(), egui::Align2::CENTER_CENTER, "Expand", egui::FontId::proportional(12.5), t.text_disabled); + ui.painter().text(r.center(), egui::Align2::CENTER_CENTER, tl!("Expand"), egui::FontId::proportional(12.5), t.text_disabled); }) .response - .on_disabled_hover_text("Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap"); + .on_disabled_hover_text(tl!("Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap")); }); - widgets::subheader(ui, "Pathfinders:"); + widgets::subheader(ui, tl!("Pathfinders:")); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 5.0; for (icon, tip, op) in PATHFINDERS { @@ -61,17 +61,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { - menu_item(ui, "Trap…", false, false); + menu_item(ui, tl!("Trap…"), false, false); let last: Option<(String, String)> = pstate(ui.ctx(), "pf-last"); - let label = last.as_ref().map(|(_, l)| format!("Repeat {l}")).unwrap_or_else(|| "Repeat Pathfinder".into()); + let label = + last.as_ref().map(|(_, l)| crate::i18n::fmt(tl!("Repeat {name}"), &[("name", tl!(l))])).unwrap_or_else(|| tl!("Repeat Pathfinder").into()); if menu_item(ui, &label, last.is_some() && selection_len(app) >= 1, false) && let Some((op, l)) = last { run(app, ui, &op, &l); } - menu_item(ui, "Pathfinder Options…", false, false); + menu_item(ui, tl!("Pathfinder Options…"), false, false); ui.separator(); - menu_item(ui, "Make Compound Shape", false, false); - menu_item(ui, "Release Compound Shape", false, false); - menu_item(ui, "Expand Compound Shape", false, false); + menu_item(ui, tl!("Make Compound Shape"), false, false); + menu_item(ui, tl!("Release Compound Shape"), false, false); + menu_item(ui, tl!("Expand Compound Shape"), false, false); } diff --git a/crates/ui-egui/src/panels/pattern_options.rs b/crates/ui-egui/src/panels/pattern_options.rs index 09a113882..cf33bc7a9 100644 --- a/crates/ui-egui/src/panels/pattern_options.rs +++ b/crates/ui-egui/src/panels/pattern_options.rs @@ -35,7 +35,7 @@ fn set(app: &mut VectorcraftApp, name: &str, mut p: Value) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if app.session.active().is_none() { - super::empty_state(ui, "swatch-book", "No document", "Open a document to edit its patterns."); + super::empty_state(ui, "swatch-book", tl!("No document"), tl!("Open a document to edit its patterns.")); return; } let Some((def, unit)) = editing(app) else { @@ -44,7 +44,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }; let name = def.name.clone(); // Name (commit on Enter / focus loss). - row(ui, "Name:", |ui| { + row(ui, tl!("Name:"), |ui| { let id = ui.id().with("po-name"); let mut buf: String = ui.data_mut(|d| d.get_temp::(id)).unwrap_or_else(|| name.clone()); if !ui.memory(|m| m.has_focus(id)) { @@ -57,7 +57,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); let tt_index = TileType::IDS.iter().position(|i| *i == def.tile_type.id()).unwrap_or(0); - row(ui, "Tile Type:", |ui| { + row(ui, tl!("Tile Type:"), |ui| { if let Some(i) = widgets::dropdown(ui, "po-tile", TILE_LABELS[tt_index], &TILE_LABELS, 150.0) { set(app, &name, json!({"tileType": TileType::IDS[i]})); } @@ -66,7 +66,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { TileType::BrickByRow { offset } | TileType::BrickByColumn { offset } => Some(offset), _ => None, }; - row(ui, "Brick Offset:", |ui| { + row(ui, tl!("Brick Offset:"), |ui| { ui.add_enabled_ui(offset.is_some(), |ui| { let cur = BRICK.iter().find(|(_, v)| offset.is_some_and(|o| (o - v).abs() < 1e-6)).map(|(l, _)| *l).unwrap_or("1/2"); let labels: Vec<&str> = BRICK.iter().map(|(l, _)| *l).collect(); @@ -76,38 +76,38 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); }); let sized = def.size_tile_to_art; - row(ui, "Width:", |ui| { + row(ui, tl!("Width:"), |ui| { ui.add_enabled_ui(!sized, |ui| { if let Some(v) = widgets::num_field(ui, "po-w", Some(def.tile.width()), unit, 90.0) { set(app, &name, json!({"width": v})); } }); }); - row(ui, "Height:", |ui| { + row(ui, tl!("Height:"), |ui| { ui.add_enabled_ui(!sized, |ui| { if let Some(v) = widgets::num_field(ui, "po-h", Some(def.tile.height()), unit, 90.0) { set(app, &name, json!({"height": v})); } }); }); - if widgets::check(ui, "Size Tile to Art", sized, true) { + if widgets::check(ui, tl!("Size Tile to Art"), sized, true) { set(app, &name, json!({"sizeTileToArt": !sized})); } - row(ui, "H Spacing:", |ui| { + row(ui, tl!("H Spacing:"), |ui| { ui.add_enabled_ui(sized, |ui| { if let Some(v) = widgets::num_field(ui, "po-hs", Some(def.h_spacing), unit, 90.0) { set(app, &name, json!({"hSpacing": v})); } }); }); - row(ui, "V Spacing:", |ui| { + row(ui, tl!("V Spacing:"), |ui| { ui.add_enabled_ui(sized, |ui| { if let Some(v) = widgets::num_field(ui, "po-vs", Some(def.v_spacing), unit, 90.0) { set(app, &name, json!({"vSpacing": v})); } }); }); - row(ui, "Overlap:", |ui| { + row(ui, tl!("Overlap:"), |ui| { let h = if def.overlap.right_in_front { "Right in Front" } else { "Left in Front" }; if let Some(i) = widgets::dropdown(ui, "po-oh", h, &["Left in Front", "Right in Front"], 110.0) { set(app, &name, json!({"overlap": {"h": if i == 1 { "right" } else { "left" }}})); @@ -120,35 +120,35 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); widgets::divider(ui); - row(ui, "Copies:", |ui| { + row(ui, tl!("Copies:"), |ui| { let cur = COPIES.iter().find(|(_, n)| *n == def.copies).map(|(l, _)| *l).unwrap_or("5 x 5"); let labels: Vec<&str> = COPIES.iter().map(|(l, _)| *l).collect(); if let Some(i) = widgets::dropdown(ui, "po-copies", cur, &labels, 80.0) { set(app, &name, json!({"copies": COPIES[i].1})); } }); - row(ui, "Dim Copies to:", |ui| { + row(ui, tl!("Dim Copies to:"), |ui| { if let Some(v) = widgets::plain_field(ui, "po-dim", def.dim_copies as f64, "%", 0, 60.0) { set(app, &name, json!({"dimCopies": v})); } }); - if widgets::check(ui, "Show Tile Edge", def.show_tile_edge, true) { + if widgets::check(ui, tl!("Show Tile Edge"), def.show_tile_edge, true) { set(app, &name, json!({"showTileEdge": !def.show_tile_edge})); } - if widgets::check(ui, "Show Swatch Bounds", def.show_swatch_bounds, true) { + if widgets::check(ui, tl!("Show Swatch Bounds"), def.show_swatch_bounds, true) { set(app, &name, json!({"showSwatchBounds": !def.show_swatch_bounds})); } widgets::divider(ui); ui.horizontal(|ui| { - if widgets::flat_button(ui, "Save a Copy", 90.0).clicked() + if widgets::flat_button(ui, tl!("Save a Copy"), 90.0).clicked() && let Err(e) = app.run("object.pattern.saveCopy", json!({})) { app.status(e); } - if widgets::flat_button(ui, "Done", 60.0).clicked() { + if widgets::flat_button(ui, tl!("Done"), 60.0).clicked() { app.run("object.pattern.done", json!({})).ok(); } - if widgets::flat_button(ui, "Cancel", 60.0).clicked() { + if widgets::flat_button(ui, tl!("Cancel"), 60.0).clicked() { app.run("object.pattern.cancel", json!({})).ok(); } }); @@ -157,10 +157,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { /// Not editing: the document's patterns, one to pick and edit. fn idle(app: &mut VectorcraftApp, ui: &mut Ui) { let names: Vec = app.session.active().map(|st| st.doc.patterns.iter().map(|p| p.name.clone()).collect()).unwrap_or_default(); - widgets::dim_label(ui, "Pattern options are available in pattern editing mode."); + widgets::dim_label(ui, tl!("Pattern options are available in pattern editing mode.")); ui.add_space(4.0); if names.is_empty() { - widgets::dim_label(ui, "Select art and choose Object › Pattern › Make."); + widgets::dim_label(ui, tl!("Select art and choose Object › Pattern › Make.")); return; } let mut chosen: String = pstate(ui.ctx(), "po-chosen"); @@ -172,7 +172,7 @@ fn idle(app: &mut VectorcraftApp, ui: &mut Ui) { if let Some(i) = widgets::dropdown(ui, "po-pick", &chosen, &labels, 150.0) { set_pstate(ui.ctx(), "po-chosen", names[i].clone()); } - if widgets::flat_button(ui, "Edit Pattern", 90.0).clicked() + if widgets::flat_button(ui, tl!("Edit Pattern"), 90.0).clicked() && let Err(e) = app.run("object.pattern.edit", json!({"name": chosen})) { app.status(e); @@ -182,13 +182,13 @@ fn idle(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let editing = editing(app).is_some(); - if menu_item(ui, "Save a Copy", editing, false) { + if menu_item(ui, tl!("Save a Copy"), editing, false) { app.run("object.pattern.saveCopy", json!({})).ok(); } - if menu_item(ui, "Done", editing, false) { + if menu_item(ui, tl!("Done"), editing, false) { app.run("object.pattern.done", json!({})).ok(); } - if menu_item(ui, "Cancel", editing, false) { + if menu_item(ui, tl!("Cancel"), editing, false) { app.run("object.pattern.cancel", json!({})).ok(); } } diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index c54b4f3b4..0b4953abd 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -12,15 +12,15 @@ use crate::{VectorcraftApp, icons}; pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { - dim_label(ui, "No document open"); + dim_label(ui, tl!("No document open")); return; }; let n_sel = st.selection.len(); let first = first_selected(app); let label = match (&first, n_sel) { - (None, _) => "Document".to_string(), - (_, n) if n > 1 => format!("{n} Objects"), - (Some(n), _) => n.kind_label().to_string(), + (None, _) => tl!("Document").to_string(), + (_, n) if n > 1 => crate::i18n::tn(n as u64, "{n} Object", "{n} Objects"), + (Some(n), _) => tl!(n.kind_label()).to_string(), }; ui.label(egui::RichText::new(label).size(11.5).color(t.text_dim)); ui.add_space(4.0); @@ -40,15 +40,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } appearance_section(app, ui); divider(ui); - section_header(ui, "Align"); + section_header(ui, tl!("Align")); ui.horizontal(|ui| { for (icon, tip, p) in [ - ("align-start-vertical", "Horizontal Align Left", json!({"horizontal": "left"})), - ("align-center-vertical", "Horizontal Align Center", json!({"horizontal": "center"})), - ("align-end-vertical", "Horizontal Align Right", json!({"horizontal": "right"})), - ("align-start-horizontal", "Vertical Align Top", json!({"vertical": "top"})), - ("align-center-horizontal", "Vertical Align Center", json!({"vertical": "center"})), - ("align-end-horizontal", "Vertical Align Bottom", json!({"vertical": "bottom"})), + ("align-start-vertical", tl!("Horizontal Align Left"), json!({"horizontal": "left"})), + ("align-center-vertical", tl!("Horizontal Align Center"), json!({"horizontal": "center"})), + ("align-end-vertical", tl!("Horizontal Align Right"), json!({"horizontal": "right"})), + ("align-start-horizontal", tl!("Vertical Align Top"), json!({"vertical": "top"})), + ("align-center-horizontal", tl!("Vertical Align Center"), json!({"vertical": "center"})), + ("align-end-horizontal", tl!("Vertical Align Bottom"), json!({"vertical": "bottom"})), ] { if widgets::icon_button(ui, icon, tip, false, 26.0).clicked() { let mut p = p; @@ -61,46 +61,46 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); if n_sel > 1 { divider(ui); - section_header(ui, "Pathfinder"); + section_header(ui, tl!("Pathfinder")); ui.horizontal(|ui| { for (icon, tip, op) in [ - ("squares-unite", "Unite", "unite"), - ("squares-subtract", "Minus Front", "minusFront"), - ("squares-intersect", "Intersect", "intersect"), - ("squares-exclude", "Exclude", "exclude"), + ("squares-unite", tl!("Unite"), "unite"), + ("squares-subtract", tl!("Minus Front"), "minusFront"), + ("squares-intersect", tl!("Intersect"), "intersect"), + ("squares-exclude", tl!("Exclude"), "exclude"), ] { if widgets::icon_button(ui, icon, tip, false, 28.0).clicked() { app.run(&format!("object.pathfinder.{op}"), json!({})).ok(); } } - if widgets::icon_button(ui, "ellipsis", "More Pathfinder options", false, 28.0).clicked() { + if widgets::icon_button(ui, "ellipsis", tl!("More Pathfinder options"), false, 28.0).clicked() { app.ui.open_panel = Some("pathfinder".into()); } }); } divider(ui); - section_header(ui, "Quick Actions"); + section_header(ui, tl!("Quick Actions")); let is_group = matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Group { .. })); let is_live = matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Path { live: Some(_), .. })); let mut actions: Vec<(&str, &str)> = vec![]; if n_sel > 1 { - actions.push(("Group", "object.group")); - actions.push(("Make Clipping Mask", "object.clippingMask.make")); + actions.push((tl!("Group"), "object.group")); + actions.push((tl!("Make Clipping Mask"), "object.clippingMask.make")); } if is_group { - actions.push(("Ungroup", "object.ungroup")); - actions.push(("Isolate Group", "object.isolate")); + actions.push((tl!("Ungroup"), "object.ungroup")); + actions.push((tl!("Isolate Group"), "object.isolate")); } if is_live { - actions.push(("Expand Shape", "object.expandShape")); + actions.push((tl!("Expand Shape"), "object.expandShape")); } if multi_color(app, ui.ctx()) { - actions.push(("Recolor", "ui.recolorDialog")); + actions.push((tl!("Recolor"), "ui.recolorDialog")); } - actions.push(("Offset Path", "object.path.offsetPath")); - actions.push(("Simplify", "object.path.simplify")); - actions.push(("Arrange: Bring to Front", "object.arrange.bringToFront")); - actions.push(("Lock", "object.lock")); + actions.push((tl!("Offset Path"), "object.path.offsetPath")); + actions.push((tl!("Simplify"), "object.path.simplify")); + actions.push((tl!("Arrange: Bring to Front"), "object.arrange.bringToFront")); + actions.push((tl!("Lock"), "object.lock")); let w = (ui.available_width() - 6.0) / 2.0; for pair in actions.chunks(2) { ui.horizontal(|ui| { @@ -120,7 +120,7 @@ fn image_section(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(i) = crate::place::selected_image_info(app) else { return }; let (name, details) = crate::place::image_summary(i); let (Some(id), linked) = (i["id"].as_u64(), i["linked"] == true) else { return }; - section_header(ui, if linked { "Linked File" } else { "Embedded Image" }); + section_header(ui, if linked { tl!("Linked File") } else { tl!("Embedded Image") }); ui.add(egui::Label::new(egui::RichText::new(&name).size(12.0).color(t.text_strong)).truncate()); dim_label(ui, &details); ui.add_space(4.0); @@ -128,26 +128,26 @@ fn image_section(app: &mut VectorcraftApp, ui: &mut Ui) { let links = crate::panels::links::ID; ui.horizontal(|ui| { if linked { - if widgets::flat_button(ui, "Embed", w).clicked() { + if widgets::flat_button(ui, tl!("Embed"), w).clicked() { app.run("links.embed", json!({ "ids": [id] })).ok(); } - if widgets::flat_button(ui, "Edit Original", w).clicked() { + if widgets::flat_button(ui, tl!("Edit Original"), w).clicked() { crate::menus::invoke(app, "links.editOriginal", json!({ "id": id })); } } else { - if widgets::flat_button(ui, "Unembed…", w).clicked() { + if widgets::flat_button(ui, tl!("Unembed…"), w).clicked() { crate::panels::links::unembed(app, id, &name); } - if widgets::flat_button(ui, "Image Trace", w).clicked() { + if widgets::flat_button(ui, tl!("Image Trace"), w).clicked() { app.ui.open_panel = Some("imageTrace".into()); } } }); ui.horizontal(|ui| { - if app.services.pick_open.is_some() && widgets::flat_button(ui, "Relink…", w).clicked() { + if app.services.pick_open.is_some() && widgets::flat_button(ui, tl!("Relink…"), w).clicked() { crate::panels::links::relink(app, vec![id]); } - if widgets::flat_button(ui, "Links", w).clicked() { + if widgets::flat_button(ui, tl!("Links"), w).clicked() { app.ui.open_panel = Some(links.into()); } }); @@ -171,9 +171,9 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool { fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) { let units = app.session.general_unit(); - section_header(ui, "Document"); + section_header(ui, tl!("Document")); ui.horizontal(|ui| { - dim_label(ui, "Units"); + dim_label(ui, tl!("Units")); let labels: Vec<&str> = Unit::ALL.iter().map(|u| u.label()).collect(); if let Some(i) = widgets::dropdown(ui, "units", units.label(), &labels, 140.0) { app.run("document.setUnits", json!({"units": labels[i]})).ok(); @@ -181,69 +181,69 @@ fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) { }); let w = (ui.available_width() - 6.0) / 2.0; ui.horizontal(|ui| { - if widgets::flat_button(ui, "Document Setup", w).clicked() { + if widgets::flat_button(ui, tl!("Document Setup"), w).clicked() { app.run("file.documentSetup", json!({})).ok(); } - if widgets::flat_button(ui, "Edit Artboards", w).clicked() { + if widgets::flat_button(ui, tl!("Edit Artboards"), w).clicked() { app.select_tool("artboard"); } }); divider(ui); - section_header(ui, "Rulers & Grids"); + section_header(ui, tl!("Rulers & Grids")); ui.horizontal(|ui| { let v = app.ui.view.clone(); - if widgets::icon_button(ui, "ruler", "Show Rulers (⌘R)", v.rulers, 26.0).clicked() { + if widgets::icon_button(ui, "ruler", tl!("Show Rulers (⌘R)"), v.rulers, 26.0).clicked() { app.run("view.rulers", json!({})).ok(); } - if widgets::icon_button(ui, "grid-3x3", "Show Grid (⌘')", v.grid, 26.0).clicked() { + if widgets::icon_button(ui, "grid-3x3", tl!("Show Grid (⌘')"), v.grid, 26.0).clicked() { app.run("view.grid", json!({})).ok(); } let transparency_grid = app.session.active().is_some_and(|d| d.transparency_grid); - if widgets::icon_button(ui, "square-dashed", "Show Transparency Grid", transparency_grid, 26.0).clicked() { + if widgets::icon_button(ui, "square-dashed", tl!("Show Transparency Grid"), transparency_grid, 26.0).clicked() { app.run("view.transparencyGrid", json!({})).ok(); } }); divider(ui); - section_header(ui, "Guides"); + section_header(ui, tl!("Guides")); ui.horizontal(|ui| { let v = app.ui.view.clone(); - if widgets::icon_button(ui, "layout-grid", "Show Guides", v.guides, 26.0).clicked() { + if widgets::icon_button(ui, "layout-grid", tl!("Show Guides"), v.guides, 26.0).clicked() { app.run("view.guides", json!({})).ok(); } - if widgets::icon_button(ui, "sparkles", "Smart Guides (⌘U)", v.smart_guides, 26.0).clicked() { + if widgets::icon_button(ui, "sparkles", tl!("Smart Guides (⌘U)"), v.smart_guides, 26.0).clicked() { app.run("view.smartGuides", json!({})).ok(); } }); divider(ui); - section_header(ui, "Snap Options"); + section_header(ui, tl!("Snap Options")); let mut sp = app.ui.view.snap_to_point; - if ui.checkbox(&mut sp, "Snap to Point").changed() { + if ui.checkbox(&mut sp, tl!("Snap to Point")).changed() { app.ui.view.snap_to_point = sp; } let mut sg = app.ui.view.snap_to_grid; - if ui.checkbox(&mut sg, "Snap to Grid").changed() { + if ui.checkbox(&mut sg, tl!("Snap to Grid")).changed() { app.ui.view.snap_to_grid = sg; } divider(ui); - section_header(ui, "Preferences"); + section_header(ui, tl!("Preferences")); ui.horizontal(|ui| { - dim_label(ui, "Keyboard Increment"); + dim_label(ui, tl!("Keyboard Increment")); if let Some(v) = widgets::num_field(ui, "kbinc", Some(app.session.prefs.keyboard_increment), units, 80.0) { app.session.prefs.keyboard_increment = v.max(0.001); } }); let mut ss = app.session.prefs.scale_strokes; - if ui.checkbox(&mut ss, "Scale Strokes & Effects").changed() { + if ui.checkbox(&mut ss, tl!("Scale Strokes & Effects")).changed() { super::transform::set_pref(app, "scaleStrokes", ss); } divider(ui); - section_header(ui, "Quick Actions"); + section_header(ui, tl!("Quick Actions")); let w = (ui.available_width() - 6.0) / 2.0; ui.horizontal(|ui| { - if widgets::flat_button(ui, "Document Setup", w).clicked() { + if widgets::flat_button(ui, tl!("Document Setup"), w).clicked() { app.run("file.documentSetup", json!({})).ok(); } - if widgets::flat_button(ui, "Preferences", w).clicked() { + if widgets::flat_button(ui, tl!("Preferences"), w).clicked() { app.run("edit.preferences", json!({})).ok(); } }); @@ -256,13 +256,13 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) { let units = app.session.general_unit(); // The bounding box, rotated with rotated objects (as in the Transform panel). let Some(bx) = app.selection_box() else { - dim_label(ui, "No Selection"); + dim_label(ui, tl!("No Selection")); return; }; let b = bx.rect; let refi: usize = ui.data(|d| d.get_temp(egui::Id::new("refpt"))).unwrap_or(4); let rp = bx.reference_point(refi); - section_header(ui, "Transform"); + section_header(ui, tl!("Transform")); ui.horizontal(|ui| { if let Some(i) = widgets::reference_point(ui, refi) { ui.data_mut(|d| d.insert_temp(egui::Id::new("refpt"), i)); @@ -300,10 +300,10 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) { app.run("object.rotate", json!({"angle": a, "absolute": true})).ok(); } ui.add_space(10.0); - if widgets::icon_button(ui, "flip-horizontal-2", "Flip Along Horizontal Axis", false, 24.0).clicked() { + if widgets::icon_button(ui, "flip-horizontal-2", tl!("Flip Along Horizontal Axis"), false, 24.0).clicked() { app.run("object.reflect", json!({"axis": "vertical"})).ok(); } - if widgets::icon_button(ui, "flip-vertical-2", "Flip Along Vertical Axis", false, 24.0).clicked() { + if widgets::icon_button(ui, "flip-vertical-2", tl!("Flip Along Vertical Axis"), false, 24.0).clicked() { app.run("object.reflect", json!({"axis": "horizontal"})).ok(); } }); @@ -314,7 +314,7 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) { match live { vectorcraft_doc::LiveShape::Rectangle { radii, .. } => { ui.horizontal(|ui| { - dim_label(ui, "Corner Radius:"); + dim_label(ui, tl!("Corner Radius:")); if let Some(r) = widgets::num_field(ui, "radius", Some(radii[0]), units, 80.0) { app.run("object.setLiveShape", json!({"radius": r})).ok(); } @@ -322,7 +322,7 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) { } vectorcraft_doc::LiveShape::Polygon { sides, .. } => { ui.horizontal(|ui| { - dim_label(ui, "Sides:"); + dim_label(ui, tl!("Sides:")); if let Some(s) = widgets::plain_field(ui, "sides", *sides as f64, "", 0, 60.0) { app.run("object.setLiveShape", json!({"sides": s as u32})).ok(); } @@ -335,9 +335,9 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) { fn appearance_section(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(n) = first_selected(app) else { return }; - section_header(ui, "Appearance"); + section_header(ui, tl!("Appearance")); let weight = super::stroke::shown_weight(app, super::current_stroke(app).as_ref(), &super::stroke_mixed(app, ui.ctx())); - for (label, is_fill) in [("Fill", true), ("Stroke", false)] { + for (label, is_fill) in [(tl!("Fill"), true), (tl!("Stroke"), false)] { ui.horizontal(|ui| { super::paint_chip(app, ui, !is_fill, 22.0, false); ui.label(egui::RichText::new(label).size(12.0)); @@ -346,26 +346,26 @@ fn appearance_section(app: &mut VectorcraftApp, ui: &mut Ui) { if let Some(w) = widgets::num_field(ui, "ap-w", weight, app.session.stroke_unit(), 70.0) { app.run("stroke.set", json!({"weight": w})).ok(); } - let more = widgets::icon_button(ui, "ellipsis", "Stroke options", false, 22.0); + let more = widgets::icon_button(ui, "ellipsis", tl!("Stroke options"), false, 22.0); super::stroke::popover(app, &more); } }); } ui.horizontal(|ui| { - ui.label(egui::RichText::new("Opacity").size(12.0)); + ui.label(egui::RichText::new(tl!("Opacity")).size(12.0)); ui.add_space(8.0); if let Some(o) = widgets::plain_field(ui, "ap-op", n.opacity as f64 * 100.0, "%", 0, 64.0) { app.run("object.setProps", json!({"opacity": o.clamp(0.0, 100.0)})).ok(); } - if widgets::icon_button(ui, "ellipsis", "Transparency", false, 22.0).clicked() { + if widgets::icon_button(ui, "ellipsis", tl!("Transparency"), false, 22.0).clicked() { app.ui.open_panel = Some("transparency".into()); } }); ui.horizontal(|ui| { // The fx button opens the effect menu, as the Appearance panel's does. - let r = widgets::flat_button(ui, "fx", 34.0).on_hover_text("Add New Effect"); + let r = widgets::flat_button(ui, "fx", 34.0).on_hover_text(tl!("Add New Effect")); egui::Popup::menu(&r).show(|ui| super::appearance::fx_menu(app, ui)); - if widgets::icon_button(ui, "ellipsis", "Appearance panel", false, 22.0).clicked() { + if widgets::icon_button(ui, "ellipsis", tl!("Appearance panel"), false, 22.0).clicked() { app.ui.open_panel = Some("appearance".into()); } }); @@ -373,36 +373,36 @@ fn appearance_section(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(n) = first_selected(app) else { - dim_label(ui, "Select a text object"); + dim_label(ui, tl!("Select a text object")); return; }; let NodeKind::Text(tx) = &n.kind else { - dim_label(ui, "Select a text object"); + dim_label(ui, tl!("Select a text object")); return; }; let s = tx.first_style(); - section_header(ui, "Character"); + section_header(ui, tl!("Character")); if let Some(f) = widgets::font_dropdown(ui, "font", &s.font_family, ui.available_width() - 4.0) { app.run("text.setStyle", json!({ "font": f })).ok(); } ui.horizontal(|ui| { - dim_label(ui, "Size"); + dim_label(ui, tl!("Size")); let type_unit = app.session.type_unit(); if let Some(v) = widgets::num_field(ui, "fsize", Some(s.size), type_unit, 70.0) { app.run("text.setStyle", json!({"size": v})).ok(); } - dim_label(ui, "Leading"); + dim_label(ui, tl!("Leading")); if let Some(v) = widgets::num_field(ui, "lead", Some(s.effective_leading()), type_unit, 70.0) { app.run("text.setStyle", json!({"leading": v})).ok(); } }); ui.horizontal(|ui| { - dim_label(ui, "Tracking"); + dim_label(ui, tl!("Tracking")); if let Some(v) = widgets::plain_field(ui, "track", s.tracking, "", 0, 60.0) { app.run("text.setStyle", json!({"tracking": v})).ok(); } }); - section_header(ui, "Paragraph"); + section_header(ui, tl!("Paragraph")); ui.horizontal(|ui| { for (icon, j) in [("align-start-vertical", "left"), ("align-center-vertical", "center"), ("align-end-vertical", "right")] { if widgets::icon_button(ui, icon, j, false, 24.0).clicked() { diff --git a/crates/ui-egui/src/panels/separations.rs b/crates/ui-egui/src/panels/separations.rs index d7d484df8..39c80ea2d 100644 --- a/crates/ui-egui/src/panels/separations.rs +++ b/crates/ui-egui/src/panels/separations.rs @@ -31,11 +31,11 @@ fn plates_section(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let v = proof::view(); let on = v.separations.is_some(); - if widgets::check(ui, "Overprint Preview (separations)", on, app.session.active().is_some()) { + if widgets::check(ui, tl!("Overprint Preview (separations)"), on, app.session.active().is_some()) { run(app, "view.separationsPreview", json!({"on": !on})); } let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { - dim_label(ui, "No document open."); + dim_label(ui, tl!("No document open.")); return; }; let plates = proof::plates(&doc); @@ -46,7 +46,7 @@ fn plates_section(app: &mut VectorcraftApp, ui: &mut Ui) { // CMYK composite row. ui.horizontal(|ui| { ui.add_enabled_ui(on, |ui| { - if widgets::toggle_icon(ui, "eye", all_on, 18.0, "Show all plates") && on { + if widgets::toggle_icon(ui, "eye", all_on, 18.0, tl!("Show all plates")) && on { run(app, "view.separationsPreview", json!({"plates": plates.iter().map(|p| p.name.clone()).collect::>()})); } }); @@ -57,7 +57,7 @@ fn plates_section(app: &mut VectorcraftApp, ui: &mut Ui) { let vis = visible(&p.name); let mut clicked = false; ui.add_enabled_ui(on, |ui| { - clicked = widgets::toggle_icon(ui, "eye", vis, 18.0, "Show/hide plate (Alt-click: only this plate)"); + clicked = widgets::toggle_icon(ui, "eye", vis, 18.0, tl!("Show/hide plate (Alt-click: only this plate)")); }); if clicked && on { let key = if alt { "only" } else { "toggle" }; @@ -66,7 +66,7 @@ fn plates_section(app: &mut VectorcraftApp, ui: &mut Ui) { let (r, _) = ui.allocate_exact_size(vec2(14.0, 14.0), Sense::hover()); let q = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8; ui.painter().rect_filled(r, 2.0, egui::Color32::from_rgb(q(p.rgb[0]), q(p.rgb[1]), q(p.rgb[2]))); - let label = if p.spot { format!("{} (spot)", p.name) } else { p.name.clone() }; + let label = if p.spot { crate::i18n::fmt(tl!("{name} (spot)"), &[("name", &p.name)]) } else { p.name.clone() }; ui.label(egui::RichText::new(label).size(12.0).color(if on { t.text } else { t.text_dim })); }); } @@ -82,7 +82,7 @@ fn profile_dropdown(ui: &mut Ui, id: &str, current: &str, kind: ProfileKind, ext fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) { let st = cms::active_settings(); - widgets::section_header(ui, "Color Settings"); + widgets::section_header(ui, tl!("Color Settings")); ui.horizontal(|ui| { ui.label("RGB:"); if let Some(n) = profile_dropdown(ui, "cms-rgb", &st.rgb, ProfileKind::Rgb, None) { @@ -96,17 +96,17 @@ fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) { } }); ui.horizontal(|ui| { - ui.label("Intent:"); + ui.label(tl!("Intent:")); let labels: Vec<&str> = Intent::ALL.iter().map(|i| i.label()).collect(); if let Some(i) = widgets::dropdown(ui, "cms-intent", st.intent.label(), &labels, 170.0) { run(app, "edit.colorSettings", json!({"intent": Intent::ALL[i].id()})); } }); - if widgets::check(ui, "Use Black Point Compensation", st.bpc, true) { + if widgets::check(ui, tl!("Use Black Point Compensation"), st.bpc, true) { run(app, "edit.colorSettings", json!({"bpc": !st.bpc})); } ui.horizontal(|ui| { - if widgets::flat_button(ui, "Load Profile…", 110.0).clicked() + if widgets::flat_button(ui, tl!("Load Profile…"), 110.0).clicked() && let Some(path) = app .services .pick_open @@ -115,7 +115,7 @@ fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) { { run(app, "color.loadProfile", json!({ "path": path })); } - if widgets::flat_button(ui, "Gamut Check", 100.0).clicked() { + if widgets::flat_button(ui, tl!("Gamut Check"), 100.0).clicked() { match app.run("color.gamutCheck", json!({})) { Ok(r) => { let n = r["outOfGamut"].as_array().map_or(0, Vec::len); @@ -130,7 +130,7 @@ fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) { let (_, cmyk) = vectorcraft_engine::cmd::colormgmt::doc_profiles(&d.doc); const WORKING: &str = "Working CMYK (don't tag)"; ui.horizontal(|ui| { - ui.label("Assign:"); + ui.label(tl!("Assign:")); if let Some(n) = profile_dropdown(ui, "cms-assign", cmyk.as_deref().unwrap_or(WORKING), ProfileKind::Cmyk, Some(WORKING)) { let v = if n == WORKING { Value::Null } else { Value::String(n) }; run(app, "edit.assignProfile", json!({ "cmyk": v })); @@ -141,7 +141,7 @@ fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) { fn proof_section(app: &mut VectorcraftApp, ui: &mut Ui) { let v = proof::view(); - widgets::section_header(ui, "Proof Setup"); + widgets::section_header(ui, tl!("Proof Setup")); let cur = v.setup.target.id(); let label = PROOF_TARGETS.iter().find(|(id, _)| *id == cur).map_or(cur.as_str(), |(_, l)| l); let labels: Vec<&str> = PROOF_TARGETS.iter().map(|(_, l)| *l).collect(); @@ -149,14 +149,14 @@ fn proof_section(app: &mut VectorcraftApp, ui: &mut Ui) { run(app, "view.proofSetup", json!({"target": PROOF_TARGETS[i].0})); } ui.horizontal(|ui| { - if widgets::check(ui, "Proof Colors", v.proof_colors, true) { + if widgets::check(ui, tl!("Proof Colors"), v.proof_colors, true) { run(app, "view.proofColors", json!({"on": !v.proof_colors})); } - if widgets::check(ui, "Simulate Paper", v.setup.simulate_paper, v.setup.target.is_cmyk()) { + if widgets::check(ui, tl!("Simulate Paper"), v.setup.simulate_paper, v.setup.target.is_cmyk()) { run(app, "view.proofSetup", json!({"simulatePaper": !v.setup.simulate_paper})); } }); - if widgets::check(ui, "Overprint Preview", v.overprint, true) { + if widgets::check(ui, tl!("Overprint Preview"), v.overprint, true) { run(app, "view.overprintPreview", json!({"on": !v.overprint})); } } @@ -171,13 +171,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let v = proof::view(); - if menu_item(ui, "Overprint Preview", true, v.overprint) { + if menu_item(ui, tl!("Overprint Preview"), true, v.overprint) { run(app, "view.overprintPreview", json!({})); } - if menu_item(ui, "Proof Colors", true, v.proof_colors) { + if menu_item(ui, tl!("Proof Colors"), true, v.proof_colors) { run(app, "view.proofColors", json!({})); } - if menu_item(ui, "Show All Plates", v.separations.is_some(), false) { + if menu_item(ui, tl!("Show All Plates"), v.separations.is_some(), false) { run(app, "view.separationsPreview", json!({"on": false})); run(app, "view.separationsPreview", json!({"on": true})); } diff --git a/crates/ui-egui/src/panels/stroke.rs b/crates/ui-egui/src/panels/stroke.rs index fcfa29e3b..63e887fba 100644 --- a/crates/ui-egui/src/panels/stroke.rs +++ b/crates/ui-egui/src/panels/stroke.rs @@ -113,7 +113,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_sized(vec2(label_w, 24.0), egui::Label::new(egui::RichText::new(s).size(12.5).color(t.text)).halign(egui::Align::RIGHT)); }; ui.horizontal(|ui| { - row_label(ui, "Weight:"); + row_label(ui, tl!("Weight:")); weight_field(app, ui, "stroke-weight", weight, 120.0); }); if hidden { @@ -124,11 +124,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let join = st.as_ref().map(|s| s.join).filter(|_| !mixed.join); let align = st.as_ref().map(|s| s.align).filter(|_| !mixed.align); ui.horizontal(|ui| { - row_label(ui, "Cap:"); + row_label(ui, tl!("Cap:")); for (v, icon, tip, name) in [ - (LineCap::Butt, "dc-cap-butt", "Butt Cap", "butt"), - (LineCap::Round, "dc-cap-round", "Round Cap", "round"), - (LineCap::Square, "dc-cap-square", "Projecting Cap", "square"), + (LineCap::Butt, "dc-cap-butt", tl!("Butt Cap"), "butt"), + (LineCap::Round, "dc-cap-round", tl!("Round Cap"), "round"), + (LineCap::Square, "dc-cap-square", tl!("Projecting Cap"), "square"), ] { if widgets::icon_button(ui, icon, tip, cap == Some(v), 24.0).clicked() { set(app, json!({"cap": name})); @@ -136,17 +136,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); ui.horizontal(|ui| { - row_label(ui, "Corner:"); + row_label(ui, tl!("Corner:")); for (v, icon, tip, name) in [ - (LineJoin::Miter, "dc-join-miter", "Miter Join", "miter"), - (LineJoin::Round, "dc-join-round", "Round Join", "round"), - (LineJoin::Bevel, "dc-join-bevel", "Bevel Join", "bevel"), + (LineJoin::Miter, "dc-join-miter", tl!("Miter Join"), "miter"), + (LineJoin::Round, "dc-join-round", tl!("Round Join"), "round"), + (LineJoin::Bevel, "dc-join-bevel", tl!("Bevel Join"), "bevel"), ] { if widgets::icon_button(ui, icon, tip, join == Some(v), 24.0).clicked() { set(app, json!({"join": name})); } } - widgets::dim_label(ui, "Limit:"); + widgets::dim_label(ui, tl!("Limit:")); let lim = st.as_ref().map(|s| s.miter_limit).unwrap_or(10.0); if !matches!(join, Some(LineJoin::Round | LineJoin::Bevel)) { if let Some(v) = widgets::mixed_field(ui, "stroke-limit", (!mixed.miter_limit).then_some(lim), " x", 0, 50.0) { @@ -157,11 +157,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); ui.horizontal(|ui| { - row_label(ui, "Align Stroke:"); + row_label(ui, tl!("Align Stroke:")); for (v, icon, tip, name) in [ - (StrokeAlign::Center, "dc-stroke-center", "Align Stroke to Center", "center"), - (StrokeAlign::Inside, "dc-stroke-inside", "Align Stroke to Inside", "inside"), - (StrokeAlign::Outside, "dc-stroke-outside", "Align Stroke to Outside", "outside"), + (StrokeAlign::Center, "dc-stroke-center", tl!("Align Stroke to Center"), "center"), + (StrokeAlign::Inside, "dc-stroke-inside", tl!("Align Stroke to Inside"), "inside"), + (StrokeAlign::Outside, "dc-stroke-outside", tl!("Align Stroke to Outside"), "outside"), ] { // Only closed paths take a stroke inside or outside (not open paths, not type). let enabled = v == StrokeAlign::Center || mixed.can_align; @@ -176,7 +176,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let (fields, align_corners) = dash_state(dash, pstate(ui.ctx(), "stroke-dash-last")); let shown = if mixed.dash { [None; 6] } else { fields }; ui.horizontal(|ui| { - if widgets::check(ui, "Dashed Line", dash.is_some(), st.is_some()) { + if widgets::check(ui, tl!("Dashed Line"), dash.is_some(), st.is_some()) { if dash.is_some() { set_pstate(ui.ctx(), "stroke-dash-last", Some((fields, align_corners))); set(app, json!({"dash": null})); @@ -186,10 +186,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.add_space((ui.available_width() - 56.0).max(0.0)); let on = dash.is_some(); - if widgets::icon_button_enabled(ui, "dc-dash-exact", "Exact dash lengths", on && !align_corners, on, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-dash-exact", tl!("Exact dash lengths"), on && !align_corners, on, 24.0).clicked() { set(app, json!({"alignDashes": false})); } - if widgets::icon_button_enabled(ui, "dc-dash-align", "Fit dashes to corners and ends", on && align_corners, on, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-dash-align", tl!("Fit dashes to corners and ends"), on && align_corners, on, 24.0).clicked() { set(app, json!({"alignDashes": true})); } }); @@ -213,7 +213,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 3.0; let fw = ((ui.available_width() - 15.0) / 6.0).clamp(28.0, 40.0); - for lbl in ["dash", "gap", "dash", "gap", "dash", "gap"] { + for lbl in [tl!("dash"), tl!("gap"), tl!("dash"), tl!("gap"), tl!("dash"), tl!("gap")] { ui.add_sized(vec2(fw, 12.0), egui::Label::new(egui::RichText::new(lbl).size(10.5).color(t.text_dim))); } }); @@ -221,14 +221,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // Arrowheads. let (sa, ea) = (st.as_ref().and_then(|s| s.start_arrow), st.as_ref().and_then(|s| s.end_arrow)); ui.horizontal(|ui| { - row_label(ui, "Arrowheads:"); + row_label(ui, tl!("Arrowheads:")); if let Some(a) = arrow_dropdown(ui, "arrow-start", sa, true) { set(app, json!({"startArrow": a.map(|a| format!("{a:?}"))})); } if let Some(a) = arrow_dropdown(ui, "arrow-end", ea, false) { set(app, json!({"endArrow": a.map(|a| format!("{a:?}"))})); } - if widgets::icon_button_enabled(ui, "arrow-left-right", "Swap start and end arrowheads", false, st.is_some(), 22.0).clicked() { + if widgets::icon_button_enabled(ui, "arrow-left-right", tl!("Swap start and end arrowheads"), false, st.is_some(), 22.0).clicked() { app.run("stroke.setAdvanced", json!({"swapArrows": true})).ok(); } }); @@ -237,7 +237,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // Scale and Align only mean something for a stroke with a head. let (arrow_align, has_head) = arrow_align_state(st.as_ref()); ui.horizontal(|ui| { - row_label(ui, "Scale:"); + row_label(ui, tl!("Scale:")); let mut ns = None; ui.add_enabled_ui(has_head, |ui| { if let Some(v) = widgets::plain_field(ui, "arrow-scale-s", scale.0, "%", 0, 56.0) { @@ -247,7 +247,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ns = Some((if linked { v } else { scale.0 }, v)); } }); - if widgets::icon_button(ui, if linked { "link" } else { "link-2-off" }, "Link start and end arrowhead scales", linked, 22.0).clicked() { + if widgets::icon_button(ui, if linked { "link" } else { "link-2-off" }, tl!("Link start and end arrowhead scales"), linked, 22.0).clicked() { set_pstate(ui.ctx(), "arrow-scale-link", !linked); } if let Some((a, b)) = ns { @@ -255,7 +255,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); ui.horizontal(|ui| { - row_label(ui, "Align:"); + row_label(ui, tl!("Align:")); for (v, name, icon, tip) in ARROW_ALIGN { if widgets::icon_button_enabled(ui, icon, tip, arrow_align == v, has_head, 22.0).clicked() { set(app, json!({"arrowAlign": name})); @@ -265,15 +265,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { widgets::divider(ui); // Profile. ui.horizontal(|ui| { - row_label(ui, "Profile:"); + row_label(ui, tl!("Profile:")); if let Some(id) = profile_dropdown(app, ui, st.as_ref().and_then(|s| s.profile.as_ref())) { set(app, json!({"profile": id})); } let can_flip = st.as_ref().is_some_and(|s| s.profile.is_some()); - if widgets::icon_button_enabled(ui, "flip-horizontal-2", "Flip Along", false, can_flip, 22.0).clicked() { + if widgets::icon_button_enabled(ui, "flip-horizontal-2", tl!("Flip Along"), false, can_flip, 22.0).clicked() { app.run("stroke.setAdvanced", json!({"flipProfile": "along"})).ok(); } - if widgets::icon_button_enabled(ui, "flip-vertical-2", "Flip Across", false, can_flip, 22.0).clicked() { + if widgets::icon_button_enabled(ui, "flip-vertical-2", tl!("Flip Across"), false, can_flip, 22.0).clicked() { app.run("stroke.setAdvanced", json!({"flipProfile": "across"})).ok(); } }); @@ -318,7 +318,7 @@ fn arrow_dropdown(ui: &mut Ui, id: &str, cur: Option, start: bool) -> let body = Rect::from_min_max(r.min + vec2(3.0, 0.0), pos2(r.right() - 16.0, r.bottom())); paint_arrow(ui, body, cur, start, t.text_strong); icons::paint(ui, "chevron-down", Rect::from_center_size(pos2(r.right() - 8.0, r.center().y), vec2(10.0, 10.0)), t.icon); - let resp = resp.on_hover_text(if start { "Start arrowhead" } else { "End arrowhead" }); + let resp = resp.on_hover_text(if start { tl!("Start arrowhead") } else { tl!("End arrowhead") }); let mut out = None; egui::Popup::menu(&resp).id(egui::Id::new(("arrow-pop", id))).show(|ui| { ui.set_min_width(170.0); @@ -334,7 +334,7 @@ fn arrow_dropdown(ui: &mut Ui, id: &str, cur: Option, start: bool) -> ui.painter().text( row.left_center() + vec2(66.0, 0.0), egui::Align2::LEFT_CENTER, - arrow_label(a), + tl!(arrow_label(a)), egui::FontId::proportional(11.5), t.text, ); @@ -385,7 +385,7 @@ pub(crate) fn profile_dropdown(app: &VectorcraftApp, ui: &mut Ui, cur: Option<&W let body = Rect::from_min_max(r.min + vec2(6.0, 5.0), pos2(r.right() - 20.0, r.bottom() - 5.0)); paint_profile(ui, body, cur.and_then(|p| silhouette(&p.points)), t.text_strong); icons::paint(ui, "chevron-down", Rect::from_center_size(pos2(r.right() - 9.0, r.center().y), vec2(10.0, 10.0)), t.icon); - let resp = resp.on_hover_text("Variable Width Profile"); + let resp = resp.on_hover_text(tl!("Variable Width Profile")); let mut out = None; egui::Popup::menu(&resp).show(|ui| { egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| { @@ -402,7 +402,13 @@ pub(crate) fn profile_dropdown(app: &VectorcraftApp, ui: &mut Ui, cur: Option<&W ui.painter().rect_filled(row, 0.0, t.hover); } paint_profile(ui, Rect::from_min_size(row.min + vec2(6.0, 6.0), vec2(70.0, 14.0)), silhouette(e.points), t.text_strong); - ui.painter().text(row.left_center() + vec2(84.0, 0.0), egui::Align2::LEFT_CENTER, e.label, egui::FontId::proportional(11.5), t.text); + ui.painter().text( + row.left_center() + vec2(84.0, 0.0), + egui::Align2::LEFT_CENTER, + tl!(e.label), + egui::FontId::proportional(11.5), + t.text, + ); if rr.clicked() { out = Some(e.id.to_string()); ui.close(); @@ -415,11 +421,11 @@ pub(crate) fn profile_dropdown(app: &VectorcraftApp, ui: &mut Ui, cur: Option<&W pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "stroke-hide-options"); - if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), "stroke-hide-options", !hidden); } ui.separator(); - if menu_item(ui, "Add to Profiles…", crate::menus::enabled(app, "stroke.widthProfile.add"), false) { + if menu_item(ui, tl!("Add to Profiles…"), crate::menus::enabled(app, "stroke.widthProfile.add"), false) { let name = app.session.next_profile_name(); let p = json!({"command": "stroke.widthProfile.add", "label": "Variable Width Profile", "params": {"name": name}}); app.run("ui.paramDialog", p).ok(); @@ -427,10 +433,10 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { // Deletes the selected stroke's saved profile. let shown = current_stroke(app).and_then(|s| s.profile); let saved = shown.as_ref().and_then(|p| app.session.profile_entry(Some(p))).is_some_and(|e| !e.built_in); - if menu_item(ui, "Delete Profile", saved, false) { + if menu_item(ui, tl!("Delete Profile"), saved, false) { app.run("stroke.widthProfile.delete", json!({})).ok(); } - if menu_item(ui, "Reset Profiles", crate::menus::enabled(app, "stroke.widthProfile.reset"), false) { + if menu_item(ui, tl!("Reset Profiles"), crate::menus::enabled(app, "stroke.widthProfile.reset"), false) { app.run("stroke.widthProfile.reset", json!({})).ok(); } } diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 9d74e8afe..3af7cb0ae 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -234,17 +234,17 @@ pub(crate) fn describe(paint: &Paint, global: bool, spot: bool) -> String { match paint { Paint::Solid { color, .. } => { let kind = if spot { - "Spot Color" + tl!("Spot Color") } else if global { - "Global Process Color" + tl!("Global Process Color") } else { - "Process Color" + tl!("Process Color") }; - format!("{kind}, {}", color.model_name()) + crate::i18n::fmt(tl!("{kind}, {model}"), &[("kind", kind), ("model", color.model_name())]) } - Paint::Gradient(g) => format!("{} Gradient", g.gradient.kind.label()), - Paint::Pattern { .. } => "Pattern".into(), - Paint::None => "None".into(), + Paint::Gradient(g) => crate::i18n::fmt(tl!("{kind} Gradient"), &[("kind", tl!(g.gradient.kind.label()))]), + Paint::Pattern { .. } => tl!("Pattern").into(), + Paint::None => tl!("None").into(), } } @@ -423,7 +423,7 @@ fn tile_input(ui: &Ui, resp: Response, e: &Entry, items: &[Entry], sel: &[String } tile_drop(ui, &resp, e, list, ev); let resp = match e { - Entry::Folder(n) => resp.on_hover_text(format!("Color Group: {n}")), + Entry::Folder(n) => resp.on_hover_text(crate::i18n::fmt(tl!("Color Group: {name}"), &[("name", n)])), Entry::Swatch { paint, global, spot, .. } if list => resp.on_hover_ui(|ui| { ui.label(format!("{name} ({})", describe(paint, *global, *spot))); }), @@ -614,7 +614,7 @@ const POPOVER_TILE: &str = "swatch-pop-tile"; /// The panel's body; `salt` keys its tiles (see [`tile_id`]). fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) { if app.session.active().is_none() { - super::empty_state(ui, "swatch-book", "No document", "Open a document to see its swatches."); + super::empty_state(ui, "swatch-book", tl!("No document"), tl!("Open a document to see its swatches.")); return; } let view: View = pstate(ui.ctx(), "swatch-view"); @@ -623,18 +623,18 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) { ui.horizontal(|ui| { super::proxy(app, ui, 34.0); ui.add_space(ui.available_width() - 58.0); - if widgets::icon_button(ui, "dc-list-view", "Show List View", view.is_list(), 26.0).clicked() { + if widgets::icon_button(ui, "dc-list-view", tl!("Show List View"), view.is_list(), 26.0).clicked() { set_pstate(ui.ctx(), "swatch-view", View::SmallList); } - if widgets::icon_button(ui, "dc-grid-view", "Show Thumbnail View", !view.is_list(), 26.0).clicked() { + if widgets::icon_button(ui, "dc-grid-view", tl!("Show Thumbnail View"), !view.is_list(), 26.0).clicked() { set_pstate(ui.ctx(), "swatch-view", View::MediumThumb); } }); ui.add_space(4.0); super::recent_colors_row(app, ui); widgets::divider(ui); - widgets::subheader(ui, "Swatch Tiles"); - let query = if pstate(ui.ctx(), "swatch-show-find") { widgets::search_field(ui, find_id(), "Find") } else { String::new() }; + widgets::subheader(ui, tl!("Swatch Tiles")); + let query = if pstate(ui.ctx(), "swatch-show-find") { widgets::search_field(ui, find_id(), tl!("Find")) } else { String::new() }; let items = entries(app, kind, &query); let active_swatch = active_swatch(app); let selected = selection(app, ui); @@ -832,10 +832,10 @@ fn delete(app: &mut VectorcraftApp, names: Vec, now: bool) { return; } let message = match names.as_slice() { - [n] => format!("Delete “{n}”?"), - _ => format!("Delete these {} swatches and groups?", names.len()), + [n] => crate::i18n::fmt(tl!("Delete “{name}”?"), &[("name", n)]), + _ => crate::i18n::fmt(tl!("Delete these {count} swatches and groups?"), &[("count", &names.len().to_string())]), }; - crate::dialogs::confirm::ask(app, &message, "Art using a deleted global swatch keeps its colour.", "swatch.delete", params); + crate::dialogs::confirm::ask(app, &message, tl!("Art using a deleted global swatch keeps its colour."), "swatch.delete", params); } fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, sel: &[String]) { @@ -847,7 +847,7 @@ fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, sel: &[String]) { ui.set_min_width(200.0); library_panel::library_menu::(app, ui); }); - let kr = widgets::icon_button(ui, "dc-swatch-kinds", "Show Swatch Kinds", kind != Kind::All, 24.0); + let kr = widgets::icon_button(ui, "dc-swatch-kinds", tl!("Show Swatch Kinds"), kind != Kind::All, 24.0); egui::Popup::menu(&kr).show(|ui| { for (k, label) in Kind::ALL { if menu_item(ui, label, true, k == kind) { @@ -858,26 +858,26 @@ fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, sel: &[String]) { // With a colour group selected the options button edits or applies the group. let opts = editable(app, sel); if let Some(g) = selected_group(app, sel).filter(|_| opts.is_none()) { - if widgets::icon_button(ui, "palette", "Edit or Apply Color Group", false, 24.0).clicked() { + if widgets::icon_button(ui, "palette", tl!("Edit or Apply Color Group"), false, 24.0).clicked() { edit_group(app, &g); } - } else if widgets::icon_button_enabled(ui, "dc-options", "Swatch Options", false, opts.is_some(), 24.0).clicked() + } else if widgets::icon_button_enabled(ui, "dc-options", tl!("Swatch Options"), false, opts.is_some(), 24.0).clicked() && let Some(n) = opts { app.run("ui.swatchOptions", json!({"name": n})).ok(); } ui.add_space((ui.available_width() - 3.0 * 28.0).max(0.0)); - if widgets::icon_button(ui, "dc-folder", "New Color Group", false, 24.0).clicked() { + if widgets::icon_button(ui, "dc-folder", tl!("New Color Group"), false, 24.0).clicked() { app.run("ui.newColorGroup", json!({"swatches": deletable(sel)})).ok(); } // Alt-click skips the dialog; Ctrl/Cmd-click makes a spot colour. - if widgets::icon_button(ui, "dc-new-item", "New Swatch", false, 24.0).clicked() { + if widgets::icon_button(ui, "dc-new-item", tl!("New Swatch"), false, 24.0).clicked() { let m = ui.input(|i| i.modifiers); new_swatch(app, sel, m.command, m.alt); } // Alt-click deletes without asking. let del = deletable(sel); - if widgets::icon_button_enabled(ui, "trash-2", "Delete Swatch", false, !del.is_empty(), 24.0).clicked() { + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Swatch"), false, !del.is_empty(), 24.0).clicked() { delete(app, del, ui.input(|i| i.modifiers.alt)); } }); @@ -911,50 +911,50 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let selected = selection(app, ui); let del = deletable(&selected); let opts = editable(app, &selected); - if menu_item(ui, "New Swatch…", true, false) { + if menu_item(ui, tl!("New Swatch…"), true, false) { new_swatch(app, &selected, false, false); } - if menu_item(ui, "New Color Group…", true, false) { + if menu_item(ui, tl!("New Color Group…"), true, false) { app.run("ui.newColorGroup", json!({"swatches": del})).ok(); } - if menu_item(ui, "Duplicate Swatch", opts.is_some(), false) + if menu_item(ui, tl!("Duplicate Swatch"), opts.is_some(), false) && let Some(n) = &opts { app.run("swatch.duplicate", json!({"name": n})).ok(); } // The first selected colour is kept. let merge = mergeable(app, &selected); - if menu_item(ui, "Merge Swatches", merge.len() > 1, false) { + if menu_item(ui, tl!("Merge Swatches"), merge.len() > 1, false) { app.run("swatch.merge", json!({ "names": merge })).ok(); } - if menu_item(ui, "Delete Swatch", !del.is_empty(), false) { + if menu_item(ui, tl!("Delete Swatch"), !del.is_empty(), false) { delete(app, del, false); } let group = selected_group(app, &selected); - if menu_item(ui, "Edit or Apply Color Group…", group.is_some(), false) + if menu_item(ui, tl!("Edit or Apply Color Group…"), group.is_some(), false) && let Some(g) = &group { edit_group(app, g); } - if menu_item(ui, "Ungroup Color Group", group.is_some(), false) + if menu_item(ui, tl!("Ungroup Color Group"), group.is_some(), false) && let Some(g) = group { app.run("swatch.ungroup", json!({ "name": g })).ok(); } - if menu_item(ui, "Select All Unused", true, false) + if menu_item(ui, tl!("Select All Unused"), true, false) && let Ok(r) = app.run("swatch.unused", json!({})) { let names: Vec = r["names"].as_array().into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect(); set_pstate(ui.ctx(), "swatch-selected", names); } - if menu_item(ui, "Add Used Colors", true, false) { + if menu_item(ui, tl!("Add Used Colors"), true, false) { app.run("swatch.addUsedColors", json!({})).ok(); } ui.separator(); - if menu_item(ui, "Sort by Name", true, false) { + if menu_item(ui, tl!("Sort by Name"), true, false) { app.run("swatch.sortByName", json!({})).ok(); } - if menu_item(ui, "Sort by Kind", true, false) { + if menu_item(ui, tl!("Sort by Kind"), true, false) { app.run("swatch.sortByKind", json!({})).ok(); } ui.separator(); @@ -965,21 +965,21 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.separator(); let find: bool = pstate(ui.ctx(), "swatch-show-find"); - if menu_item(ui, "Show Find Field", true, find) { + if menu_item(ui, tl!("Show Find Field"), true, find) { set_pstate(ui.ctx(), "swatch-show-find", !find); } ui.separator(); - if menu_item(ui, "Swatch Options…", opts.is_some(), false) + if menu_item(ui, tl!("Swatch Options…"), opts.is_some(), false) && let Some(n) = opts { app.run("ui.swatchOptions", json!({"name": n})).ok(); } - if menu_item(ui, "Spot Colors…", app.session.active().is_some(), false) { + if menu_item(ui, tl!("Spot Colors…"), app.session.active().is_some(), false) { app.run("ui.spotColors", json!({})).ok(); } ui.separator(); - ui.menu_button("Open Swatch Library", |ui| library_panel::library_menu::(app, ui)); - if menu_item(ui, "Save Swatch Library…", true, false) { + ui.menu_button(tl!("Open Swatch Library"), |ui| library_panel::library_menu::(app, ui)); + if menu_item(ui, tl!("Save Swatch Library…"), true, false) { app.run("ui.saveSwatchLibrary", json!({ "names": deletable(&selected) })).ok(); } } @@ -1071,19 +1071,19 @@ impl LibraryKind for SwatchLibraries { add_from_library(app, json!({"library": id, "names": names})); } fn menu_head(app: &mut VectorcraftApp, ui: &mut Ui) { - if menu_item(ui, "Default Swatches", app.session.active().is_some(), false) { + if menu_item(ui, tl!("Default Swatches"), app.session.active().is_some(), false) { app.run("swatch.resetDefaults", json!({})).ok(); } ui.separator(); } fn menu_tail(app: &mut VectorcraftApp, ui: &mut Ui) { ui.separator(); - if menu_item(ui, "Other Library…", true, false) + if menu_item(ui, tl!("Other Library…"), true, false) && let Err(e) = other_library(app, None) { app.status(e); } - if menu_item(ui, "Save Swatch Library…", app.session.active().is_some(), false) { + if menu_item(ui, tl!("Save Swatch Library…"), app.session.active().is_some(), false) { let names = deletable(&selection(app, ui)); app.run("ui.saveSwatchLibrary", json!({ "names": names })).ok(); } diff --git a/crates/ui-egui/src/panels/symbols.rs b/crates/ui-egui/src/panels/symbols.rs index 486fb20e6..54dead932 100644 --- a/crates/ui-egui/src/panels/symbols.rs +++ b/crates/ui-egui/src/panels/symbols.rs @@ -77,7 +77,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.set_min_height(110.0); ui.set_width(ui.available_width()); if names.is_empty() { - super::empty_state(ui, "spray-can", "No symbols in this document", "Select art and click New Symbol to make one."); + super::empty_state(ui, "spray-can", tl!("No symbols in this document"), tl!("Select art and click New Symbol to make one.")); return; } let paint_thumb = |ui: &Ui, r: egui::Rect, n: &str| { @@ -133,18 +133,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty()); let inst = has_instances_selected(app); widgets::bottom_bar(ui, |ui| { - widgets::icon_button_enabled(ui, "library", "Symbol Libraries (on the roadmap)", false, false, 24.0); - if widgets::icon_button_enabled(ui, "dc-place-symbol", "Place Symbol Instance", false, sel.is_some(), 24.0).clicked() { + widgets::icon_button_enabled(ui, "library", tl!("Symbol Libraries (on the roadmap)"), false, false, 24.0); + if widgets::icon_button_enabled(ui, "dc-place-symbol", tl!("Place Symbol Instance"), false, sel.is_some(), 24.0).clicked() { app.run("symbol.place", json!({})).ok(); } - if widgets::icon_button_enabled(ui, "link-2-off", "Break Link to Symbol", false, inst, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "link-2-off", tl!("Break Link to Symbol"), false, inst, 24.0).clicked() { app.run("symbol.breakLink", json!({})).ok(); } ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0)); - if widgets::icon_button_enabled(ui, "dc-new-item", "New Symbol", false, has_sel, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-new-item", tl!("New Symbol"), false, has_sel, 24.0).clicked() { app.run("symbol.new", json!({})).ok(); } - if widgets::icon_button_enabled(ui, "trash-2", "Delete Symbol", false, sel.is_some(), 24.0).clicked() + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Symbol"), false, sel.is_some(), 24.0).clicked() && let Some(n) = &sel { app.run("symbol.delete", json!({"name": n})).ok(); @@ -158,15 +158,15 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let inst = has_instances_selected(app); let name = json!({"name": sel}); let items: [(&str, bool, &str, Value); 9] = [ - ("New Symbol…", has_sel, "symbol.new", json!({})), - ("Redefine Symbol", has_sel && sel.is_some(), "symbol.update", name.clone()), - ("Duplicate Symbol", sel.is_some(), "symbol.duplicate", name.clone()), - ("Delete Symbol", sel.is_some(), "symbol.delete", name.clone()), - ("Edit Symbol", inst, "symbol.edit", json!({})), - ("Place Symbol Instance", sel.is_some(), "symbol.place", name.clone()), - ("Replace Symbol", inst && sel.is_some(), "symbol.replace", name.clone()), - ("Break Link to Symbol", inst, "symbol.breakLink", json!({})), - ("Select All Instances", sel.is_some(), "symbol.selectInstances", name), + (tl!("New Symbol…"), has_sel, "symbol.new", json!({})), + (tl!("Redefine Symbol"), has_sel && sel.is_some(), "symbol.update", name.clone()), + (tl!("Duplicate Symbol"), sel.is_some(), "symbol.duplicate", name.clone()), + (tl!("Delete Symbol"), sel.is_some(), "symbol.delete", name.clone()), + (tl!("Edit Symbol"), inst, "symbol.edit", json!({})), + (tl!("Place Symbol Instance"), sel.is_some(), "symbol.place", name.clone()), + (tl!("Replace Symbol"), inst && sel.is_some(), "symbol.replace", name.clone()), + (tl!("Break Link to Symbol"), inst, "symbol.breakLink", json!({})), + (tl!("Select All Instances"), sel.is_some(), "symbol.selectInstances", name), ]; for (label, enabled, cmd, params) in items { if menu_item(ui, label, enabled, false) { @@ -175,10 +175,10 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.separator(); let list: bool = pstate(ui.ctx(), "sym-list"); - if menu_item(ui, "Thumbnail View", true, !list) { + if menu_item(ui, tl!("Thumbnail View"), true, !list) { set_pstate(ui.ctx(), "sym-list", false); } - if menu_item(ui, "List View", true, list) { + if menu_item(ui, tl!("List View"), true, list) { set_pstate(ui.ctx(), "sym-list", true); } } diff --git a/crates/ui-egui/src/panels/tabs.rs b/crates/ui-egui/src/panels/tabs.rs index 1f6d2aeb0..7a5e3809a 100644 --- a/crates/ui-egui/src/panels/tabs.rs +++ b/crates/ui-egui/src/panels/tabs.rs @@ -58,7 +58,7 @@ fn apply(app: &mut VectorcraftApp, stops: &[TabStop]) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some((mut stops, span)) = current(app) else { - super::empty_state(ui, "pilcrow", "No text selected", "Select a text object to set its tab stops."); + super::empty_state(ui, "pilcrow", tl!("No text selected"), tl!("Select a text object to set its tab stops.")); return; }; let mut sel: usize = pstate(ui.ctx(), "tabs-sel"); @@ -70,7 +70,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.spacing_mut().item_spacing.x = 3.0; for (i, (a, label, tip)) in ALIGNS.iter().enumerate() { let on = stops.get(sel).map_or(align_idx == i, |s| s.align == *a); - if ui.add(egui::Button::new(egui::RichText::new(*label).size(13.0)).selected(on).min_size(vec2(28.0, 24.0))).on_hover_text(*tip).clicked() + if ui + .add(egui::Button::new(egui::RichText::new(*label).size(13.0)).selected(on).min_size(vec2(28.0, 24.0))) + .on_hover_text(tl!(*tip)) + .clicked() { align_idx = i; set_pstate(ui.ctx(), "tabs-align", i); @@ -93,7 +96,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { apply(app, &stops); } ui.end_row(); - ui.label(egui::RichText::new("Leader:").color(t.text)); + ui.label(egui::RichText::new(tl!("Leader:")).color(t.text)); let mut leader = stops.get(sel).map(|s| s.leader.clone()).unwrap_or_default(); if ui.add_enabled(stops.get(sel).is_some(), egui::TextEdit::singleline(&mut leader).desired_width(90.0)).lost_focus() && let Some(s) = stops.get_mut(sel) @@ -103,7 +106,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { apply(app, &stops); } ui.end_row(); - ui.label(egui::RichText::new("Align On:").color(t.text)); + ui.label(egui::RichText::new(tl!("Align On:")).color(t.text)); let decimal = stops.get(sel).is_some_and(|s| s.align == TabAlign::Decimal); let mut on = stops.get(sel).map(|s| s.align_on.to_string()).unwrap_or_else(|| ".".into()); if ui.add_enabled(decimal, egui::TextEdit::singleline(&mut on).desired_width(30.0)).lost_focus() @@ -196,24 +199,24 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } } - resp.on_hover_text("Click to add a tab stop, drag to move it, drag it off the ruler to delete it"); + resp.on_hover_text(tl!("Click to add a tab stop, drag to move it, drag it off the ruler to delete it")); } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let Some((mut stops, _)) = current(app) else { - ui.add_enabled(false, egui::Button::new("Select text").frame(false)); + ui.add_enabled(false, egui::Button::new(tl!("Select text")).frame(false)); return; }; let sel: usize = pstate(ui.ctx(), "tabs-sel"); - if menu_item(ui, "Clear All Tabs", false, !stops.is_empty()) { + if menu_item(ui, tl!("Clear All Tabs"), false, !stops.is_empty()) { app.run("text.tabs.clear", json!({})).ok(); } - if menu_item(ui, "Delete Tab", false, sel < stops.len()) { + if menu_item(ui, tl!("Delete Tab"), false, sel < stops.len()) { stops.remove(sel); apply(app, &stops); } // Repeat Tab: copies of the selected stop at its distance from the previous one, across the ruler. - if menu_item(ui, "Repeat Tab", false, sel < stops.len()) { + if menu_item(ui, tl!("Repeat Tab"), false, sel < stops.len()) { let prev = if sel == 0 { 0.0 } else { stops[sel - 1].position }; let step = (stops[sel].position - prev).max(1.0); let base = stops[sel].clone(); diff --git a/crates/ui-egui/src/panels/text_styles.rs b/crates/ui-egui/src/panels/text_styles.rs index 972987a2b..5f34153a9 100644 --- a/crates/ui-egui/src/panels/text_styles.rs +++ b/crates/ui-egui/src/panels/text_styles.rs @@ -26,6 +26,10 @@ impl Kind { fn what(self) -> &'static str { if self == Kind::Char { "Character" } else { "Paragraph" } } + /// `what` in the UI language, for text shown to the user. + fn what_label(self) -> &'static str { + if self == Kind::Char { tl!("Character") } else { tl!("Paragraph") } + } fn key(self) -> &'static str { if self == Kind::Char { "char-style-sel" } else { "para-style-sel" } } @@ -94,7 +98,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { } else if resp.hovered() { ui.painter().rect_filled(r, 0.0, t.hover); } - let label = if overridden { format!("{name}+") } else { name.clone() }; + // The built-in "[Normal … Style]" names are translated where painted; other names are user data. + let shown = if name.starts_with('[') && name.ends_with(']') { tl!(name) } else { name.as_str() }; + let label = if overridden { format!("{shown}+") } else { shown.to_string() }; ui.painter().text(r.left_center() + vec2(8.0, 0.0), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(12.5), t.text); if resp.clicked() { clicked = Some((name.clone(), ui.input(|i| i.modifiers.alt))); @@ -113,12 +119,28 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { let deletable = sel.as_ref().is_some_and(|s| *s != normal); widgets::bottom_bar(ui, |ui| { ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0)); - if widgets::icon_button_enabled(ui, "dc-new-item", &format!("Create New {} Style", kind.what()), false, true, 24.0).clicked() + if widgets::icon_button_enabled( + ui, + "dc-new-item", + &crate::i18n::fmt(tl!("Create New {kind} Style"), &[("kind", kind.what_label())]), + false, + true, + 24.0, + ) + .clicked() && let Ok(r) = app.run(&format!("{}.new", kind.prefix()), target(app, kind, json!({}))) { set_pstate(ui.ctx(), kind.key(), r["name"].as_str().map(str::to_string)); } - if widgets::icon_button_enabled(ui, "trash-2", &format!("Delete {} Style", kind.what()), false, deletable, 24.0).clicked() + if widgets::icon_button_enabled( + ui, + "trash-2", + &crate::i18n::fmt(tl!("Delete {kind} Style"), &[("kind", kind.what_label())]), + false, + deletable, + 24.0, + ) + .clicked() && let Some(n) = &sel && app.run(&format!("{}.delete", kind.prefix()), json!({ "name": n })).is_ok() { @@ -133,31 +155,31 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { let user = sel.as_ref().is_some_and(|s| s != normal); let has_text = current(app, kind).is_some() || selection_len(app) > 0; let p = kind.prefix(); - if menu_item(ui, &format!("New {} Style…", kind.what()), true, false) { + if menu_item(ui, &crate::i18n::fmt(tl!("New {kind} Style…"), &[("kind", kind.what_label())]), true, false) { app.run(&format!("{p}.new"), target(app, kind, json!({}))).ok(); } - if menu_item(ui, &format!("Duplicate {} Style", kind.what()), sel.is_some(), false) + if menu_item(ui, &crate::i18n::fmt(tl!("Duplicate {kind} Style"), &[("kind", kind.what_label())]), sel.is_some(), false) && let Some(n) = &sel { app.run(&format!("{p}.duplicate"), json!({ "name": n })).ok(); } - if menu_item(ui, &format!("Delete {} Style", kind.what()), user, false) + if menu_item(ui, &crate::i18n::fmt(tl!("Delete {kind} Style"), &[("kind", kind.what_label())]), user, false) && let Some(n) = &sel { app.run(&format!("{p}.delete"), json!({ "name": n })).ok(); } ui.separator(); - if menu_item(ui, "Clear Overrides", sel.is_some() && has_text, false) + if menu_item(ui, tl!("Clear Overrides"), sel.is_some() && has_text, false) && let Some(n) = &sel { app.run(&format!("{p}.apply"), target(app, kind, json!({ "name": n, "clearOverrides": true }))).ok(); } - if menu_item(ui, &format!("Redefine {} Style", kind.what()), sel.is_some() && has_text, false) + if menu_item(ui, &crate::i18n::fmt(tl!("Redefine {kind} Style"), &[("kind", kind.what_label())]), sel.is_some() && has_text, false) && let Some(n) = &sel { app.run(&format!("{p}.redefine"), target(app, kind, json!({ "name": n }))).ok(); } - if menu_item(ui, &format!("{} Style Options…", kind.what()), user, false) + if menu_item(ui, &crate::i18n::fmt(tl!("{kind} Style Options…"), &[("kind", kind.what_label())]), user, false) && let Some(n) = &sel { let label = format!("{} Style Options", kind.what()); diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs index 54c061d0e..0f366269f 100644 --- a/crates/ui-egui/src/panels/transform.rs +++ b/crates/ui-egui/src/panels/transform.rs @@ -24,7 +24,7 @@ pub fn constrained(w: f64, h: f64, new_w: Option, new_h: Option) -> (f pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); if app.session.active().is_none() { - widgets::dim_label(ui, "No document"); + widgets::dim_label(ui, tl!("No document")); return; } let units = app.session.general_unit(); @@ -69,7 +69,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let origin = rp.map(|p| json!([p.x, p.y])); ui.horizontal(|ui| { ui.add_enabled_ui(has, |ui| { - icons::icon(ui, "rotate-ccw", 16.0, t.icon).on_hover_text("Rotate"); + icons::icon(ui, "rotate-ccw", 16.0, t.icon).on_hover_text(tl!("Rotate")); // The bounding box's angle: a new value turns the selection to it. let angle = bx.map_or(0.0, |b| b.angle); if let Some(a) = widgets::spin_plain(ui, "xfp-rot", angle, "°", 2, 96.0, 15.0, -360.0, &ANGLE_PRESETS) @@ -78,7 +78,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { app.run("object.rotate", json!({"angle": a, "absolute": true, "origin": origin})).ok(); } ui.add_space(4.0); - icons::icon(ui, "dc-shear", 16.0, t.icon).on_hover_text("Shear"); + icons::icon(ui, "dc-shear", 16.0, t.icon).on_hover_text(tl!("Shear")); if let Some(a) = widgets::plain_field(ui, "xfp-shear", 0.0, "°", 1, 56.0) && a != 0.0 { @@ -87,10 +87,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); }); ui.horizontal(|ui| { - if widgets::icon_button_enabled(ui, "flip-horizontal-2", "Flip Horizontal", false, has, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "flip-horizontal-2", tl!("Flip Horizontal"), false, has, 24.0).clicked() { app.run("object.reflect", json!({"axis": "vertical", "origin": origin})).ok(); } - if widgets::icon_button_enabled(ui, "flip-vertical-2", "Flip Vertical", false, has, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "flip-vertical-2", tl!("Flip Vertical"), false, has, 24.0).clicked() { app.run("object.reflect", json!({"axis": "horizontal", "origin": origin})).ok(); } }); @@ -101,26 +101,26 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { widgets::divider(ui); match live { vectorcraft_doc::LiveShape::Rectangle { radii, .. } => { - widgets::subheader(ui, "Rectangle Properties:"); + widgets::subheader(ui, tl!("Rectangle Properties:")); ui.horizontal(|ui| { - widgets::dim_label(ui, "Corner Radius:"); + widgets::dim_label(ui, tl!("Corner Radius:")); if let Some(r) = widgets::num_field(ui, "xfp-radius", Some(radii[0]), units, 80.0) { app.run("object.setLiveShape", json!({"radius": r})).ok(); } }); } vectorcraft_doc::LiveShape::Polygon { sides, .. } => { - widgets::subheader(ui, "Polygon Properties:"); + widgets::subheader(ui, tl!("Polygon Properties:")); ui.horizontal(|ui| { - widgets::dim_label(ui, "Sides:"); + widgets::dim_label(ui, tl!("Sides:")); if let Some(s) = widgets::plain_field(ui, "xfp-sides", *sides as f64, "", 0, 60.0) { app.run("object.setLiveShape", json!({"sides": s.clamp(3.0, 20.0) as u32})).ok(); } }); } _ => { - widgets::subheader(ui, "Shape Properties:"); - widgets::dim_label(ui, n.kind_label()); + widgets::subheader(ui, tl!("Shape Properties:")); + widgets::dim_label(ui, tl!(n.kind_label())); } } } @@ -129,10 +129,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } widgets::divider(ui); let (sc, ss) = (app.session.prefs.scale_corners, app.session.prefs.scale_strokes); - if widgets::check(ui, "Scale Corners", sc, true) { + if widgets::check(ui, tl!("Scale Corners"), sc, true) { set_pref(app, "scaleCorners", !sc); } - if widgets::check(ui, "Scale Strokes & Effects", ss, true) { + if widgets::check(ui, tl!("Scale Strokes & Effects"), ss, true) { set_pref(app, "scaleStrokes", !ss); } } @@ -141,7 +141,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { /// `constrainProportions` preference, which the size fields pass on as `proportional`. pub fn constrain_link(app: &mut VectorcraftApp, ui: &mut Ui) { let on = app.session.prefs.constrain_proportions; - if widgets::icon_button(ui, if on { "link" } else { "link-2-off" }, "Constrain Width and Height Proportions", on, 22.0).clicked() { + if widgets::icon_button(ui, if on { "link" } else { "link-2-off" }, tl!("Constrain Width and Height Proportions"), on, 22.0).clicked() { set_pref(app, "constrainProportions", !on); } } @@ -156,27 +156,27 @@ pub fn set_pref(app: &mut VectorcraftApp, key: &str, on: bool) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "xf-hide-options"); let has = super::selection_len(app) > 0; - if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), "xf-hide-options", !hidden); } ui.separator(); - if menu_item(ui, "Flip Horizontal", has, false) { + if menu_item(ui, tl!("Flip Horizontal"), has, false) { app.run("object.reflect", json!({"axis": "vertical"})).ok(); } - if menu_item(ui, "Flip Vertical", has, false) { + if menu_item(ui, tl!("Flip Vertical"), has, false) { app.run("object.reflect", json!({"axis": "horizontal"})).ok(); } ui.separator(); let ss = app.session.prefs.scale_strokes; - if menu_item(ui, "Scale Strokes & Effects", true, ss) { + if menu_item(ui, tl!("Scale Strokes & Effects"), true, ss) { set_pref(app, "scaleStrokes", !ss); } ui.separator(); - menu_item(ui, "Transform Object Only", false, true); - menu_item(ui, "Transform Pattern Only", false, false); - menu_item(ui, "Transform Both", false, false); + menu_item(ui, tl!("Transform Object Only"), false, true); + menu_item(ui, tl!("Transform Pattern Only"), false, false); + menu_item(ui, tl!("Transform Both"), false, false); ui.separator(); - menu_item(ui, "Use Registration Point for Symbol", false, false); + menu_item(ui, tl!("Use Registration Point for Symbol"), false, false); } #[cfg(test)] diff --git a/crates/ui-egui/src/panels/transparency.rs b/crates/ui-egui/src/panels/transparency.rs index 5d2c3c99e..c38d7ac7c 100644 --- a/crates/ui-egui/src/panels/transparency.rs +++ b/crates/ui-egui/src/panels/transparency.rs @@ -74,7 +74,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // The thumbnail being edited is outlined (object normally, the mask while editing it). let (obj_w, mask_w) = if editing.is_some() { (0.5, 1.5) } else { (1.5, 1.0) }; ui.painter().rect_stroke(r, 0.0, Stroke::new(obj_w, t.border), StrokeKind::Outside); - if editing.is_some() && oresp.on_hover_text("Stop editing the opacity mask").clicked() { + if editing.is_some() && oresp.on_hover_text(tl!("Stop editing the opacity mask")).clicked() { app.run("transparency.stopEditingOpacityMask", json!({})).ok(); } if let Some(n) = &n { @@ -101,7 +101,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { egui::Rect::from_center_size(lr.center(), vec2(12.0, 12.0)), t.icon, ); - if lresp.on_hover_text(if m.linked { "Unlink the mask" } else { "Link the mask" }).clicked() { + if lresp.on_hover_text(if m.linked { tl!("Unlink the mask") } else { tl!("Link the mask") }).clicked() { app.run("transparency.setOpacityMask", json!({"linked": !m.linked})).ok(); } let (mr, _) = ui.allocate_exact_size(vec2(50.0, 50.0), Sense::hover()); @@ -123,7 +123,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.painter().line_segment([mr.right_top(), mr.left_bottom()], red); } let (shift, alt) = ui.input(|i| (i.modifiers.shift, i.modifiers.alt)); - let tip = "Click to edit the mask; Alt-click to view only the mask; Shift-click to disable or enable it"; + let tip = tl!("Click to edit the mask; Alt-click to view only the mask; Shift-click to disable or enable it"); if mresp.on_hover_text(tip).clicked() { if shift { app.run(if m.disabled { "transparency.enableOpacityMask" } else { "transparency.disableOpacityMask" }, json!({})).ok(); @@ -142,25 +142,25 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } ui.vertical(|ui| { - let label = if mask.is_some() { "Release" } else { "Make Mask" }; + let label = if mask.is_some() { tl!("Release") } else { tl!("Make Mask") }; let enabled = mask.is_some() || (has && editing.is_none()); let r = ui.add_enabled_ui(enabled, |ui| widgets::flat_button(ui, label, 96.0)).inner; - if r.on_disabled_hover_text("Select the art (and, on top of it, the mask object)").clicked() { + if r.on_disabled_hover_text(tl!("Select the art (and, on top of it, the mask object)")).clicked() { let id = if mask.is_some() { "transparency.releaseOpacityMask" } else { "transparency.makeOpacityMask" }; app.run(id, json!({})).ok(); } let (clip, invert) = mask.map(|m| (m.clip, m.invert)).unwrap_or((new_clip, new_invert)); - if widgets::check(ui, "Clip", clip, mask.is_some()) { + if widgets::check(ui, tl!("Clip"), clip, mask.is_some()) { app.run("transparency.setOpacityMask", json!({"clip": !clip})).ok(); } - if widgets::check(ui, "Invert Mask", invert, mask.is_some()) { + if widgets::check(ui, tl!("Invert Mask"), invert, mask.is_some()) { app.run("transparency.setOpacityMask", json!({"invert": !invert})).ok(); } }); }); if !pstate::(ui.ctx(), "tr-hide-options") { widgets::divider(ui); - if widgets::check(ui, "Isolate Blending", isolate, has) { + if widgets::check(ui, tl!("Isolate Blending"), isolate, has) { app.run("transparency.set", json!({"isolate": !isolate})).ok(); } // Neutral (and mixed) show a dash; a click moves on → neutral → off → on (mixed → on). @@ -169,26 +169,26 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { Some(Knockout::Off) => Some(false), _ => None, }; - if widgets::check3(ui, "Knockout Group", shown, has) { + if widgets::check3(ui, tl!("Knockout Group"), shown, has) { let next = info.knockout.map_or(Knockout::On, Knockout::cycle); app.run("transparency.set", json!({"knockout": next.label()})).ok(); } - if widgets::check(ui, "Opacity & Mask Define Knockout Shape", knockout_shape, has) { + if widgets::check(ui, tl!("Opacity & Mask Define Knockout Shape"), knockout_shape, has) { app.run("transparency.set", json!({"knockoutShape": !knockout_shape})).ok(); } } if !has { - widgets::dim_label(ui, if selection_len(app) == 0 { "No Selection" } else { "" }); + widgets::dim_label(ui, if selection_len(app) == 0 { tl!("No Selection") } else { "" }); } } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hide_thumbs: bool = pstate(ui.ctx(), "tr-hide-thumbs"); let hide_opts: bool = pstate(ui.ctx(), "tr-hide-options"); - if menu_item(ui, if hide_thumbs { "Show Thumbnails" } else { "Hide Thumbnails" }, true, false) { + if menu_item(ui, if hide_thumbs { tl!("Show Thumbnails") } else { tl!("Hide Thumbnails") }, true, false) { set_pstate(ui.ctx(), "tr-hide-thumbs", !hide_thumbs); } - if menu_item(ui, if hide_opts { "Show Options" } else { "Hide Options" }, true, false) { + if menu_item(ui, if hide_opts { tl!("Show Options") } else { tl!("Hide Options") }, true, false) { set_pstate(ui.ctx(), "tr-hide-options", !hide_opts); } ui.separator(); @@ -197,25 +197,25 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let (_, n) = state(app, ui.ctx()); let mask = n.as_ref().and_then(|n| n.mask.as_deref()); for (label, id, enabled) in mask_items(mask, n.is_some() && !editing) { - if menu_item(ui, label, enabled, false) { + if menu_item(ui, tl!(label), enabled, false) { app.run(id, json!({})).ok(); } } ui.separator(); let (clip, invert) = app.session.new_mask_defaults(); - if menu_item(ui, "New Opacity Masks Are Clipping", true, clip) { + if menu_item(ui, tl!("New Opacity Masks Are Clipping"), true, clip) { app.run("transparency.toggleNewMasksClipping", json!({})).ok(); } - if menu_item(ui, "New Opacity Masks Are Inverted", true, invert) { + if menu_item(ui, tl!("New Opacity Masks Are Inverted"), true, invert) { app.run("transparency.toggleNewMasksInverted", json!({})).ok(); } ui.separator(); let page = app.session.active().map(|d| (d.doc.page_isolate, d.doc.page_knockout)); let (isolate, knockout) = page.unwrap_or_default(); - if menu_item(ui, "Page Isolated Blending", page.is_some(), isolate) { + if menu_item(ui, tl!("Page Isolated Blending"), page.is_some(), isolate) { app.run("transparency.togglePageIsolatedBlending", json!({})).ok(); } - if menu_item(ui, "Page Knockout Group", page.is_some(), knockout) { + if menu_item(ui, tl!("Page Knockout Group"), page.is_some(), knockout) { app.run("transparency.togglePageKnockoutGroup", json!({})).ok(); } } @@ -225,24 +225,24 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { fn mask_items(mask: Option<&OpacityMask>, can_make: bool) -> [(&'static str, &'static str, bool); 4] { match mask { Some(m) => [ - ("Make Opacity Mask", "transparency.makeOpacityMask", false), - ("Release Opacity Mask", "transparency.releaseOpacityMask", true), + (tl!("Make Opacity Mask"), "transparency.makeOpacityMask", false), + (tl!("Release Opacity Mask"), "transparency.releaseOpacityMask", true), if m.disabled { - ("Enable Opacity Mask", "transparency.enableOpacityMask", true) + (tl!("Enable Opacity Mask"), "transparency.enableOpacityMask", true) } else { - ("Disable Opacity Mask", "transparency.disableOpacityMask", true) + (tl!("Disable Opacity Mask"), "transparency.disableOpacityMask", true) }, if m.linked { - ("Unlink Opacity Mask", "transparency.unlinkOpacityMask", true) + (tl!("Unlink Opacity Mask"), "transparency.unlinkOpacityMask", true) } else { - ("Link Opacity Mask", "transparency.linkOpacityMask", true) + (tl!("Link Opacity Mask"), "transparency.linkOpacityMask", true) }, ], None => [ - ("Make Opacity Mask", "transparency.makeOpacityMask", can_make), - ("Release Opacity Mask", "", false), - ("Disable Opacity Mask", "", false), - ("Unlink Opacity Mask", "", false), + (tl!("Make Opacity Mask"), "transparency.makeOpacityMask", can_make), + (tl!("Release Opacity Mask"), "", false), + (tl!("Disable Opacity Mask"), "", false), + (tl!("Unlink Opacity Mask"), "", false), ], } } diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index 06c85a23d..b7ea9b1cb 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -13,7 +13,7 @@ use crate::theme::{self, Brightness, Tokens}; use crate::{VectorcraftApp, widgets}; /// UI-only fields shown in the dialog (backed by `UiState`, not engine prefs). -const UI_FIELDS: &[(&str, &str, &str)] = &[ +pub const UI_FIELDS: &[(&str, &str, &str)] = &[ ("__smartGuides", "Smart Guides", "Smart Guides (View → Smart Guides)"), ("__snapToGrid", "Guides & Grid", "Snap to Grid"), ("__taskBar", "General", "Enable Contextual Task Bar"), @@ -60,6 +60,15 @@ pub fn restore(app: &mut VectorcraftApp) { // Older preference files: keep the saved brightness. p.ui_brightness = app.ui.brightness.id().into(); } + // Older preference files kept the interface language with the UI state; English was the + // default there, so only another language is carried over (onto an unset preference). + if let Some(code) = app.ui.legacy_language.take() + && p.interface_language == "auto" + && code != "en" + && crate::i18n::Lang::from_code(&code).is_some() + { + p.interface_language = code; + } app.session.apply_prefs(p); } @@ -112,7 +121,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { }); let cat = d.str("__category"); let cat = PREF_CATEGORIES.iter().find(|c| **c == cat).copied().unwrap_or(PREF_CATEGORIES[0]); - egui::Window::new("Preferences") + egui::Window::new(tl!("Preferences")) .id(egui::Id::new("dialog-preferences")) .order(egui::Order::Foreground) .collapsible(false) @@ -122,7 +131,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { .frame(egui::Frame::window(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(20))) .show(ctx, |ui| { ui.set_width(760.0); - ui.label(egui::RichText::new("Preferences").font(theme::semibold(16.0)).color(t.text)); + ui.label(egui::RichText::new(tl!("Preferences")).font(theme::semibold(16.0)).color(t.text)); ui.add_space(12.0); ui.horizontal_top(|ui| { // Category list. @@ -135,7 +144,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.spacing_mut().item_spacing.y = 1.0; for c in PREF_CATEGORIES { let sel = *c == cat; - let text = egui::RichText::new(*c).size(12.5).color(if sel { t.text_strong } else { t.text }); + let text = egui::RichText::new(tl!(*c)).size(12.5).color(if sel { t.text_strong } else { t.text }); let b = egui::Button::selectable(sel, text).frame_when_inactive(false).min_size(egui::vec2(184.0, 24.0)); if ui.add(b).clicked() { d.fields.insert("__category".into(), json!(c)); @@ -148,7 +157,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { // Fields. ui.vertical(|ui| { ui.set_width(530.0); - ui.label(egui::RichText::new(cat).font(theme::semibold(14.0)).color(t.text_strong)); + ui.label(egui::RichText::new(tl!(cat)).font(theme::semibold(14.0)).color(t.text_strong)); ui.add_space(8.0); egui::ScrollArea::vertical().id_salt(("prefs", cat)).max_height(400.0).auto_shrink([false, false]).show(ui, |ui| { category_fields(ui, &mut d, cat); @@ -157,26 +166,26 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { }); ui.add_space(14.0); ui.horizontal(|ui| { - if widgets::secondary_button(ui, "Reset Preferences") - .on_hover_text("Restore every preference to its default (applied on OK)") + if widgets::secondary_button(ui, tl!("Reset Preferences")) + .on_hover_text(tl!("Restore every preference to its default (applied on OK)")) .clicked() { reset = true; } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if widgets::primary_button(ui, "OK").clicked() { + if widgets::primary_button(ui, tl!("OK")).clicked() { ok = true; } ui.add_space(8.0); - if widgets::secondary_button(ui, "Cancel").clicked() { + if widgets::secondary_button(ui, tl!("Cancel")).clicked() { cancel = true; } ui.add_space(16.0); let i = PREF_CATEGORIES.iter().position(|c| *c == cat).unwrap_or(0); - if ui.add_enabled(i + 1 < PREF_CATEGORIES.len(), egui::Button::new("Next")).clicked() { + if ui.add_enabled(i + 1 < PREF_CATEGORIES.len(), egui::Button::new(tl!("Next"))).clicked() { d.fields.insert("__category".into(), json!(PREF_CATEGORIES[i + 1])); } - if ui.add_enabled(i > 0, egui::Button::new("Previous")).clicked() { + if ui.add_enabled(i > 0, egui::Button::new(tl!("Previous"))).clicked() { d.fields.insert("__category".into(), json!(PREF_CATEGORIES[i - 1])); } }); @@ -216,7 +225,7 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { if !first { ui.add_space(8.0); } - ui.label(egui::RichText::new(section).font(theme::semibold(12.5)).color(t.text_strong)); + ui.label(egui::RichText::new(tl!(section)).font(theme::semibold(12.5)).color(t.text_strong)); ui.add_space(2.0); } } @@ -228,7 +237,7 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { labeled(ui, sp.label, |ui| { let mut x = v.as_f64().unwrap_or(min); let r = if sp.key == "uiScaling" { - ui.add(egui::Slider::new(&mut x, min..=max).step_by(0.05).text("Smaller ↔ Larger")) + ui.add(egui::Slider::new(&mut x, min..=max).step_by(0.05).text(tl!("Smaller ↔ Larger"))) } else { let speed = if max - min > 100.0 { 0.5 } else { 0.05 }; ui.add(egui::DragValue::new(&mut x).range(min..=max).speed(speed).max_decimals(3).suffix(format!(" {unit}"))) @@ -252,7 +261,7 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { labeled(ui, sp.label, |ui| { let mut x = v.as_i64().unwrap_or(min); let r = if sp.key == "anchorSize" { - ui.add(egui::Slider::new(&mut x, min..=max).show_value(false).text("Size")) + ui.add(egui::Slider::new(&mut x, min..=max).show_value(false).text(tl!("Size"))) } else { ui.add(egui::DragValue::new(&mut x).range(min..=max).speed(0.2)) }; @@ -282,6 +291,20 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { ui.label(egui::RichText::new(hex).color(t.text_dim).size(11.0)); }); } + PrefKind::Text if sp.key == "interfaceLanguage" => { + // `auto` plus every registered language, by its own name. + labeled(ui, sp.label, |ui| { + let cur = v.as_str().unwrap_or("auto"); + let codes: Vec<&str> = std::iter::once("auto").chain(crate::i18n::Lang::all().map(|l| l.code())).collect(); + let labels: Vec<&str> = std::iter::once(tl!("Auto")).chain(crate::i18n::Lang::all().map(|l| l.name())).collect(); + let cur_label = codes.iter().position(|c| c.eq_ignore_ascii_case(cur)).and_then(|i| labels.get(i).copied()).unwrap_or(cur); + if let Some(i) = widgets::dropdown(ui, sp.key, cur_label, &labels, 260.0) + && let Some(code) = codes.get(i) + { + d.fields.insert(sp.key.into(), json!(code)); + } + }); + } PrefKind::Text => { labeled(ui, sp.label, |ui| { let mut s = v.as_str().unwrap_or("").to_string(); @@ -296,7 +319,7 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { fn bool_row(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) { let mut b = d.bool(key); - if ui.checkbox(&mut b, label).changed() { + if ui.checkbox(&mut b, tl!(label)).changed() { d.fields.insert(key.into(), json!(b)); } } @@ -306,7 +329,7 @@ fn labeled(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui)) { ui.horizontal(|ui| { ui.allocate_ui_with_layout(egui::vec2(210.0, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| { ui.set_min_width(210.0); - ui.add(egui::Label::new(egui::RichText::new(format!("{label}:")).color(t.text)).truncate()); + ui.add(egui::Label::new(egui::RichText::new(format!("{}{}", tl!(label), tl!(":"))).color(t.text)).truncate()); }); add(ui); }); diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index 265b69b04..234477e70 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -518,7 +518,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { egui::Area::new(egui::Id::new("modal-dim")).order(egui::Order::Middle).fixed_pos(egui::pos2(0.0, 0.0)).show(ctx, |ui| { ui.allocate_rect(ctx.content_rect(), egui::Sense::click()); }); - egui::Window::new("Keyboard Shortcuts") + egui::Window::new(tl!("Keyboard Shortcuts")) .id(egui::Id::new("dialog-shortcuts")) .order(egui::Order::Foreground) .collapsible(false) @@ -528,11 +528,11 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { .frame(egui::Frame::window(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(20))) .show(ctx, |ui| { ui.set_width(640.0); - ui.label(egui::RichText::new("Keyboard Shortcuts").font(theme::semibold(16.0)).color(t.text)); + ui.label(egui::RichText::new(tl!("Keyboard Shortcuts")).font(theme::semibold(16.0)).color(t.text)); ui.add_space(10.0); // Set row. ui.horizontal(|ui| { - ui.label(egui::RichText::new("Set:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Set:")).color(t.text_dim)); let cur = d.str("set"); let mut opts: Vec<&str> = PRESETS.to_vec(); if !PRESETS.contains(&cur.as_str()) { @@ -546,16 +546,16 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { d.fields.insert("__message".into(), json!("")); } ui.add_space(12.0); - if ui.button("Import…").clicked() { + if ui.button(tl!("Import…")).clicked() { d.fields.insert("__import".into(), json!(true)); } - if ui.button("Export…").clicked() { + if ui.button(tl!("Export…")).clicked() { d.fields.insert("__export".into(), json!(true)); } - if ui.button("Reset to Defaults").clicked() { + if ui.button(tl!("Reset to Defaults")).clicked() { ov.clear(); d.fields.insert("set".into(), json!(PRESETS[0])); - d.fields.insert("__message".into(), json!("All shortcuts reset to defaults.")); + d.fields.insert("__message".into(), json!(tl!("All shortcuts reset to defaults."))); } }); ui.add_space(8.0); @@ -563,13 +563,13 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.horizontal(|ui| { let tab = d.str("tab"); for (id, label) in [("tools", "Tools"), ("menu", "Menu Commands")] { - if ui.selectable_label(tab == id, egui::RichText::new(label).font(theme::semibold(12.5))).clicked() { + if ui.selectable_label(tab == id, egui::RichText::new(tl!(label)).font(theme::semibold(12.5))).clicked() { d.fields.insert("tab".into(), json!(id)); } } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let mut q = d.str("query"); - if ui.add(egui::TextEdit::singleline(&mut q).hint_text("Search").desired_width(200.0)).changed() { + if ui.add(egui::TextEdit::singleline(&mut q).hint_text(tl!("Search")).desired_width(200.0)).changed() { d.fields.insert("query".into(), json!(q)); } }); @@ -584,21 +584,27 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.horizontal(|ui| { ui.allocate_ui_with_layout(egui::vec2(360.0, 16.0), egui::Layout::left_to_right(egui::Align::Center), |ui| { ui.set_min_width(360.0); - ui.label(egui::RichText::new(if tools { "Tool" } else { "Command" }).color(t.text_dim).size(11.0)); + ui.label(egui::RichText::new(if tools { tl!("Tool") } else { tl!("Command") }).color(t.text_dim).size(11.0)); }); - ui.label(egui::RichText::new("Shortcut").color(t.text_dim).size(11.0)); + ui.label(egui::RichText::new(tl!("Shortcut")).color(t.text_dim).size(11.0)); }); egui::ScrollArea::vertical().max_height(360.0).auto_shrink([false, false]).show(ui, |ui| { for e in entries().iter().filter(|e| e.is_tool == tools) { let sc = effective_in(&ov, &e.key); let matches = e.label.to_lowercase().contains(&q) + || tl!(&e.label).to_lowercase().contains(&q) || e.group.to_lowercase().contains(&q) + || tl!(&e.group).to_lowercase().contains(&q) || sc.as_deref().is_some_and(|s| s.to_lowercase().contains(&q)); if !q.is_empty() && !matches { continue; } let row = ui.horizontal(|ui| { - let name = if e.is_tool || e.group.is_empty() { e.label.clone() } else { format!("{} › {}", e.group, e.label) }; + let name = if e.is_tool || e.group.is_empty() { + tl!(&e.label).to_string() + } else { + format!("{} › {}", tl!(&e.group), tl!(&e.label)) + }; let r = ui .allocate_ui_with_layout(egui::vec2(360.0, 20.0), egui::Layout::left_to_right(egui::Align::Center), |ui| { ui.set_min_width(360.0); @@ -609,7 +615,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { d.fields.insert("__selected".into(), json!(e.key)); } let label = if rec == e.key { - "Press keys…".to_string() + tl!("Press keys…").to_string() } else { sc.as_deref().map(menus::pretty_shortcut).unwrap_or_else(|| "—".into()) }; @@ -617,7 +623,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let txt = egui::RichText::new(label).color(if changed { t.accent } else { t.text }); let b = ui .add_sized([150.0, 20.0], egui::Button::new(txt).selected(rec == e.key)) - .on_hover_text("Click, then press the new shortcut (Esc cancels)"); + .on_hover_text(tl!("Click, then press the new shortcut (Esc cancels)")); if b.clicked() { d.fields.insert("__recording".into(), json!(e.key)); d.fields.insert("__selected".into(), json!(e.key)); @@ -634,16 +640,16 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.horizontal(|ui| { let sel = d.str("__selected"); ui.add_enabled_ui(!sel.is_empty(), |ui| { - if ui.button("Clear").on_hover_text("Remove the shortcut").clicked() { + if ui.button(tl!("Clear")).on_hover_text(tl!("Remove the shortcut")).clicked() { let _ = assign(&mut ov, &sel, None, true); d.fields.insert("set".into(), json!(CUSTOM)); } - if ui.button("Use Default").clicked() { + if ui.button(tl!("Use Default")).clicked() { reset_one(&mut ov, &sel); } }); let conflict = d.str("__conflict"); - if !conflict.is_empty() && ui.button("Go to Conflict").clicked() { + if !conflict.is_empty() && ui.button(tl!("Go to Conflict")).clicked() { let tab = if conflict.starts_with("tool:") { "tools" } else { "menu" }; d.fields.insert("tab".into(), json!(tab)); d.fields.insert("query".into(), json!("")); @@ -658,15 +664,23 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { } let n = all_conflicts(&ov).len(); if n > 0 { - ui.label(egui::RichText::new(format!("{n} shortcut(s) are assigned more than once")).color(t.text_dim).size(11.0)); + ui.label( + egui::RichText::new(crate::i18n::tn( + n as u64, + "{n} shortcut is assigned more than once", + "{n} shortcuts are assigned more than once", + )) + .color(t.text_dim) + .size(11.0), + ); } ui.add_space(12.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if widgets::primary_button(ui, "OK").clicked() { + if widgets::primary_button(ui, tl!("OK")).clicked() { ok = true; } ui.add_space(8.0); - if widgets::secondary_button(ui, "Cancel").clicked() { + if widgets::secondary_button(ui, tl!("Cancel")).clicked() { cancel = true; } }); @@ -686,7 +700,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { if let Some(d) = app.ui.dialog.as_mut() { d.fields.insert("overrides".into(), serde_json::to_value(&o).unwrap_or(json!({}))); d.fields.insert("set".into(), json!(s)); - d.fields.insert("__message".into(), json!("Imported. Press OK to keep the imported set.")); + d.fields.insert("__message".into(), json!(tl!("Imported. Press OK to keep the imported set."))); } } Some(Err(e)) if e != "cancelled" => app.status(e), @@ -720,7 +734,10 @@ fn record(d: &mut Dialog, ov: &mut BTreeMap, key: &str, chord: & let names: Vec = removed.iter().map(|k| entry(k).map(|e| e.label.clone()).unwrap_or(k.clone())).collect(); d.fields.insert( "__message".into(), - json!(format!("⚠ {} was already used by {} — it has been removed there.", menus::pretty_shortcut(chord), names.join(", "))), + json!(crate::i18n::fmt( + tl!("⚠ {chord} was already used by {names} — it has been removed there."), + &[("chord", &menus::pretty_shortcut(chord)), ("names", &names.join(", "))] + )), ); d.fields.insert("__conflict".into(), json!(first)); } else { diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index e718e1b6b..fdece9afb 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -207,8 +207,11 @@ impl Dialog { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct UiState { - #[serde(default)] - pub language: crate::i18n::Language, + /// The interface language older versions saved here (`ja`, `cs`…). It now lives in the + /// `interfaceLanguage` preference, which [`crate::prefs_dialog::restore`] carries it over to; + /// it is never written back. + #[serde(rename = "language", skip_serializing)] + pub legacy_language: Option, pub brightness: Brightness, pub dock_tab: DockTab, /// Icon panel currently popped out of the collapsed column. @@ -344,7 +347,7 @@ impl UiState { impl Default for UiState { fn default() -> Self { Self { - language: crate::i18n::Language::default(), + legacy_language: None, brightness: Brightness::MediumDark, dock_tab: DockTab::Properties, open_panel: None, diff --git a/crates/ui-egui/src/tests_contextmenu.rs b/crates/ui-egui/src/tests_contextmenu.rs index dd2fee08e..893583f5f 100644 --- a/crates/ui-egui/src/tests_contextmenu.rs +++ b/crates/ui-egui/src/tests_contextmenu.rs @@ -150,3 +150,32 @@ fn the_menu_follows_the_selection() { assert!(!matches!(items.first(), Some(Item::Sep)) && !matches!(items.last(), Some(Item::Sep))); assert!(items.windows(2).all(|w| !matches!(w, [Item::Sep, Item::Sep]))); } + +/// Every label the context menu can show is a menu string, so complete catalogs must translate it. +#[test] +fn every_context_label_is_a_menu_string() { + let strings = menus::menu_strings(); + let mut app = app(); + let mut shown = vec![]; + // Nothing selected, one path, two paths, a group, a clipping group, a compound path, isolation. + shown.extend(labels(&menus::context_items(&app))); + let (a, b) = (rect(&mut app, 0.0, 0.0), rect(&mut app, 50.0, 50.0)); + shown.extend(labels(&menus::context_items(&app))); + app.run("select.set", json!({"ids": [a.0, b.0]})).unwrap(); + shown.extend(labels(&menus::context_items(&app))); + app.run("object.compoundPath.make", json!({})).unwrap(); + shown.extend(labels(&menus::context_items(&app))); + app.run("object.compoundPath.release", json!({})).unwrap(); + app.run("select.set", json!({"ids": [a.0, b.0]})).unwrap(); + app.run("object.clippingMask.make", json!({})).unwrap(); + shown.extend(labels(&menus::context_items(&app))); + app.run("object.group", json!({})).unwrap(); + shown.extend(labels(&menus::context_items(&app))); + menus::invoke(&mut app, "object.isolate", json!({})); + shown.extend(labels(&menus::context_items(&app))); + let missing: Vec<_> = shown.iter().filter(|l| !strings.contains(**l)).collect(); + assert!(missing.is_empty(), "context menu labels missing from menu_strings: {missing:?}"); + for label in menus::CONTEXT_LABELS { + assert!(shown.contains(label), "`{label}` is listed but the context menu never shows it"); + } +} diff --git a/crates/ui-egui/src/titlebar.rs b/crates/ui-egui/src/titlebar.rs index 9209b30ac..13e70dfe7 100644 --- a/crates/ui-egui/src/titlebar.rs +++ b/crates/ui-egui/src/titlebar.rs @@ -80,10 +80,10 @@ pub fn caption_buttons(app: &mut VectorcraftApp, ui: &mut Ui, bar: Rect) { let ink = if close && resp.hovered() { t.caption_close_text } else { t.icon }; paint_glyph(ui, c, max, r.center(), Stroke::new(1.0, ink)); let tip = match c { - Caption::Minimize => "Minimize", - Caption::Maximize if max => "Restore", - Caption::Maximize => "Maximize", - Caption::Close => "Close", + Caption::Minimize => tl!("Minimize"), + Caption::Maximize if max => tl!("Restore"), + Caption::Maximize => tl!("Maximize"), + Caption::Close => tl!("Close"), }; if resp.on_hover_text(tip).clicked() { clicked = Some(c); diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 8d782dfdf..8707bc27c 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -50,8 +50,8 @@ pub const BASIC: &[(&str, &[&[&str]])] = &[ fn tip(t: &ToolInfo) -> String { match crate::shortcut_editor::tool_shortcut(t.id) { - Some(s) => format!("{} ({})", t.label, s), - None => t.label.to_string(), + Some(s) => format!("{} ({})", tl!(t.label), s), + None => tl!(t.label).to_string(), } } @@ -96,7 +96,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { egui::Rect::from_min_size(hdr.left_top() + vec2(3.0, 2.0), vec2(10.0, 10.0)), if hresp.hovered() { t.text_strong } else { t.text }, ); - if hresp.on_hover_text("Toggle single/double column").clicked() { + if hresp.on_hover_text(tl!("Toggle single/double column")).clicked() { app.ui.toolbar_double = !app.ui.toolbar_double; } let (grip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 6.0), Sense::hover()); @@ -120,7 +120,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { while i < all.len() { if let Some(cat) = all[i].0 { let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 18.0), Sense::hover()); - let label = if cols == 1 && cat.len() > 6 { format!("{}...", &cat[..4]) } else { cat.to_string() }; + // A long name is cut to its first four characters in the single column. + let cat = tl!(cat); + let label = if cols == 1 && cat.chars().count() > 6 { + format!("{}...", cat.chars().take(4).collect::()) + } else { + cat.to_string() + }; ui.painter().text(r.center() + vec2(0.0, 2.0), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(11.0), t.text); } // One row = `cols` slots (a category label always starts a new row). @@ -246,7 +252,7 @@ pub fn control_bar_options(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let opts = app.session.tool_options(); for (key, label, choices) in MIRROR { - ui.label(egui::RichText::new(label).size(12.0).color(t.text)); + ui.label(egui::RichText::new(tl!(label)).size(12.0).color(t.text)); let cur = opts[key].as_str().unwrap_or_default(); let shown = choices.iter().find(|(v, _)| *v == cur).map_or(cur, |(_, l)| *l); let labels: Vec<&str> = choices.iter().map(|(_, l)| *l).collect(); @@ -272,7 +278,7 @@ fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens) { "paint.lastGradient" => widgets::gradient_chip(ui, r, &app.session.last_gradient.gradient), _ => widgets::paint_chip(ui, r, &Paint::None), } - if resp.on_hover_text(tip).clicked() { + if resp.on_hover_text(tl!(tip)).clicked() { clicked = Some(cmd); } } @@ -289,7 +295,7 @@ fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens) { vectorcraft_engine::DrawMode::Behind => 1, vectorcraft_engine::DrawMode::Inside => 2, }; - if widgets::icon_button(ui, modes[m], &format!("{} (Shift+D)", names[m]), m != 0, 26.0).clicked() { + if widgets::icon_button(ui, modes[m], &format!("{} (Shift+D)", tl!(names[m])), m != 0, 26.0).clicked() { app.run("view.drawMode", json!({})).ok(); } if widgets::icon_button(ui, "dc-screen-mode", "Change Screen Mode (F)", false, 26.0).clicked() { @@ -341,7 +347,7 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.painter().text( r.left_center() + vec2(52.0, 0.0), egui::Align2::LEFT_CENTER, - tool.label, + tl!(tool.label), egui::FontId::proportional(13.0), color, ); @@ -376,7 +382,7 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) { fn puppet_warp_options(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let opts = app.session.tool_options(); - ui.label(egui::RichText::new("Expand:").size(12.0).color(t.text)); + ui.label(egui::RichText::new(tl!("Expand:")).size(12.0).color(t.text)); let unit = app.session.general_unit(); if let Some(v) = widgets::num_field(ui, ("cb-tool", "expand"), opts["expand"].as_f64(), unit, 64.0) { app.run("tool.setOption", json!({ "key": "expand", "value": v })).ok(); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index d3ede10f5..0a7d9ef7a 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -21,7 +21,7 @@ pub fn icon_button(ui: &mut Ui, icon: &str, tip: &str, selected: bool, size: f32 ui.painter().rect_filled(rect, CornerRadius::same(3), bg); let pad = (size * 0.2).round(); icons::paint(ui, icon, rect.shrink(pad), if selected { t.text } else { t.icon }); - if !tip.is_empty() { resp.on_hover_text(tip) } else { resp } + if !tip.is_empty() { resp.on_hover_text(tl!(tip)) } else { resp } } /// Small flat text button (Quick Actions style). @@ -40,14 +40,14 @@ pub fn flat_button(ui: &mut Ui, text: &str, width: f32) -> Response { Stroke::new(1.0, if resp.hovered() { t.text } else { t.button_border }), StrokeKind::Inside, ); - ui.painter().with_clip_rect(rect).text(rect.center(), egui::Align2::CENTER_CENTER, text, egui::FontId::proportional(12.5), t.text_strong); + ui.painter().with_clip_rect(rect).text(rect.center(), egui::Align2::CENTER_CENTER, tl!(text), egui::FontId::proportional(12.5), t.text_strong); resp } /// Blue call-to-action pill (dialog OK/Create). pub fn primary_button(ui: &mut Ui, text: &str) -> Response { let t = Tokens::get(ui.ctx()); - let galley = ui.painter().layout_no_wrap(text.to_string(), theme::semibold(12.5), Color32::WHITE); + let galley = ui.painter().layout_no_wrap(tl!(text).to_string(), theme::semibold(12.5), Color32::WHITE); let w = galley.size().x + 32.0; let (rect, resp) = ui.allocate_exact_size(vec2(w.max(72.0), 28.0), Sense::click()); let bg = if resp.hovered() { t.accent } else { t.accent_strong }; @@ -59,7 +59,7 @@ pub fn primary_button(ui: &mut Ui, text: &str) -> Response { /// Outlined secondary pill (dialog Cancel). pub fn secondary_button(ui: &mut Ui, text: &str) -> Response { let t = Tokens::get(ui.ctx()); - let galley = ui.painter().layout_no_wrap(text.to_string(), theme::semibold(12.5), t.text); + let galley = ui.painter().layout_no_wrap(tl!(text).to_string(), theme::semibold(12.5), t.text); let w = galley.size().x + 32.0; let (rect, resp) = ui.allocate_exact_size(vec2(w.max(72.0), 28.0), Sense::click()); if resp.hovered() { @@ -74,7 +74,7 @@ pub fn secondary_button(ui: &mut Ui, text: &str) -> Response { pub fn section_header(ui: &mut Ui, text: &str) { let t = Tokens::get(ui.ctx()); ui.add_space(2.0); - ui.label(egui::RichText::new(text).size(13.0).color(t.text)); + ui.label(egui::RichText::new(tl!(text)).size(13.0).color(t.text)); ui.add_space(2.0); } @@ -89,7 +89,7 @@ pub fn divider(ui: &mut Ui) { pub fn dim_label(ui: &mut Ui, text: &str) -> Response { let t = Tokens::get(ui.ctx()); - ui.label(egui::RichText::new(text).color(t.text).size(12.5)) + ui.label(egui::RichText::new(tl!(text)).color(t.text).size(12.5)) } /// A recessed numeric field showing `value` (points) in `unit`. Returns the new value (points) @@ -193,7 +193,7 @@ pub fn label_row(ui: &mut Ui, label: &str, label_width: f32, add: impl FnOnce(&m ui.horizontal(|ui| { let (r, _) = ui.allocate_exact_size(vec2(label_width, 24.0), Sense::hover()); let t = Tokens::get(ui.ctx()); - ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(12.5), t.text); + ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, tl!(label), egui::FontId::proportional(12.5), t.text); add(ui); }); } @@ -409,10 +409,10 @@ pub fn dropdown_with( width: f32, enabled: impl Fn(usize) -> bool, ) -> Option { - combo(ui, id, current, width, false, |ui| { + combo(ui, id, tl!(current), width, false, |ui| { let mut chosen = None; for (i, o) in options.iter().enumerate() { - if ui.add_enabled(enabled(i), egui::Button::selectable(*o == current, *o)).clicked() { + if ui.add_enabled(enabled(i), egui::Button::selectable(*o == current, tl!(o))).clicked() { chosen = Some(i); } } @@ -533,13 +533,13 @@ pub fn blend_separator_before(i: usize) -> bool { /// The blend-mode dropdown, its groups separated (Normal | darken | lighten | contrast | inversion /// | component modes). `None` (objects that differ) shows blank. Returns the chosen mode. pub fn blend_dropdown(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, current: Option, width: f32) -> Option { - combo(ui, id, current.map_or("", BlendMode::label), width, false, |ui| { + combo(ui, id, current.map_or("", |m| tl!(m.label())), width, false, |ui| { let mut chosen = None; for (i, m) in BlendMode::ALL.into_iter().enumerate() { if blend_separator_before(i) { ui.separator(); } - if ui.selectable_label(Some(m) == current, m.label()).clicked() { + if ui.selectable_label(Some(m) == current, tl!(m.label())).clicked() { chosen = Some(m); } } @@ -644,7 +644,7 @@ pub fn toggle_icon(ui: &mut Ui, on_icon: &str, on: bool, size: f32, tip: &str) - } else if resp.hovered() { icons::paint(ui, on_icon, rect.shrink(size * 0.2), t.text_disabled); } - resp.on_hover_text(tip).clicked() + resp.on_hover_text(tl!(tip)).clicked() } // ---------- panel widgets (Swatches, Color, Stroke, Gradient, Appearance, …) ---------- @@ -658,19 +658,19 @@ pub fn icon_button_enabled(ui: &mut Ui, icon: &str, tip: &str, selected: bool, e let (rect, resp) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover()); let pad = (size * 0.2).round(); icons::paint(ui, icon, rect.shrink(pad), t.text_disabled); - if tip.is_empty() { resp } else { resp.on_hover_text(tip) } + if tip.is_empty() { resp } else { resp.on_hover_text(tl!(tip)) } } /// Regular-weight panel sub-header ("Shape Modes:", "Align Objects:"). pub fn subheader(ui: &mut Ui, text: &str) { let t = Tokens::get(ui.ctx()); - ui.label(egui::RichText::new(text).size(12.5).color(t.text)); + ui.label(egui::RichText::new(tl!(text)).size(12.5).color(t.text)); } /// A label drawn with a dotted underline (Illustrator's link labels: "Stroke:", "Opacity:"). pub fn link_label(ui: &mut Ui, text: &str) -> Response { let t = Tokens::get(ui.ctx()); - let galley = ui.painter().layout_no_wrap(text.to_string(), egui::FontId::proportional(12.5), t.text_strong); + let galley = ui.painter().layout_no_wrap(tl!(text).to_string(), egui::FontId::proportional(12.5), t.text_strong); let (rect, resp) = ui.allocate_exact_size(galley.size() + vec2(0.0, 3.0), Sense::click()); let y = rect.top() + galley.size().y + 1.0; ui.painter().galley(rect.min, galley, t.text_strong); @@ -686,7 +686,8 @@ pub fn link_label(ui: &mut Ui, text: &str) -> Response { /// returns the box, the response and the box's border colour. fn choice_row(ui: &mut Ui, label: &str, enabled: bool) -> (Rect, Response, Color32) { let t = Tokens::get(ui.ctx()); - let galley = ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(12.5), if enabled { t.text } else { t.text_disabled }); + let galley = + ui.painter().layout_no_wrap(tl!(label).to_string(), egui::FontId::proportional(12.5), if enabled { t.text } else { t.text_disabled }); let (rect, resp) = ui.allocate_exact_size(vec2(18.0 + galley.size().x, 20.0f32.max(galley.size().y)), if enabled { Sense::click() } else { Sense::hover() }); let bx = Rect::from_min_size(pos2(rect.left(), rect.center().y - 6.5), Vec2::splat(13.0)); @@ -1027,7 +1028,7 @@ pub fn search_field(ui: &mut Ui, id: egui::Id, hint: &str) -> String { egui::TextEdit::singleline(&mut query) .id(id.with("edit")) .frame(egui::Frame::NONE) - .hint_text(egui::RichText::new(hint).italics()) + .hint_text(egui::RichText::new(tl!(hint)).italics()) .desired_width(ui.available_width()), ); }); @@ -1056,6 +1057,7 @@ pub fn bottom_bar(ui: &mut Ui, add: impl FnOnce(&mut Ui)) { /// A menu row for panel (≡) menus: label, optional check mark, disabled when not implemented. pub fn menu_item(ui: &mut Ui, label: &str, enabled: bool, checked: bool) -> bool { + let label = tl!(label); let text = if checked { format!("✓ {label}") } else { format!(" {label}") }; ui.add_enabled(enabled, egui::Button::new(egui::RichText::new(text).size(12.5)).frame(false)).clicked() } @@ -1297,7 +1299,7 @@ pub fn tab_bar(ui: &mut Ui, tabs: &[&str], current: usize) -> Option { for (i, tab) in tabs.iter().enumerate() { let sel = i == current; let font = if sel { theme::semibold(13.0) } else { egui::FontId::proportional(13.0) }; - let galley = ui.painter().layout_no_wrap(tab.to_string(), font, t.text); + let galley = ui.painter().layout_no_wrap(tl!(tab).to_string(), font, t.text); let (rect, resp) = ui.allocate_exact_size(vec2(galley.size().x, 30.0), Sense::click()); let color = if sel || resp.hovered() { t.text_strong } else { t.text_dim }; ui.painter().galley(pos2(rect.left(), rect.center().y - galley.size().y / 2.0 - 2.0), galley, color); @@ -1334,7 +1336,7 @@ pub fn orientation_button(ui: &mut Ui, landscape: bool, selected: bool, tip: &st ui.painter().add(egui::Shape::convex_polygon(outline.clone(), fill, Stroke::new(1.2, color))); let corner = [page.right_top() - vec2(fold, 0.0), page.right_top() + vec2(-fold, fold), page.right_top() + vec2(0.0, fold)]; ui.painter().add(egui::Shape::line(corner.to_vec(), Stroke::new(1.2, if selected { t.panel } else { color }))); - resp.on_hover_text(tip).clicked() + resp.on_hover_text(tl!(tip)).clicked() } /// `region` of `doc` rendered on white, its longest side `px` pixels, as a texture named `name` diff --git a/crates/ui-egui/src/workspaces.rs b/crates/ui-egui/src/workspaces.rs index 30002af44..17bb04091 100644 --- a/crates/ui-egui/src/workspaces.rs +++ b/crates/ui-egui/src/workspaces.rs @@ -240,6 +240,7 @@ pub fn popup(app: &mut VectorcraftApp, ui: &mut egui::Ui) { } crate::menus::Item::Cmd(label, id, p) => { let label = crate::menus::dynamic_label(app, id, label); + let label = tl!(&label); let text = match crate::menus::checked(app, id, &p) { Some(true) => format!("✓ {label}"), Some(false) => format!(" {label}"), @@ -327,7 +328,9 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { .frame(egui::Frame::window(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(22))) .show(ctx, |ui| { ui.set_width(340.0); - ui.label(egui::RichText::new(if manage { "Manage Workspaces" } else { "New Workspace" }).font(theme::semibold(16.0)).color(t.text)); + ui.label( + egui::RichText::new(if manage { tl!("Manage Workspaces") } else { tl!("New Workspace") }).font(theme::semibold(16.0)).color(t.text), + ); ui.add_space(12.0); if manage { egui::Frame::NONE.fill(t.input).stroke(egui::Stroke::new(1.0, t.input_border)).inner_margin(egui::Margin::same(4)).show(ui, |ui| { @@ -335,7 +338,8 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.set_width(ui.available_width()); for n in names(&app.ui) { let builtin = is_builtin(&n); - let text = if builtin { egui::RichText::new(&n).color(t.text_dim) } else { egui::RichText::new(&n).color(t.text) }; + let shown = if builtin { tl!(&n) } else { n.as_str() }; + let text = if builtin { egui::RichText::new(shown).color(t.text_dim) } else { egui::RichText::new(shown).color(t.text) }; if ui.selectable_label(d.str("selected") == n, text).clicked() && !builtin { d.fields.insert("selected".into(), json!(n)); d.fields.insert("name".into(), json!(n)); @@ -349,36 +353,36 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { if ui.add(egui::TextEdit::singleline(&mut name).desired_width(180.0)).changed() { d.fields.insert("name".into(), json!(name)); } - if ui.button("New").on_hover_text("Save the current layout under this name").clicked() { + if ui.button(tl!("New")).on_hover_text(tl!("Save the current layout under this name")).clicked() { action = Some(("window.workspace.new", json!({"name": d.str("name")}))); } ui.add_enabled_ui(!sel.is_empty(), |ui| { - if ui.button("Rename").clicked() { + if ui.button(tl!("Rename")).clicked() { action = Some(("window.workspace.rename", json!({"name": sel, "to": d.str("name")}))); } - if ui.button("Delete").clicked() { + if ui.button(tl!("Delete")).clicked() { action = Some(("window.workspace.delete", json!({"name": sel}))); } }); }); } else { ui.horizontal(|ui| { - ui.label(egui::RichText::new("Name:").color(t.text_dim)); + ui.label(egui::RichText::new(tl!("Name:")).color(t.text_dim)); let mut name = d.str("name"); let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(240.0)); if r.changed() { d.fields.insert("name".into(), json!(name)); } }); - ui.label(egui::RichText::new("Saves the current bars, toolbar and panel layout.").color(t.text_dim).size(11.0)); + ui.label(egui::RichText::new(tl!("Saves the current bars, toolbar and panel layout.")).color(t.text_dim).size(11.0)); } ui.add_space(16.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if widgets::primary_button(ui, "OK").clicked() { + if widgets::primary_button(ui, tl!("OK")).clicked() { ok = true; } ui.add_space(8.0); - if !manage && widgets::secondary_button(ui, "Cancel").clicked() { + if !manage && widgets::secondary_button(ui, tl!("Cancel")).clicked() { cancel = true; } }); diff --git a/docs/development.md b/docs/development.md index 68ad36f4d..71a9a1b0b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -58,6 +58,48 @@ CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo xtask ci # also runs the Japanese- CI job and every release job (`release.yml`) check craft-fonts out at a pinned commit; release packages carry each embedded font's `OFL-.txt`. +## Localisation + +Strings in code stay English and are the default lookup keys. `crates/ui-egui/src/i18n` maps them to display +text at render time from one catalog per language (`i18n/.tsv`; the format is documented in the header of +`zh-hant.tsv`). Command ids, menu paths used for logic, `ui.menu.list`, the control channel, the CLI and MCP +always use the English ids and labels, so agents and scripts never see translated text. + +Languages shipped: English (`en`, the source), Traditional Chinese (`zh-hant`, complete, in the vocabulary used +in Taiwan; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-*` locales all resolve to it), Czech (`cs`, every menu +label) and Japanese (`ja`, the main menus so far). Untranslated text falls back to English until its rows are +added. Simplified Chinese locales +(`zh-CN`, `zh-SG`, `zh-Hans`) fall back to English until a `zh-hans` catalog is registered: the resolver already +tells the two scripts apart, so the Traditional catalog is never shown to a Simplified locale. + +- `tl!("…")` translates a literal into the language the UI is drawn in; `i18n::t(s)` is the same for a + `&str`. `tr(lang, s)` takes the language; `tr_ctx` when one English word needs different translations; + `tr_id(lang, command_id, label)` for menu items (keyed by command id, English label as the fallback); + `tn` / `trn(lang, n, one, other)` for plurals; `fmt` fills `{name}` placeholders, which a translation may + reorder. +- The shared widgets (`widgets::check`, `dropdown`, `menu_item`, the buttons, `label_row`, tooltips of + `icon_button`…), the menus, the dock, the toolbar, the dialog frame and `panels::empty_state` translate + the text they are given, so a panel mostly needs its literals wrapped in `tl!` to be covered by the tests. +- The language is VectorCraft › Language (the `app.language` UI command, `{lang: auto|}`) or Edit › + Preferences › User Interface › Language; both set the `interfaceLanguage` preference (`auto` or a language + code; `auto` follows the system locale: `VECTORCRAFT_LOCALE`, then `LC_ALL`/`LC_MESSAGES`/`LANG`/`LANGUAGE`, + the macOS preferred languages, the Windows user locale). The web build has no locale detection yet and + starts in English. The Preferences dialog previews the chosen language before OK. +- To add a language: add `.tsv` and one row in `i18n::LANGUAGES` (code, native name, catalog, plural + rule). The Language menu, the dropdown, locale matching and the catalog tests (well-formed, no duplicates, + placeholders and ellipses agree, command ids exist, every row of a partial catalog is a string the UI + shows, the UI fonts have every glyph, no Simplified characters in `zh-hant`) pick it up. Set `complete_menus` + once every menu string and `tl!` literal is translated; `i18n::tests` then enforce it + (`VECTORCRAFT_I18N_DUMP=strings.txt cargo test -p vectorcraft-ui-egui dump_source_strings` lists them). +- Translations are clean-room: written from the meaning of the English text in ordinary vocabulary, never + from another product's localisation resources. Product and technology names stay in Latin letters. +- Not translated on purpose: status-bar messages and errors (agents and tests read them), names that are + user data (layers, swatches, fonts, documents), the tab title's colour mode. Not done yet: locale-aware + number and date formats, right-to-left layout, locale detection on the web. Chinese and Japanese UI text + is drawn with craft-fonts' BIZ UDPGothic when the build embeds it (see Fonts above), else with an installed + system font; the glyph test checks Latin-script catalogs always and the CJK ones only with craft-fonts. + A Traditional Chinese UI font is still to be added to craft-fonts for the web build. + ## Vendor names gate `cargo xtask brands` (part of `cargo xtask ci`) fails when user-visible text names another vendor's products or company: string literals in Rust sources (command labels and params docs, menus, panels, MCP tool definitions), `Cargo.toml` descriptions and packaging files. Comments and test code are not checked. Say "the reference app" or name the feature itself. A line that must keep an old name, such as an alias that files or preferences from earlier versions still use, carries a `brand-ok` comment.