Merge pull request #205 from WildfootW/claude/laughing-pasteur-5d7kc7
i18n: UI localisation framework and a Traditional Chinese (zh-hant) catalog
This commit is contained in:
+2
-1
@@ -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 `<mask>` 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):**
|
||||
|
||||
@@ -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<String, (String, Value, Handle, String)>,
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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")),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::<f32>();
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>/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>/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) {
|
||||
|
||||
@@ -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<egui::Galley>, 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)]
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<Value, String> {
|
||||
let mut p = Value::Object(d.fields.clone());
|
||||
|
||||
@@ -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<Value, String> {
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Col
|
||||
egui::ScrollArea::vertical().id_salt("cp-swatches").max_height(FIELD).show(ui, |ui| {
|
||||
ui.set_width(FIELD + 24.0);
|
||||
let Some(st) = app.session.active() else {
|
||||
widgets::dim_label(ui, "Open a document to see its swatches.");
|
||||
widgets::dim_label(ui, tl!("Open a document to see its swatches."));
|
||||
return;
|
||||
};
|
||||
let doc = &st.doc;
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::VectorcraftApp;
|
||||
use crate::state::Dialog;
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
heading: |d| d.str("__label"),
|
||||
heading: |d| tl!(&d.str("__label")).to_string(),
|
||||
body: |app, ui, d| {
|
||||
let lengths = lengths(&d.str("__command"));
|
||||
form::param_fields(ui, d, &|k| lengths.contains(&k), app.session.general_unit());
|
||||
|
||||
@@ -15,11 +15,11 @@ use crate::theme::Tokens;
|
||||
pub const KIND: &str = "confirm";
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
heading: |d| d.str("message"),
|
||||
heading: |d| tl!(&d.str("message")).to_string(),
|
||||
body: |_, ui, d| {
|
||||
let detail = d.str("detail");
|
||||
if !detail.is_empty() {
|
||||
ui.label(egui::RichText::new(detail).color(Tokens::get(ui.ctx()).text_dim));
|
||||
ui.label(egui::RichText::new(tl!(&detail)).color(Tokens::get(ui.ctx()).text_dim));
|
||||
}
|
||||
false
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ const UI_ONLY: [&str; 2] = ["tab", "bleedLinked"];
|
||||
pub(super) const LABEL: f32 = 130.0;
|
||||
|
||||
pub(super) const SPEC: DialogSpec =
|
||||
DialogSpec { heading: |_| "Document Setup".into(), body, confirm, min_width: 540.0, max_width: Some(540.0), ..DialogSpec::FORM };
|
||||
DialogSpec { heading: |_| tl!("Document Setup").into(), body, confirm, min_width: 540.0, max_width: Some(540.0), ..DialogSpec::FORM };
|
||||
|
||||
/// Open Document Setup on the active document's settings.
|
||||
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
@@ -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<String> = 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<String> = 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::<String>(), "single": q.single.iter().collect::<String>()}));
|
||||
}
|
||||
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<String> = 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)]
|
||||
|
||||
@@ -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<String> =
|
||||
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
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Value, String> {
|
||||
@@ -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
|
||||
|
||||
@@ -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<Vec<&'static Format>> = 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: <name>-<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: <name>-<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
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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<Value> = 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!(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<Value, String> {
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Value, String> {
|
||||
@@ -45,7 +46,7 @@ pub fn open_merge(app: &mut VectorcraftApp, names: Vec<String>) -> Result<Value,
|
||||
|
||||
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
grid(ui, |ui| {
|
||||
label(ui, "Style Name:");
|
||||
label(ui, tl!("Style Name:"));
|
||||
form::text(ui, d, "name", 190.0);
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<Value>) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String, Value> = 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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<Value, String> {
|
||||
|
||||
@@ -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<Value, String> {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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<Value, String> {
|
||||
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<Value, String> {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<Value, String> {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Value> = 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
|
||||
|
||||
@@ -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<Value, String> {
|
||||
@@ -48,12 +48,12 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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::<PrintArtboards>(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::<Media>(ui, d, "media", 160.0, |_| true);
|
||||
});
|
||||
// A paper size shows its size; Custom takes one.
|
||||
let media = choice::<Media>(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::<Orientation>(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::<PrintLayers>(ui, d, "printLayers", 220.0, |_| true);
|
||||
});
|
||||
let scaling = choice::<PrintScaling>(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::<Origin>(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::<PrintScaling>(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<Value, String>) {
|
||||
row(ui, "Mode:", |ui| {
|
||||
row(ui, tl!("Mode:"), |ui| {
|
||||
pick::<OutputMode>(ui, d, "output.mode", 160.0, |_| true);
|
||||
});
|
||||
row(ui, "Emulsion:", |ui| {
|
||||
row(ui, tl!("Emulsion:"), |ui| {
|
||||
pick::<Emulsion>(ui, d, "output.emulsion", 160.0, |_| true);
|
||||
});
|
||||
row(ui, "Image:", |ui| {
|
||||
row(ui, tl!("Image:"), |ui| {
|
||||
pick::<PrintImage>(ui, d, "output.image", 160.0, |_| true);
|
||||
});
|
||||
let separations = choice::<OutputMode>(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<Value, String>) {
|
||||
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<Value, String>) {
|
||||
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::<FontDownload>(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::<Intent>(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::<OutputMode>(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::<PrintOverprints>(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<Value, String>, 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,18 +25,18 @@ pub(super) fn defaults(cmyk: bool, o: &mut Map<String, Value>) {
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> = 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<String> = 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<usize> = 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<String> = keys.iter().map(|k| limit_name(app, k).unwrap_or_else(|| "None".into())).collect();
|
||||
let labels: Vec<String> = 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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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<Item = &'static FormatOption> {
|
||||
/// 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();
|
||||
}
|
||||
|
||||
@@ -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::<Standard>(ui, d, "standard", 170.0, |_| true);
|
||||
let standard = choice::<Standard>(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::<Compatibility>(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::<Standard>(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::<Compatibility>(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::<Downsample>(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::<MonoCodec>(ui, d, &format!("{base}.compression"), 150.0, |_| true);
|
||||
} else {
|
||||
pick::<ImageCodec>(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::<JpegQuality>(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::<MarkKind>(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::<Standard>(d, "standard").unwrap_or_default();
|
||||
heading(ui, "Color");
|
||||
row(ui, "Color conversion:", |ui| {
|
||||
heading(ui, tl!("Color"));
|
||||
row(ui, tl!("Color conversion:"), |ui| {
|
||||
pick::<ColorConversion>(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::<ProfileInclusion>(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::<Overprint>(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::<Compatibility>(d, "compatibility") == Some(Compatibility::Pdf13)
|
||||
|| choice::<Standard>(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::<Standard>(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::<Printing>(ui, d, "security.printing", 260.0, |_| true);
|
||||
});
|
||||
row(ui, "Changes allowed:", |ui| {
|
||||
row(ui, tl!("Changes allowed:"), |ui| {
|
||||
pick::<Changes>(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<Item
|
||||
let t = Tokens::get(ui.ctx());
|
||||
for c in changed {
|
||||
let value = match &c["value"] {
|
||||
Value::Bool(b) => 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<Item
|
||||
pub(super) fn warning_rows(ui: &mut egui::Ui, warnings: &[Value]) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
if warnings.is_empty() {
|
||||
note(ui, "None.");
|
||||
note(ui, tl!("None."));
|
||||
}
|
||||
for w in warnings {
|
||||
ui.label(egui::RichText::new(format!("⚠ {}", w.as_str().unwrap_or_default())).color(t.text));
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::state::Dialog;
|
||||
pub const KIND: &str = "saveGraphicStyleLibrary";
|
||||
|
||||
pub(super) const SPEC: DialogSpec =
|
||||
DialogSpec { heading: |_| "Save Graphic Style Library".into(), body, confirm, min_width: 360.0, ..DialogSpec::FORM };
|
||||
DialogSpec { heading: |_| tl!("Save Graphic Style Library").into(), body, confirm, min_width: 360.0, ..DialogSpec::FORM };
|
||||
|
||||
/// Open Save Graphic Style Library for the document's styles (`names`: the ones selected in the
|
||||
/// panel).
|
||||
@@ -31,7 +31,7 @@ pub fn open(app: &mut VectorcraftApp, names: Vec<String>) -> Result<Value, Strin
|
||||
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
grid(ui, |ui| {
|
||||
name_row(ui, d);
|
||||
destination(ui, d, "Selected Styles Only");
|
||||
destination(ui, d, tl!("Selected Styles Only"));
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
@@ -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<String>) -> Result<Value, String> {
|
||||
@@ -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();
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T: Copy>(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
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 <tspan> 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 <tspan> 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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Value, String> {
|
||||
@@ -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))
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<egui::Color32>) {
|
||||
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),
|
||||
|
||||
@@ -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<Self> {
|
||||
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<String> {
|
||||
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<String> = 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<String> = 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}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<String, String>,
|
||||
/// strings with a disambiguating context, keyed `context \u{1} source`
|
||||
contextual: HashMap<String, String>,
|
||||
/// command-id keyed strings
|
||||
ids: HashMap<String, String>,
|
||||
/// plural messages: `one|other` → forms
|
||||
plurals: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// 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<Entry>, Vec<String>) {
|
||||
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 `context<TAB>source<TAB>translation`", 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
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
# Czech (cs) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> 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
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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…"),
|
||||
];
|
||||
@@ -0,0 +1,42 @@
|
||||
# Japanese (ja) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> 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 ズームアウト
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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<Catalog>,
|
||||
}
|
||||
|
||||
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<Lang> {
|
||||
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<Item = Lang> {
|
||||
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<String> {
|
||||
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<Lang> {
|
||||
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<Lang> = 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<Lang> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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;
|
||||
@@ -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<String> = 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<String> =
|
||||
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<char> =
|
||||
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<String> {
|
||||
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=<file> 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<String> {
|
||||
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<String> = 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<usize> = [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");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
+182
-19
@@ -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|<code>} 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<Result<Value, String>> {
|
||||
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<R
|
||||
/// Checked state for toggle items.
|
||||
pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
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<Item>)> {
|
||||
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<String> {
|
||||
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<String>) {
|
||||
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::*;
|
||||
|
||||
@@ -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::<Vec<_>>().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<String> = 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());
|
||||
|
||||
@@ -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::<Option<(usize, usize)>>(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."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::<Option<f64>>(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)]
|
||||
|
||||
@@ -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>, 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<usize>, 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<usize>, 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<usize>, 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let abs: Vec<String> = 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Settings>, 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<u64> = 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<u64> = 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::<u64>::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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<bool>(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::<bool>(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);
|
||||
|
||||
@@ -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<String> = 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<f64> = 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::<bool>(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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user