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:
aiongg
2026-10-11 18:01:03 +00:00
21 changed files with 385 additions and 100 deletions
+7 -6
View File
@@ -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);
}
+2 -2
View File
@@ -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();
}
}
+23 -17
View File
@@ -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 {
+18 -17
View File
@@ -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;
+83
View File
@@ -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 [
+133 -1
View File
@@ -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")];
+23 -4
View File
@@ -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, &params) {
@@ -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.
+6 -6
View File
@@ -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);
+1 -1
View File
@@ -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();
+3 -3
View File
@@ -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),
}
}
});
+1 -1
View File
@@ -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(
+15 -12
View File
@@ -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}));
+3 -3
View File
@@ -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),
+13 -9
View File
@@ -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}")),
}
}
+1 -1
View File
@@ -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));
+28 -6
View File
@@ -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()));
}
}
});
+5 -1
View File
@@ -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;
}
+13 -5
View File
@@ -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"));
+2 -1
View File
@@ -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))
+1 -1
View File
@@ -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| {
+4 -3
View File
@@ -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,
);