mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 13:46:52 -06:00
Merge #490: i18n: Czech interface language
Conflict: crates/ui-egui/src/lib.rs: next's take_ime_discard/canvas_ime, then the PR's status_error Conflict: crates/ui-egui/src/panels/glyphs.rs: next's grid test assertions kept, the PR's misplaced status line dropped; the insert error goes through status_error
This commit is contained in:
@@ -122,9 +122,9 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
ui.allocate_ui_with_layout(vec2(ui.available_width(), h), egui::Layout::top_down(egui::Align::Min), |ui| {
|
||||
ui.set_min_height(h);
|
||||
if app.ui.about_tab == 1 {
|
||||
crate::credits::contributors_ui(ui);
|
||||
crate::credits::contributors_ui(ui, &app.ui.language);
|
||||
} else {
|
||||
crate::credits::models_ui(ui);
|
||||
crate::credits::models_ui(ui, &app.ui.language);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
@@ -164,7 +164,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
.color(t.text_dim),
|
||||
));
|
||||
ui.add_space(16.0);
|
||||
if discord_button(ui, "Join the ArtCraft Discord", vec2(300.0, 40.0)) {
|
||||
if discord_button(ui, crate::i18n::tr(&app.ui.language, "Join the ArtCraft Discord"), vec2(300.0, 40.0)) {
|
||||
open = Some("discord");
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
@@ -182,7 +182,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
for (text, key) in
|
||||
[("DesignCraft page", "appPage"), ("GitHub", "github"), ("getartcraft.com", "website"), ("Report an issue", "issues")]
|
||||
{
|
||||
if link(ui, text, links::get(key).unwrap_or_default(), t.accent) {
|
||||
if link(ui, crate::i18n::tr(&app.ui.language, text), links::get(key).unwrap_or_default(), t.accent) {
|
||||
open = Some(key);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
@@ -206,8 +206,9 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
ui.add_space(((ui.available_width() - w) / 2.0).max(0.0));
|
||||
for (slug, name, what) in SIBLINGS {
|
||||
let url = format!("{}/apps/{slug}", links::WEBSITE);
|
||||
let b =
|
||||
ui.add(egui::Button::new(egui::RichText::new(*name).size(12.0)).corner_radius(10.0)).on_hover_text(format!("{what} — {url}"));
|
||||
let b = ui
|
||||
.add(egui::Button::new(egui::RichText::new(*name).size(12.0)).corner_radius(10.0))
|
||||
.on_hover_text(format!("{} — {url}", crate::i18n::tr(&app.ui.language, what)));
|
||||
if b.clicked() {
|
||||
app.ui.pending_urls.push(url);
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
if icons::button(ui, "share", 22.0, false, crate::i18n::tr(&app.ui.language, "Share")).clicked() {
|
||||
app.status("Export a PDF, IDML or package to share — no cloud account needed.");
|
||||
app.status(crate::i18n::tr(&app.ui.language, "Export a PDF, IDML or package to share — no cloud account needed."));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
// Always one click away: the ArtCraft community Discord.
|
||||
@@ -321,7 +321,7 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
|
||||
impl DesignApp {
|
||||
fn session_home(&mut self) {
|
||||
self.ui.status = "Home".into();
|
||||
self.ui.status = crate::i18n::tr(&self.ui.language, "Home").into();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ use std::cmp::Ordering;
|
||||
|
||||
use egui::RichText;
|
||||
|
||||
use crate::i18n::{tr, tr_context};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Contributor {
|
||||
pub login: &'static str,
|
||||
@@ -211,45 +213,48 @@ impl Default for View {
|
||||
}
|
||||
|
||||
/// About ▸ Contributors: a name toggle, a sort, and the list as a grab bag or a table.
|
||||
pub fn contributors_ui(ui: &mut egui::Ui) {
|
||||
pub fn contributors_ui(ui: &mut egui::Ui, lang: &str) {
|
||||
let id = egui::Id::new("credits_view");
|
||||
let mut v = ui.data_mut(|d| d.get_temp::<View>(id)).unwrap_or_default();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label("Show");
|
||||
ui.label(tr(lang, "Show"));
|
||||
for m in NameMode::ALL {
|
||||
if ui.selectable_label(v.names == m, m.label()).clicked() {
|
||||
if ui.selectable_label(v.names == m, tr(lang, m.label())).clicked() {
|
||||
v.names = m;
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.label("Sort");
|
||||
egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| {
|
||||
ui.label(tr(lang, "Sort"));
|
||||
egui::ComboBox::from_id_salt("credits_sort").selected_text(tr(lang, v.key.label().0)).show_ui(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
if ui.selectable_label(v.key == k, k.label().0).clicked() {
|
||||
if ui.selectable_label(v.key == k, tr(lang, k.label().0)).clicked() {
|
||||
v.key = k;
|
||||
v.ascending = k.default_ascending();
|
||||
}
|
||||
}
|
||||
});
|
||||
if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text("Reverse the order").clicked() {
|
||||
if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text(tr(lang, "Reverse the order")).clicked() {
|
||||
v.ascending = !v.ascending;
|
||||
}
|
||||
ui.separator();
|
||||
if ui.selectable_label(!v.table, "Grab bag").clicked() {
|
||||
if ui.selectable_label(!v.table, tr(lang, "Grab bag")).clicked() {
|
||||
v.table = false;
|
||||
}
|
||||
if ui.selectable_label(v.table, "Table").clicked() {
|
||||
if ui.selectable_label(v.table, tr(lang, "Table")).clicked() {
|
||||
v.table = true;
|
||||
}
|
||||
});
|
||||
let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending);
|
||||
ui.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().weak());
|
||||
let totals = tr(lang, "{contributors} contributors · {commits} commits")
|
||||
.replace("{contributors}", &list.len().to_string())
|
||||
.replace("{commits}", &group(TOTAL_COMMITS));
|
||||
ui.label(RichText::new(totals).small().weak());
|
||||
ui.separator();
|
||||
egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
if list.is_empty() {
|
||||
ui.label("No contributor data was built into this copy.");
|
||||
ui.label(tr(lang, "No contributor data was built into this copy."));
|
||||
} else if v.table {
|
||||
table(ui, &list, &mut v);
|
||||
table(ui, &list, &mut v, lang);
|
||||
} else {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (i, c) in list.iter().enumerate() {
|
||||
@@ -264,11 +269,12 @@ pub fn contributors_ui(ui: &mut egui::Ui) {
|
||||
ui.data_mut(|d| d.insert_temp(id, v));
|
||||
}
|
||||
|
||||
fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) {
|
||||
fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View, lang: &str) {
|
||||
egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" };
|
||||
if ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(k.label().0).clicked() {
|
||||
let header = format!("{}{arrow}", tr_context(lang, k.label().1, "contributors"));
|
||||
if ui.button(RichText::new(header).strong()).on_hover_text(tr(lang, k.label().0)).clicked() {
|
||||
if v.key == k {
|
||||
v.ascending = !v.ascending;
|
||||
} else {
|
||||
@@ -295,16 +301,16 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) {
|
||||
}
|
||||
|
||||
/// About ▸ Models: AI models credited in Co-Authored-By trailers.
|
||||
pub fn models_ui(ui: &mut egui::Ui) {
|
||||
pub fn models_ui(ui: &mut egui::Ui, lang: &str) {
|
||||
if MODELS.is_empty() {
|
||||
ui.label("No model credits were built into this copy.");
|
||||
ui.label(tr(lang, "No model credits were built into this copy."));
|
||||
return;
|
||||
}
|
||||
let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1);
|
||||
egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
egui::Grid::new("credits_models").striped(true).num_columns(6).show(ui, |ui| {
|
||||
for h in ["Company", "Model", "Version", "Commits", "% of all commits", "Lines +/−"] {
|
||||
ui.label(RichText::new(h).strong());
|
||||
ui.label(RichText::new(tr(lang, h)).strong());
|
||||
}
|
||||
ui.end_row();
|
||||
for m in MODELS {
|
||||
|
||||
@@ -144,7 +144,7 @@ fn user_dictionary(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
Ok(_) => {
|
||||
d.fields.insert("word".into(), json!(""));
|
||||
}
|
||||
Err(e) => app.status(format!("User Dictionary: {e}")),
|
||||
Err(e) => app.status_error("User Dictionary", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,7 +220,7 @@ fn keyboard_shortcuts(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, label.trim_end_matches('…')))
|
||||
.on_hover_text(format!("{id}{}", if menu.is_empty() { String::new() } else { format!(" ({menu})") }));
|
||||
let cur = crate::menus::shortcut_of(app, id).map(|s| crate::menus::shortcut_text(&s)).unwrap_or_else(|| "—".into());
|
||||
let text = if recording == *id { "Press keys…".to_string() } else { cur };
|
||||
let text = if recording == *id { crate::i18n::tr(&app.ui.language, "Press keys…").to_string() } else { cur };
|
||||
if ui.add(egui::Button::new(text).min_size(egui::vec2(110.0, 0.0))).clicked() {
|
||||
d.fields.insert("recording".into(), json!(id));
|
||||
d.fields.insert("message".into(), json!(""));
|
||||
@@ -1023,12 +1023,12 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
check(ui, d, "snap.alignEdges", "Align to Object Edges");
|
||||
check(ui, d, "snap.alignCenters", "Align to Object Centers");
|
||||
check(ui, d, "snap.dimensions", "Smart Dimensions");
|
||||
check(ui, d, "snap.spacing", "Smart Spacing");
|
||||
check(ui, d, "snap.alignEdges", crate::i18n::tr(&app.ui.language, "Align to Object Edges"));
|
||||
check(ui, d, "snap.alignCenters", crate::i18n::tr(&app.ui.language, "Align to Object Centers"));
|
||||
check(ui, d, "snap.dimensions", crate::i18n::tr(&app.ui.language, "Smart Dimensions"));
|
||||
check(ui, d, "snap.spacing", crate::i18n::tr(&app.ui.language, "Smart Spacing"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Snap to Zone");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Snap to Zone"));
|
||||
let mut zone = d.n("snap.zone").filter(|z| z.is_finite()).unwrap_or(4.0);
|
||||
if ui.add(egui::DragValue::new(&mut zone).speed(0.1)).changed() {
|
||||
d.fields.insert("snap.zone".into(), json!(zone));
|
||||
@@ -1302,7 +1302,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
for (key, label, any) in [("objFill", "Find fill:", "(any)"), ("objChangeFill", "Change fill to:", "(unchanged)")] {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, label));
|
||||
let cur = d.s(key);
|
||||
egui::ComboBox::from_id_salt(key).selected_text(if cur.is_empty() { any } else { cur.as_str() }).width(180.0).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt(key).selected_text(if cur.is_empty() { crate::i18n::tr(&app.ui.language, any) } else { cur.as_str() }).width(180.0).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(cur.is_empty(), crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, any))).clicked() {
|
||||
d.fields.insert(key.into(), json!(""));
|
||||
}
|
||||
@@ -1316,7 +1316,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
}
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Kind:"));
|
||||
let cur = d.s("objKind");
|
||||
egui::ComboBox::from_id_salt("objKind").selected_text(if cur.is_empty() { "(any)" } else { cur.as_str() }).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt("objKind").selected_text(if cur.is_empty() { crate::i18n::tr(&app.ui.language, "(any)") } else { cur.as_str() }).show_ui(ui, |ui| {
|
||||
for v in ["", "text", "graphic", "shape", "line", "group"] {
|
||||
if ui.selectable_label(cur == v, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, if v.is_empty() { "(any)" } else { v }))).clicked() {
|
||||
d.fields.insert("objKind".into(), json!(v));
|
||||
@@ -1374,22 +1374,22 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Find Next"))).clicked() {
|
||||
let r = app.run("find.next", params());
|
||||
let msg = match r {
|
||||
Ok(Value::Null) => "No matches".to_string(),
|
||||
Ok(_) => "Found".to_string(),
|
||||
Ok(Value::Null) => crate::i18n::tr(&app.ui.language, "No matches").to_string(),
|
||||
Ok(_) => crate::i18n::tr(&app.ui.language, "Found").to_string(),
|
||||
Err(e) => e,
|
||||
};
|
||||
d.fields.insert("status".into(), json!(msg));
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Change All"))).clicked() {
|
||||
let msg = match app.run("find.change", params()) {
|
||||
Ok(v) => format!("{} replacement(s) made", v["count"]),
|
||||
Ok(v) => crate::i18n::tr(&app.ui.language, "{count} replacement(s) made").replace("{count}", &v["count"].to_string()),
|
||||
Err(e) => e,
|
||||
};
|
||||
d.fields.insert("status".into(), json!(msg));
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Count"))).clicked() {
|
||||
let msg = match app.run("find.find", params()) {
|
||||
Ok(v) => format!("{} match(es)", v.as_array().map(|a| a.len()).unwrap_or(0)),
|
||||
Ok(v) => crate::i18n::tr(&app.ui.language, "{count} match(es)").replace("{count}", &v.as_array().map_or(0, |a| a.len()).to_string()),
|
||||
Err(e) => e,
|
||||
};
|
||||
d.fields.insert("status".into(), json!(msg));
|
||||
@@ -1498,7 +1498,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
let f = app.session.execute("color.settings", &json!({})).unwrap_or_default();
|
||||
d.fields.insert("profiles".into(), f["profiles"].clone());
|
||||
}
|
||||
Err(e) => app.status(format!("Color Settings: {e}")),
|
||||
Err(e) => app.status_error("Color Settings", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1523,7 +1523,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
if !d.b("removeStyles") {
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Style Name Conflicts")).font(semibold(12.0)));
|
||||
let conflicts: Vec<String> = d.fields.get("conflicts").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
|
||||
ui.label(if conflicts.is_empty() { crate::i18n::tr(&app.ui.language, "No conflicts").to_string() } else { format!("{} conflict(s): {}", conflicts.len(), conflicts.join(", ")) });
|
||||
ui.label(if conflicts.is_empty() { crate::i18n::tr(&app.ui.language, "No conflicts").to_string() } else { crate::i18n::tr(&app.ui.language, "{count} conflict(s): {names}").replace("{count}", &conflicts.len().to_string()).replace("{names}", &conflicts.join(", ")) });
|
||||
let cur = d.s("styleConflicts");
|
||||
ui.horizontal(|ui| {
|
||||
for (v, l) in [("useExisting", "Use Document Style Definition"), ("redefine", "Redefine Document Style"), ("autoRename", "Auto Rename")] {
|
||||
@@ -2103,7 +2103,8 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
json!({"printer": if printer.is_empty() { Value::Null } else { json!(printer) }, "copies": d.n("copies").unwrap_or(1.0).max(1.0) as u64,
|
||||
"pages": pages, "spreads": d.b("spreads"), "marks": d.b("marks"), "bleed": d.b("bleed")}),
|
||||
)?;
|
||||
app.status(format!("Sent {} page(s) to {}", r["pages"], r["printer"].as_str().unwrap_or("the default printer")));
|
||||
let printer = r["printer"].as_str().unwrap_or_else(|| crate::i18n::tr(&app.ui.language, "the default printer"));
|
||||
app.status(crate::i18n::tr(&app.ui.language, "Sent {count} page(s) to {printer}").replace("{count}", &r["pages"].to_string()).replace("{printer}", printer));
|
||||
Ok(r)
|
||||
}
|
||||
"pdfExport" => {
|
||||
@@ -4742,7 +4743,7 @@ fn insert_xref(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let all = "[All Paragraphs]";
|
||||
ui.horizontal(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Link To:"));
|
||||
combo(ui, d, "linkTo", &[("paragraph", "Paragraph"), ("anchor", "Text Anchor")]);
|
||||
translated_combo(&app.ui.language, ui, d, "linkTo", &[("paragraph", "Paragraph"), ("anchor", "Text Anchor")]);
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
let dim = crate::theme::Tokens::get(ui.ctx()).text_dim;
|
||||
|
||||
@@ -3882,16 +3882,86 @@ mod tests {
|
||||
keys.extend(spec.menu.iter().copied());
|
||||
}
|
||||
keys.extend(crate::menus::UI_COMMANDS.iter().map(|(_, label, _, _)| *label));
|
||||
// Tool names reach `tr` from the Tools panel.
|
||||
keys.extend(designcraft_tools::TOOL_GROUPS.iter().flat_map(|group| group.iter()).map(|tool| tool.label));
|
||||
// So do the About window's tab names, app descriptions and the credits' column and sort labels.
|
||||
keys.extend(crate::about::ABOUT_TABS);
|
||||
keys.extend(crate::about::SIBLINGS.iter().map(|(_, _, what)| *what));
|
||||
keys.extend(crate::credits::NameMode::ALL.iter().map(|mode| mode.label()));
|
||||
keys.extend(crate::credits::SortKey::ALL.iter().flat_map(|key| [key.label().0, key.label().1]));
|
||||
// UI labels passed to `tr` that no other catalog has yet.
|
||||
keys.extend([
|
||||
"(any)",
|
||||
"(unchanged)",
|
||||
"[No Language]",
|
||||
"Cell Number:",
|
||||
"Change fill to:",
|
||||
"Chinese: Simplified",
|
||||
"Chinese: Traditional",
|
||||
"Don't Save",
|
||||
"Email Address:",
|
||||
"Email:",
|
||||
"Find fill:",
|
||||
"Group with",
|
||||
"H:",
|
||||
"Message:",
|
||||
"Organization:",
|
||||
"Phone:",
|
||||
"W:",
|
||||
"Resize panel window",
|
||||
"Resize panels",
|
||||
"Subject:",
|
||||
"Text:",
|
||||
"Unsaved Changes",
|
||||
"“{}” has changes that are not saved. Save them before closing?",
|
||||
"% of all commits",
|
||||
"{count} conflict(s): {names}",
|
||||
"{count} match(es)",
|
||||
"{count} replacement(s) made",
|
||||
"{contributors} contributors · {commits} commits",
|
||||
"{label}: {name} (double-click: Color Picker)",
|
||||
"All",
|
||||
"Align To",
|
||||
"Auto Rename",
|
||||
"Character Style",
|
||||
"Company",
|
||||
"DesignCraft page",
|
||||
"Equal fixed heights for the selected rows; cell text may become overset.",
|
||||
"Found",
|
||||
"Grab bag",
|
||||
"Key Object",
|
||||
"layer",
|
||||
"layers",
|
||||
"Lines +/−",
|
||||
"New Cell Style",
|
||||
"New Table Style",
|
||||
"No contributor data was built into this copy.",
|
||||
"No matches",
|
||||
"No model credits were built into this copy.",
|
||||
"Object Style",
|
||||
"Paragraph Style",
|
||||
"Press keys…",
|
||||
"Range:",
|
||||
"Redefine Document Style",
|
||||
"Report an issue",
|
||||
"Reverse the order",
|
||||
"Story {story} ← story {parent}",
|
||||
"Story Editor — {count} words",
|
||||
"Text Anchor",
|
||||
"Use Document Style Definition",
|
||||
// Status-bar messages.
|
||||
"Alternate layout",
|
||||
"Couldn't write recovery data: {error}",
|
||||
"Export a PDF, IDML or package to share — no cloud account needed.",
|
||||
"Import cancelled: the originating document has closed",
|
||||
"Invalid panel layout; restored the default dock",
|
||||
"PDF import cancelled: the originating document has closed",
|
||||
"Packaged {count} files into {dir}",
|
||||
"Relinked {count}",
|
||||
"Sent {count} page(s) to {printer}",
|
||||
"the default printer",
|
||||
"{name}: import cancelled while a dialog is open; choose the file again",
|
||||
"{name}: {count} step(s) done",
|
||||
]);
|
||||
// A new English string falls back to English until it is translated, so only stale
|
||||
// entries (keys nothing passes to `tr` any more) fail.
|
||||
@@ -3920,6 +3990,8 @@ mod tests {
|
||||
assert_eq!(tr("cs", "Leading"), "Proklad");
|
||||
assert_eq!(tr("cs", "Unknown label"), "Unknown label");
|
||||
assert_eq!(tr_context("cs", "Group", "selection"), "Skupina");
|
||||
assert_eq!(tr_context("cs", "Name", "contributors"), "Jméno");
|
||||
assert_eq!(tr_context("cs", "Name", "unknown"), "Název");
|
||||
assert_eq!(tr("cs", "Group"), "Seskupit");
|
||||
assert_eq!(tr_context("cs", "Columns", "table"), tr("cs", "Columns"));
|
||||
assert_eq!(style_name("cs", "[Basic Paragraph]"), "[Základní odstavec]");
|
||||
@@ -3942,6 +4014,9 @@ mod tests {
|
||||
assert_eq!(count_label("cs", "items", 1), "1 položka");
|
||||
assert_eq!(count_label("cs", "objects", 7), "7 objektů");
|
||||
assert_eq!(count_label("cs", "warnings", 3), "3 varování");
|
||||
assert_eq!(count_label("cs", "layer", 1), "1 vrstva");
|
||||
assert_eq!(count_label("cs", "layers", 3), "3 vrstvy");
|
||||
assert_eq!(count_label("cs", "layers", 8), "8 vrstev");
|
||||
// A noun without plural forms keeps the plain translation.
|
||||
assert_eq!(count_label("cs", "a new noun", 3), "3 a new noun");
|
||||
for (noun, forms) in cs::PLURALS {
|
||||
@@ -3950,6 +4025,14 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn czech_translates_every_tool_name() {
|
||||
for tool in designcraft_tools::TOOL_GROUPS.iter().flat_map(|group| group.iter()) {
|
||||
assert_ne!(tr("cs", tool.label), tool.label, "{}", tool.id);
|
||||
assert_eq!(tr("", tool.label), tool.label);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_vertical_type_commands_are_translated() {
|
||||
for (en, ja) in [
|
||||
|
||||
@@ -1724,30 +1724,160 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Working CMYK", "Pracovní CMYK"),
|
||||
("Yellow", "Žlutá"),
|
||||
// Labels the code passes to `tr` that no other catalog has yet.
|
||||
("About", "O aplikaci"),
|
||||
("Activate Panel", "Aktivovat panel"),
|
||||
("Add Anchor Point Tool", "Nástroj Přidat kotevní bod"),
|
||||
("Align To", "Zarovnat k"),
|
||||
("All", "Vše"),
|
||||
("Alternate layout", "Alternativní rozvržení"),
|
||||
("Auto Rename", "Přejmenovat automaticky"),
|
||||
("Binary assets added", "Přidané binární soubory"),
|
||||
("Binary assets removed", "Odebrané binární soubory"),
|
||||
("Cell Number:", "Číslo mobilu:"),
|
||||
("Change fill to:", "Změnit výplň na:"),
|
||||
("Change Panel Layout", "Změnit rozvržení panelů"),
|
||||
("Character Style", "Styl znaku"),
|
||||
("Chinese: Simplified", "Čínština: zjednodušená"),
|
||||
("Chinese: Traditional", "Čínština: tradiční"),
|
||||
("Close Panel", "Zavřít panel"),
|
||||
("Color Theme Tool", "Nástroj Motiv barev"),
|
||||
("Commits", "Commity"),
|
||||
("Company", "Společnost"),
|
||||
("Content Collector Tool", "Nástroj Sběrač obsahu"),
|
||||
("Content Placer Tool", "Nástroj Umísťovač obsahu"),
|
||||
("Contributors", "Přispěvatelé"),
|
||||
("Convert Direction Point Tool", "Nástroj Převést směrový bod"),
|
||||
("Couldn't write recovery data: {error}", "Nelze zapsat data pro obnovu: {error}"),
|
||||
("Data Fields", "Datová pole"),
|
||||
("Data Merge Options", "Možnosti sloučení dat"),
|
||||
("Delete Anchor Point Tool", "Nástroj Odstranit kotevní bod"),
|
||||
("DesignCraft page", "Stránka DesignCraftu"),
|
||||
("Direct Selection Tool", "Nástroj Přímý výběr"),
|
||||
("Display name", "Zobrazované jméno"),
|
||||
("Distribute Rows Evenly", "Rozmístit řádky rovnoměrně"),
|
||||
("Don't Save", "Neukládat"),
|
||||
("Drag Rows or Columns", "Přetáhnout řádky nebo sloupce"),
|
||||
("Ellipse Frame Tool", "Nástroj Eliptický rámeček"),
|
||||
("Ellipse Tool", "Nástroj Elipsa"),
|
||||
("Email Address:", "E-mailová adresa:"),
|
||||
("Email:", "E-mail:"),
|
||||
("Equal fixed heights for the selected rows; cell text may become overset.", "Stejné pevné výšky vybraných řádků; text buněk může přesáhnout."),
|
||||
("Erase Tool", "Nástroj Guma"),
|
||||
(
|
||||
"Export a PDF, IDML or package to share — no cloud account needed.",
|
||||
"Ke sdílení exportujte PDF, IDML nebo balíček – cloudový účet není potřeba.",
|
||||
),
|
||||
("Eyedropper Tool", "Nástroj Kapátko"),
|
||||
("Find fill:", "Najít výplň:"),
|
||||
("First commit", "První commit"),
|
||||
("Found", "Nalezeno"),
|
||||
("Free Transform Tool", "Nástroj Volná transformace"),
|
||||
("Gap Tool", "Nástroj Mezera"),
|
||||
("Grab bag", "Seznam"),
|
||||
("Gradient Feather Tool", "Nástroj Přechodové zjemnění okraje"),
|
||||
("Gradient Swatch Tool", "Nástroj Vzorek přechodu"),
|
||||
("Group with", "Seskupit s"),
|
||||
("H:", "V:"),
|
||||
("Hand Tool", "Nástroj Ruka"),
|
||||
("image editing", "úpravy obrázků"),
|
||||
("Import cancelled: the originating document has closed", "Import zrušen: původní dokument byl zavřen"),
|
||||
("Insert Field", "Vložit pole"),
|
||||
("Invalid panel layout; restored the default dock", "Neplatné rozvržení panelů; obnoveno výchozí ukotvení"),
|
||||
("Key Object", "Klíčový objekt"),
|
||||
("Last commit", "Poslední commit"),
|
||||
("layer", "vrstva"),
|
||||
("layers", "vrstev"),
|
||||
("Line delta", "Rozdíl řádků"),
|
||||
("Line Tool", "Nástroj Čára"),
|
||||
("Lines +/−", "Řádky +/−"),
|
||||
("Lines added", "Přidané řádky"),
|
||||
("Lines deleted", "Odstraněné řádky"),
|
||||
("List Composite Fonts", "Vypsat složená písma"),
|
||||
("Measure Tool", "Nástroj Měřidlo"),
|
||||
("Merged PRs", "Sloučené PR"),
|
||||
("Message:", "Zpráva:"),
|
||||
("Models", "Modely"),
|
||||
("motion graphics and visual effects", "pohyblivá grafika a vizuální efekty"),
|
||||
("Move Panel", "Přesunout panel"),
|
||||
("Name (A–Z)", "Jméno (A–Z)"),
|
||||
("New Cell Style", "Nový styl buňky"),
|
||||
("New Table Style", "Nový styl tabulky"),
|
||||
("No contributor data was built into this copy.", "Do této kopie nebyla zabudována data o přispěvatelích."),
|
||||
("No matches", "Žádné shody"),
|
||||
("No model credits were built into this copy.", "Do této kopie nebyly zabudovány údaje o modelech."),
|
||||
("Note Tool", "Nástroj Poznámka"),
|
||||
("Object & Layout", "Objekt a rozvržení"),
|
||||
("Object Style", "Styl objektu"),
|
||||
("Organization:", "Organizace:"),
|
||||
("Packaged {count} files into {dir}", "Sbaleno souborů: {count} do {dir}"),
|
||||
("Page Tool", "Nástroj Stránka"),
|
||||
("Paragraph Style", "Styl odstavce"),
|
||||
("PDF import cancelled: the originating document has closed", "Import PDF zrušen: původní dokument byl zavřen"),
|
||||
("Pen Tool", "Nástroj Pero"),
|
||||
("Pencil Tool", "Nástroj Tužka"),
|
||||
("Phone:", "Telefon:"),
|
||||
("photo library and raw development", "knihovna fotografií a vyvolávání RAW"),
|
||||
("Pin Bar Position", "Připnout polohu lišty"),
|
||||
("Polygon Frame Tool", "Nástroj Mnohoúhelníkový rámeček"),
|
||||
("Polygon Tool", "Nástroj Mnohoúhelník"),
|
||||
("Press keys…", "Stiskněte klávesy…"),
|
||||
("Preview Record", "Náhled záznamu"),
|
||||
("Range:", "Rozsah:"),
|
||||
("reading, organizing and protecting PDFs", "čtení, správa a ochrana PDF"),
|
||||
("Real name", "Skutečné jméno"),
|
||||
("Rectangle Frame Tool", "Nástroj Obdélníkový rámeček"),
|
||||
("Rectangle Tool", "Nástroj Obdélník"),
|
||||
("Redefine Document Style", "Předefinovat styl dokumentu"),
|
||||
("Relinked {count}", "Znovu propojeno: {count}"),
|
||||
("Remove Data Source", "Odebrat zdroj dat"),
|
||||
("Remove Field", "Odebrat pole"),
|
||||
("Report an issue", "Nahlásit problém"),
|
||||
("Reset Bar Position", "Obnovit polohu lišty"),
|
||||
("Resize panel window", "Změnit velikost okna panelu"),
|
||||
("Resize panels", "Změnit velikost panelů"),
|
||||
("Reverse the order", "Obrátit pořadí"),
|
||||
("Rotate Tool", "Nástroj Otočení"),
|
||||
("Scale Tool", "Nástroj Změna měřítka"),
|
||||
("Scissors Tool", "Nástroj Nůžky"),
|
||||
("Selection Tool", "Nástroj Výběr"),
|
||||
("Sent {count} page(s) to {printer}", "Odesláno stránek: {count} (tiskárna: {printer})"),
|
||||
("Set Composite Font", "Nastavit složené písmo"),
|
||||
("Shear Tool", "Nástroj Zkosení"),
|
||||
("Smooth Tool", "Nástroj Vyhladit"),
|
||||
("Stop Preview", "Ukončit náhled"),
|
||||
("Story Editor — {count} words", "Editor textu – počet slov: {count}"),
|
||||
("Story {story} ← story {parent}", "Text {story} ← text {parent}"),
|
||||
("Subject:", "Předmět:"),
|
||||
("Text Anchor", "Textová kotva"),
|
||||
("Text:", "Text:"),
|
||||
("the default printer", "výchozí"),
|
||||
("Type on a Path Tool", "Nástroj Text na cestě"),
|
||||
("Type Tool", "Nástroj Text"),
|
||||
("Unsaved Changes", "Neuložené změny"),
|
||||
("Update Data Source", "Aktualizovat zdroj dat"),
|
||||
("Use Document Style Definition", "Použít definici stylu v dokumentu"),
|
||||
("Username", "Uživatelské jméno"),
|
||||
("Validate from Root Element", "Ověřit od kořenového prvku"),
|
||||
("vector illustration", "vektorová ilustrace"),
|
||||
("Vertical Type on a Path Tool", "Nástroj Svislý text na cestě"),
|
||||
("Vertical Type Tool", "Nástroj Svislý text"),
|
||||
("video editing, color and sound", "střih videa, barvy a zvuk"),
|
||||
("W:", "Š:"),
|
||||
("Zoom Tool", "Nástroj Lupa"),
|
||||
("% of all commits", "% všech commitů"),
|
||||
("(any)", "(libovolná)"),
|
||||
("(unchanged)", "(beze změny)"),
|
||||
("[No Language]", "[Žádný jazyk]"),
|
||||
("{contributors} contributors · {commits} commits", "{contributors} přispěvatelů · {commits} commitů"),
|
||||
("{count} conflict(s): {names}", "Konflikty ({count}): {names}"),
|
||||
("{count} match(es)", "Shod: {count}"),
|
||||
("{count} replacement(s) made", "Provedeno nahrazení: {count}"),
|
||||
("{label}: {name} (double-click: Color Picker)", "{label}: {name} (dvojklik: Výběr barvy)"),
|
||||
(
|
||||
"{name}: import cancelled while a dialog is open; choose the file again",
|
||||
"{name}: import byl zrušen, protože je otevřený dialog; vyberte soubor znovu",
|
||||
),
|
||||
("{name}: {count} step(s) done", "{name}: hotovo kroků: {count}"),
|
||||
("“{}” has changes that are not saved. Save them before closing?", "„{}“ obsahuje neuložené změny. Uložit je před zavřením?"),
|
||||
];
|
||||
|
||||
@@ -1760,7 +1890,9 @@ pub(super) const PLURALS: &[(&str, [&str; 3])] = &[
|
||||
("error", ["chyba", "chyby", "chyb"]),
|
||||
("errors", ["chyba", "chyby", "chyb"]),
|
||||
("warnings", ["varování", "varování", "varování"]),
|
||||
("layer", ["vrstva", "vrstvy", "vrstev"]),
|
||||
("layers", ["vrstva", "vrstvy", "vrstev"]),
|
||||
];
|
||||
|
||||
/// Contexts whose Czech noun differs from the shared action label.
|
||||
pub(super) const CONTEXT: &[(&str, &str, &str)] = &[("Group", "selection", "Skupina")];
|
||||
pub(super) const CONTEXT: &[(&str, &str, &str)] = &[("Group", "selection", "Skupina"), ("Name", "contributors", "Jméno")];
|
||||
|
||||
@@ -656,6 +656,12 @@ impl DesignApp {
|
||||
self.canvas_ime
|
||||
}
|
||||
|
||||
/// "<feature>: <error>" in the status bar: the feature name is translated, the engine's own
|
||||
/// error text stays as the engine wrote it.
|
||||
pub fn status_error(&mut self, feature: &str, error: impl std::fmt::Display) {
|
||||
self.ui.status = format!("{}: {error}", crate::i18n::tr(&self.ui.language, feature));
|
||||
}
|
||||
|
||||
/// THE entry point for every action (menus, shortcuts, palette, panels, control channel).
|
||||
pub fn run(&mut self, id: &str, params: Value) -> Result<Value, String> {
|
||||
if let Some(r) = menus::run_ui(self, id, ¶ms) {
|
||||
@@ -825,7 +831,7 @@ impl DesignApp {
|
||||
pub(crate) fn activate_import_target(&mut self, request: &ImportRequest) -> Result<(), String> {
|
||||
let Some(index) = request.target.and_then(|uid| self.session.documents().iter().position(|d| d.uid == uid)) else {
|
||||
let message = "Import cancelled: the originating document has closed";
|
||||
self.status(message);
|
||||
self.status(crate::i18n::tr(&self.ui.language, message));
|
||||
return Err(message.into());
|
||||
};
|
||||
self.run("file.activate", json!({"index": index}))?;
|
||||
@@ -858,7 +864,7 @@ impl DesignApp {
|
||||
if owns_dialog {
|
||||
self.ui.dialog = None;
|
||||
}
|
||||
self.status("PDF import cancelled: the originating document has closed");
|
||||
self.status(crate::i18n::tr(&self.ui.language, "PDF import cancelled: the originating document has closed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -866,7 +872,10 @@ impl DesignApp {
|
||||
let files = std::mem::take(&mut *inbox.lock().unwrap_or_else(|e| e.into_inner()));
|
||||
for file in files {
|
||||
if self.ui.dialog.is_some() {
|
||||
self.status(format!("{}: import cancelled while a dialog is open; choose the file again", file.name));
|
||||
self.status(
|
||||
crate::i18n::tr(&self.ui.language, "{name}: import cancelled while a dialog is open; choose the file again")
|
||||
.replace("{name}", &file.name),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let name = file.name.clone();
|
||||
@@ -953,7 +962,7 @@ impl DesignApp {
|
||||
if self.session.documents().iter().any(|d| d.is_dirty())
|
||||
&& let Err(e) = self.session.execute("file.recovery.save", &json!({}))
|
||||
{
|
||||
self.status(format!("Couldn't write recovery data: {e}"));
|
||||
self.status(crate::i18n::tr(&self.ui.language, "Couldn't write recovery data: {error}").replace("{error}", &e.to_string()));
|
||||
}
|
||||
}
|
||||
let dt = now - self.last_time;
|
||||
@@ -1571,6 +1580,16 @@ mod tests {
|
||||
assert_eq!(alert(&app), ("Can't Place the File".to_string(), e));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_messages_follow_the_interface_language_but_engine_errors_stay_english() {
|
||||
let mut app = DesignApp::new(Session::new(), Services::default());
|
||||
app.status_error("Table", "No table selected");
|
||||
assert_eq!(app.ui.status, "Table: No table selected");
|
||||
app.ui.language = "cs".into();
|
||||
app.status_error("Table", "No table selected");
|
||||
assert_eq!(app.ui.status, "Tabulka: No table selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropping_an_unreadable_file_alerts() {
|
||||
// A document opens, so it says it can't be opened.
|
||||
|
||||
@@ -901,11 +901,11 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
Some(dir) => {
|
||||
let r = app.run("file.package", json!({"dir": dir}));
|
||||
if let Ok(v) = &r {
|
||||
app.status(format!(
|
||||
"Packaged {} files into {}",
|
||||
v["files"].as_array().map_or(0, |f| f.len()),
|
||||
v["dir"].as_str().unwrap_or("")
|
||||
));
|
||||
app.status(
|
||||
crate::i18n::tr(&app.ui.language, "Packaged {count} files into {dir}")
|
||||
.replace("{count}", &v["files"].as_array().map_or(0, |f| f.len()).to_string())
|
||||
.replace("{dir}", v["dir"].as_str().unwrap_or("")),
|
||||
);
|
||||
}
|
||||
r
|
||||
}
|
||||
@@ -2587,7 +2587,7 @@ pub fn palette(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
if let Some(sc) = &it.shortcut {
|
||||
b = b.shortcut_text(shortcut_text(sc));
|
||||
} else if !it.kind.is_empty() {
|
||||
b = b.shortcut_text(it.kind);
|
||||
b = b.shortcut_text(crate::i18n::tr(&app.ui.language, it.kind));
|
||||
}
|
||||
// The button aligns its label by the layout it's in: left, like the rows with a shortcut.
|
||||
let size = egui::vec2(460.0, 22.0);
|
||||
|
||||
@@ -300,7 +300,7 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) -> bool {
|
||||
let Some(layout) = app.ui.docking.as_ref() else { return false };
|
||||
if !valid(layout) {
|
||||
app.ui.docking = None;
|
||||
app.status("Invalid panel layout; restored the default dock");
|
||||
app.status(crate::i18n::tr(&app.ui.language, "Invalid panel layout; restored the default dock"));
|
||||
return false;
|
||||
}
|
||||
let mut layout = layout.clone();
|
||||
|
||||
@@ -58,10 +58,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if has_text {
|
||||
let on = !applied.contains(&c.name);
|
||||
if let Err(e) = app.run("condition.apply", json!({"name": c.name, "on": on})) {
|
||||
app.status(format!("Conditional Text: {e}"));
|
||||
app.status_error("Conditional Text", e);
|
||||
}
|
||||
} else {
|
||||
app.status("Select text to apply a condition".to_string());
|
||||
app.status(crate::i18n::tr(&app.ui.language, "Select text to apply a condition"));
|
||||
}
|
||||
}
|
||||
nr.context_menu(|ui| {
|
||||
@@ -83,7 +83,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New"))).clicked() && !name.trim().is_empty() {
|
||||
match app.run("condition.new", json!({"name": name.trim()})) {
|
||||
Ok(_) => name.clear(),
|
||||
Err(e) => app.status(format!("Conditional Text: {e}")),
|
||||
Err(e) => app.status_error("Conditional Text", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
st.recent.insert(0, c);
|
||||
st.recent.truncate(12);
|
||||
}
|
||||
Err(e) => app.status(format!("Glyphs: {e}")),
|
||||
Err(e) => app.status_error("Glyphs", e),
|
||||
}
|
||||
}
|
||||
crate::rtl::label(
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde_json::{Value, json};
|
||||
use crate::DesignApp;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
fn dest_text(d: &Value) -> String {
|
||||
fn dest_text(lang: &str, d: &Value) -> String {
|
||||
if let Some(u) = d.get("url").and_then(Value::as_str) {
|
||||
return u.to_string();
|
||||
}
|
||||
@@ -13,7 +13,7 @@ fn dest_text(d: &Value) -> String {
|
||||
return format!("mailto:{e}");
|
||||
}
|
||||
if let Some(p) = d.get("page").and_then(Value::as_u64) {
|
||||
return format!("Page {}", p + 1);
|
||||
return format!("{} {}", crate::i18n::tr(lang, "Page"), p + 1);
|
||||
}
|
||||
d.to_string()
|
||||
}
|
||||
@@ -34,7 +34,7 @@ pub fn hyperlinks(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
{
|
||||
let p = if let Some(m) = url.strip_prefix("mailto:") { json!({"email": m}) } else { json!({"url": url}) };
|
||||
if let Err(e) = app.run("hyperlink.create", p) {
|
||||
app.status(format!("Hyperlinks: {e}"));
|
||||
app.status_error("Hyperlinks", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -55,7 +55,7 @@ pub fn hyperlinks(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
for h in list {
|
||||
let hid = h["id"].clone();
|
||||
ui.horizontal(|ui| {
|
||||
let r = ui.selectable_label(false, h["name"].as_str().unwrap_or("")).on_hover_text(dest_text(&h["dest"]));
|
||||
let r = ui.selectable_label(false, h["name"].as_str().unwrap_or("")).on_hover_text(dest_text(&app.ui.language, &h["dest"]));
|
||||
if r.clicked() {
|
||||
let _ = app.run("hyperlink.goToSource", json!({"id": hid}));
|
||||
}
|
||||
@@ -88,7 +88,10 @@ pub fn bookmarks(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
for (i, b) in list.iter().enumerate() {
|
||||
let page = b["page"].as_u64().unwrap_or(0) as usize;
|
||||
ui.horizontal(|ui| {
|
||||
if ui.selectable_label(false, b["name"].as_str().unwrap_or("")).on_hover_text(format!("Page {}", app.session.page_label(page))).clicked()
|
||||
if ui
|
||||
.selectable_label(false, b["name"].as_str().unwrap_or(""))
|
||||
.on_hover_text(format!("{} {}", crate::i18n::tr(&app.ui.language, "Page"), app.session.page_label(page)))
|
||||
.clicked()
|
||||
{
|
||||
crate::canvas::go_to_page(app, page);
|
||||
}
|
||||
@@ -176,7 +179,7 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New"))).clicked() && !name.trim().is_empty() {
|
||||
match app.run("xml.newTag", json!({"name": name.trim()})) {
|
||||
Ok(_) => name.clear(),
|
||||
Err(e) => app.status(format!("Tags: {e}")),
|
||||
Err(e) => app.status_error("Tags", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -232,7 +235,7 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
{
|
||||
match app.run("xml.loadDtd", json!({"path": path})) {
|
||||
Ok(_) => problems = None,
|
||||
Err(e) => app.status(format!("Load DTD: {e}")),
|
||||
Err(e) => app.status_error("Load DTD", e),
|
||||
}
|
||||
}
|
||||
let has_dtd = info["dtd"].as_bool().unwrap_or(false);
|
||||
@@ -281,7 +284,7 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("xml"))
|
||||
&& let Err(e) = app.run("file.importXml", json!({"path": path}))
|
||||
{
|
||||
app.status(format!("Import XML: {e}"));
|
||||
app.status_error("Import XML", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -310,7 +313,7 @@ pub fn states(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
.clicked()
|
||||
&& let Err(e) = app.run("states.create", json!({}))
|
||||
{
|
||||
app.status(format!("Object States: {e}"));
|
||||
app.status_error("Object States", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -398,7 +401,7 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if let Some(p) = params
|
||||
&& let Err(e) = app.run("button.set", p)
|
||||
{
|
||||
app.status(format!("Buttons: {e}"));
|
||||
app.status_error("Buttons", e);
|
||||
}
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
@@ -565,7 +568,7 @@ pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Create"))).clicked()
|
||||
&& let Err(e) = app.run("layout.createAlternate", json!({"name": name, "width": w, "height": h}))
|
||||
{
|
||||
app.status(format!("Alternate layout: {e}"));
|
||||
app.status_error("Alternate layout", e);
|
||||
}
|
||||
ui.data_mut(|x| x.insert_temp(key, (name, w, h)));
|
||||
}
|
||||
@@ -598,7 +601,7 @@ pub fn media(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("place"))
|
||||
&& let Err(e) = app.run("media.options", json!({"poster": {"path": path}}))
|
||||
{
|
||||
app.status(format!("Media: {e}"));
|
||||
app.status_error("Media", e);
|
||||
}
|
||||
if !m["poster"].is_null() && ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "None"))).clicked() {
|
||||
let _ = app.run("media.options", json!({"poster": null}));
|
||||
|
||||
@@ -479,10 +479,10 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!(
|
||||
"Page: {}, {} Layer{}",
|
||||
"{}: {}, {}",
|
||||
crate::i18n::tr(&app.ui.language, "Page"),
|
||||
app.session.page_label(cur),
|
||||
doc.layers.len(),
|
||||
if doc.layers.len() == 1 { "" } else { "s" }
|
||||
crate::i18n::count_label(&app.ui.language, if doc.layers.len() == 1 { "layer" } else { "layers" }, doc.layers.len())
|
||||
))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("library"))
|
||||
&& let Err(e) = app.run("library.open", json!({"path": path}))
|
||||
{
|
||||
app.status(format!("Library: {e}"));
|
||||
app.status_error("Library", e);
|
||||
}
|
||||
});
|
||||
let Ok(items) = app.session.execute("library.list", &json!({})) else {
|
||||
@@ -51,7 +51,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if ui.add_enabled(has_sel, egui::Button::new(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Add Selection")))).clicked()
|
||||
&& let Err(e) = app.run("library.add", json!({}))
|
||||
{
|
||||
app.status(format!("Library: {e}"));
|
||||
app.status_error("Library", e);
|
||||
}
|
||||
ui.separator();
|
||||
let items = items.as_array().cloned().unwrap_or_default();
|
||||
@@ -77,7 +77,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Place"))).clicked()
|
||||
&& let Err(e) = app.run("library.place", json!({"index": i}))
|
||||
{
|
||||
app.status(format!("Library: {e}"));
|
||||
app.status_error("Library", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -93,13 +93,13 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
&& let Some(path) = app.services.pick_save.as_mut().and_then(|f| f("Book.dcbook"))
|
||||
&& let Err(e) = app.run("book.new", json!({"path": path}))
|
||||
{
|
||||
app.status(format!("Book: {e}"));
|
||||
app.status_error("Book", e);
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Open…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("book"))
|
||||
&& let Err(e) = app.run("book.open", json!({"path": path}))
|
||||
{
|
||||
app.status(format!("Book: {e}"));
|
||||
app.status_error("Book", e);
|
||||
}
|
||||
});
|
||||
let Ok(info) = app.session.execute("book.list", &json!({})) else {
|
||||
@@ -145,7 +145,7 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("open"))
|
||||
&& let Err(e) = app.run("book.add", json!({"path": path}))
|
||||
{
|
||||
app.status(format!("Book: {e}"));
|
||||
app.status_error("Book", e);
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Update Numbering"))).clicked() {
|
||||
let _ = app.run("book.paginate", json!({}));
|
||||
@@ -157,7 +157,7 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
&& let Some(path) = app.services.pick_save.as_mut().and_then(|f| f("Book.pdf"))
|
||||
&& let Err(e) = app.run("book.exportPdf", json!({"path": path}))
|
||||
{
|
||||
app.status(format!("Book: {e}"));
|
||||
app.status_error("Book", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -202,7 +202,7 @@ pub fn scripts(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
app.ui.scripts.push((name, text));
|
||||
sel = app.ui.scripts.len() - 1;
|
||||
}
|
||||
Err(e) => app.status(format!("Scripts: {e}")),
|
||||
Err(e) => app.status_error("Scripts", e),
|
||||
}
|
||||
}
|
||||
if ui
|
||||
@@ -222,7 +222,11 @@ pub fn scripts(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
&& let Some((name, text)) = app.ui.scripts.get(i).cloned()
|
||||
{
|
||||
match app.run("script.run", json!({"text": text})) {
|
||||
Ok(r) => app.status(format!("{name}: {} step(s) done", r["steps"])),
|
||||
Ok(r) => app.status(
|
||||
crate::i18n::tr(&app.ui.language, "{name}: {count} step(s) done")
|
||||
.replace("{name}", &name)
|
||||
.replace("{count}", &r["steps"].to_string()),
|
||||
),
|
||||
Err(e) => app.status(format!("{name}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
{
|
||||
match app.run("note.new", json!({"text": draft.trim()})) {
|
||||
Ok(_) => draft.clear(),
|
||||
Err(e) => app.status(format!("Notes: {e}")),
|
||||
Err(e) => app.status_error("Notes", e),
|
||||
}
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(id, draft));
|
||||
|
||||
@@ -595,7 +595,11 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
||||
let chip = sub(b, 4.0, 2.0, 17.0, 17.0);
|
||||
widgets::paint_chip(ui.painter(), chip, fc, fg);
|
||||
ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
let resp = ui.interact(chip, ui.id().with("fillchip"), Sense::click()).on_hover_text(format!("Fill: {}", i.fill));
|
||||
let resp = ui.interact(chip, ui.id().with("fillchip"), Sense::click()).on_hover_text(format!(
|
||||
"{}: {}",
|
||||
crate::i18n::tr(&app.ui.language, "Fill"),
|
||||
i.fill
|
||||
));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Fill swatch"));
|
||||
swatch_menu(app, &resp, &doc, &i.fill, |app, n| {
|
||||
let _ = app.run("object.fill", json!({"swatch": n}));
|
||||
@@ -615,7 +619,11 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
||||
let chip = sub(b, 4.0, 2.0, 17.0, 17.0);
|
||||
widgets::paint_stroke_chip(ui.painter(), chip, sc, egui::Color32::from_gray(20));
|
||||
ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
let resp = ui.interact(chip, ui.id().with("strokechip"), Sense::click()).on_hover_text(format!("Stroke: {}", i.stroke));
|
||||
let resp = ui.interact(chip, ui.id().with("strokechip"), Sense::click()).on_hover_text(format!(
|
||||
"{}: {}",
|
||||
crate::i18n::tr(&app.ui.language, "Stroke"),
|
||||
i.stroke
|
||||
));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Stroke swatch"));
|
||||
swatch_menu(app, &resp, &doc, &i.stroke, |app, n| {
|
||||
let _ = app.run("object.stroke", json!({"swatch": n}));
|
||||
@@ -695,7 +703,16 @@ fn align_row(app: &mut DesignApp, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.0;
|
||||
let to = app.ui.align_to.clone();
|
||||
let resp = icon_toggle(ui, "align-to", false, &format!("Align To: {to}"));
|
||||
let to_label = match to.as_str() {
|
||||
"selection" => "Selection",
|
||||
"keyObject" => "Key Object",
|
||||
"margins" => "Margins",
|
||||
"page" => "Page",
|
||||
"spread" => "Spread",
|
||||
other => other,
|
||||
};
|
||||
let tip = format!("{}: {}", crate::i18n::tr(&app.ui.language, "Align To"), crate::i18n::tr(&app.ui.language, to_label));
|
||||
let resp = icon_toggle(ui, "align-to", false, &tip);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
for (label, id) in [
|
||||
("Align to Selection", "selection"),
|
||||
@@ -785,7 +802,7 @@ fn convert_shape_menu(app: &mut DesignApp, resp: &egui::Response) {
|
||||
] {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() {
|
||||
if let Err(e) = app.run("object.convertShape", json!({"to": to})) {
|
||||
app.status(format!("Convert Shape: {e}"));
|
||||
app.status_error("Convert Shape", e);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
@@ -1896,7 +1913,12 @@ pub fn links_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
for l in &stories {
|
||||
let out = l["outOfDate"].as_bool().unwrap_or(false);
|
||||
ui.horizontal(|ui| {
|
||||
crate::rtl::label(ui, format!("Story {} ← story {}", l["story"], l["parent"]));
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
crate::i18n::tr(&app.ui.language, "Story {story} ← story {parent}")
|
||||
.replace("{story}", &l["story"].to_string())
|
||||
.replace("{parent}", &l["parent"].to_string()),
|
||||
);
|
||||
if out {
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
@@ -2008,7 +2030,7 @@ pub fn links_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
{
|
||||
let r = app.run("links.relinkFolder", json!({"dir": dir.to_string_lossy()}));
|
||||
if let Ok(v) = r {
|
||||
app.status(format!("Relinked {}", v["relinked"]));
|
||||
app.status(crate::i18n::tr(&app.ui.language, "Relinked {count}").replace("{count}", &v["relinked"].to_string()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -251,7 +251,11 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if k == stroke {
|
||||
ui.painter().rect_stroke(r, 2.0, egui::Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
let resp = resp.on_hover_text(format!("{label}: {n} (double-click: Color Picker)"));
|
||||
let resp = resp.on_hover_text(
|
||||
crate::i18n::tr(&app.ui.language, "{label}: {name} (double-click: Color Picker)")
|
||||
.replace("{label}", crate::i18n::tr(&app.ui.language, label))
|
||||
.replace("{name}", n),
|
||||
);
|
||||
if resp.clicked() {
|
||||
stroke = k;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Graphic…"))).clicked()
|
||||
&& let Err(e) = app.run("app.graphicCell", json!({}))
|
||||
{
|
||||
app.status(format!("Table: {e}"));
|
||||
app.status_error("Table", e);
|
||||
}
|
||||
if ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Text"))).clicked() {
|
||||
let _ = app.run("table.textCell", json!({}));
|
||||
@@ -55,7 +55,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if ui.add_enabled(ok, egui::Button::new(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).small()).clicked()
|
||||
&& let Err(e) = app.run(id, json!({"from": from, "to": to}))
|
||||
{
|
||||
app.status(format!("Table: {e}"));
|
||||
app.status_error("Table", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -76,7 +76,15 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
});
|
||||
if crate::icons::button(ui, "plus", 20.0, false, &format!("New {label}")).clicked() {
|
||||
if crate::icons::button(
|
||||
ui,
|
||||
"plus",
|
||||
20.0,
|
||||
false,
|
||||
crate::i18n::tr(&app.ui.language, if kind == "cell" { "New Cell Style" } else { "New Table Style" }),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if kind == "cell" {
|
||||
run(app, "style.cell.create", json!({"name": "Cell Style 1", "fromSelection": true}));
|
||||
} else {
|
||||
@@ -141,11 +149,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
if ui
|
||||
.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Distribute Rows Evenly")))
|
||||
.on_hover_text("Equal fixed heights for the selected rows; cell text may become overset.")
|
||||
.on_hover_text(crate::i18n::tr(&app.ui.language, "Equal fixed heights for the selected rows; cell text may become overset."))
|
||||
.clicked()
|
||||
&& let Err(e) = app.run("table.distributeRows", json!({}))
|
||||
{
|
||||
app.status(format!("Table: {e}"));
|
||||
app.status_error("Table", e);
|
||||
}
|
||||
divider(ui);
|
||||
caption(ui, crate::i18n::tr(&app.ui.language, "Cell Text"));
|
||||
|
||||
@@ -23,7 +23,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
let overset = cs.overset_at;
|
||||
let mut open = true;
|
||||
let mut edit: Option<(usize, usize, String)> = None;
|
||||
egui::Window::new(format!("Story Editor — {} words", story.text.split_whitespace().count()))
|
||||
let words = story.text.split_whitespace().count().to_string();
|
||||
egui::Window::new(crate::i18n::tr(&app.ui.language, "Story Editor — {count} words").replace("{count}", &words))
|
||||
.id(egui::Id::new("story_editor"))
|
||||
.open(&mut open)
|
||||
.default_size(vec2(560.0, 520.0))
|
||||
|
||||
@@ -164,7 +164,7 @@ fn text_controls(app: &mut DesignApp, ui: &mut Ui, in_text: bool) {
|
||||
if let Some(doc) = app.session.active().map(|d| d.doc.clone()) {
|
||||
let sw = c["fill"].as_str().unwrap_or("[Black]").to_string();
|
||||
let (col, g) = widgets::swatch_colors(&doc, &sw, 1.0);
|
||||
let resp = widgets::swatch_chip(ui, 17.0, col, g).on_hover_text(format!("Fill: {sw}"));
|
||||
let resp = widgets::swatch_chip(ui, 17.0, col, g).on_hover_text(format!("{}: {sw}", crate::i18n::tr(&app.ui.language, "Fill")));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Task Bar fill"));
|
||||
let mut picked = None;
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
|
||||
@@ -74,9 +74,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let g = TOOL_GROUPS[gi];
|
||||
let shown = g.iter().find(|tl| tl.id == current).unwrap_or(&g[0]);
|
||||
let active = g.iter().any(|tl| tl.id == current);
|
||||
let name = crate::i18n::tr(&app.ui.language, shown.label);
|
||||
let tip = match shown.shortcut {
|
||||
Some(s) => format!("{} ({s})", shown.label),
|
||||
None => shown.label.to_string(),
|
||||
Some(s) => format!("{name} ({s})"),
|
||||
None => name.to_string(),
|
||||
};
|
||||
let resp = tool_button(ui, shown.icon, active, &tip);
|
||||
if g.len() > 1 {
|
||||
@@ -135,7 +136,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.painter().text(
|
||||
r.min + vec2(28.0, 12.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
tl.label,
|
||||
crate::i18n::tr(&app.ui.language, tl.label),
|
||||
egui::FontId::proportional(12.5),
|
||||
t.text,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user