mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Strings in code stay English and are the lookup keys; `crates/ui-egui/src/i18n` maps them to display text at render time from one `*.tsv` catalog per language (`LANGUAGES` registry, `tl!`/`t`/`tr`/`tr_ctx`/`tr_id`/`tn`/`fmt`). Command ids, menu paths, `ui.menu.list`, the control channel, the CLI, MCP, status messages and undo labels never see translated text, so agents, scripts and the existing tests are unaffected (under `cargo test` the UI is always English). Wired: in-window menus and the macOS native menu (rebuilt on a language change), dynamic labels (Undo X, Reset X, Last Effect: X as templates), the command palette (matches English and translated text), dock tabs, panel tooltips, the toolbar (category names now cut by characters, not bytes), the dialog frame, the shared widgets, the Preferences dialog, the shortcut editor, the hint and status bars, the Home screen, and the literals of every panel and dialog. Preference `interfaceLanguage` (User Interface › Language): `auto` or a code. `auto` follows `VECTORCRAFT_LOCALE`, `LC_ALL`/`LC_MESSAGES`/`LANG`/`LANGUAGE`, the macOS preferred languages and, on Windows (which sets no `LANG`), the user locale from `HKCU\Control Panel\International`. `zh-TW`/`zh-HK`/`zh-MO`/ `zh-Hant-*` resolve to `zh-hant`; Simplified locales stay English until a `zh-hans` catalog exists. `zh-hant.tsv` (~2,850 rows) is clean-room, in the vocabulary used in Taiwan; terms shared with PhotoCraft reuse its wording, vector terms use everyday vector-drawing vocabulary. Tests: catalog well-formedness (placeholders, ellipses, command ids, no Simplified characters), locale resolution, and two coverage tests that fail when a menu string, panel/tool/preference label, dialog button, hint-bar fragment or `tl!` literal has no row in a language marked `complete_menus`. `VECTORCRAFT_I18N_DUMP=file cargo test -p vectorcraft-ui-egui dump_source_strings` lists the strings for translators. Also: two clippy findings under rustc 1.97 fixed in passing. Integrated with main (squashed onto it, 42 commits on): the Czech interface (#213) moves from its Rust table to `i18n/cs.tsv` on the registry (all 556 rows, Czech plural rule, its menu-coverage and glyph tests kept); Find Font's resolution notes and the canvas context menu's labels are translated and covered by the catalog tests (`menus::CONTEXT_LABELS`); the glyph test checks CJK text only when the craft-fonts build input is embedded (#220 moved the Japanese fonts out of this repository); a language saved by an older version carries over to `interfaceLanguage`; Windows' locale lookup runs reg.exe by its full path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
5.7 KiB
Rust
145 lines
5.7 KiB
Rust
//! File → Package…: where the package folder goes, its name, and what it collects (the linked
|
|
//! files, in a folder of their own or not, relinked or not; the fonts; the report). A document
|
|
//! never saved is saved first (Save As); unsaved changes are saved before packaging. The web
|
|
//! downloads the package as a zip.
|
|
//!
|
|
//! Fields: `folder` (the location), `name` (the package folder), `copyLinks`, `linksFolder`,
|
|
//! `relink`, `copyFonts`, `report`. OK runs `file.package` with them, then offers to show the
|
|
//! package folder (`file.showPackage`).
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::confirm::ask;
|
|
use super::{DialogSpec, form};
|
|
use crate::state::Dialog;
|
|
use crate::theme::Tokens;
|
|
use crate::{VectorcraftApp, io, widgets};
|
|
|
|
pub(super) const KIND: &str = "package";
|
|
const LABEL: f32 = 90.0;
|
|
/// The options: (field, label).
|
|
const OPTIONS: [(&str, &str); 5] = [
|
|
("copyLinks", "Copy Links"),
|
|
("linksFolder", "Collect Links in a Separate Folder"),
|
|
("relink", "Relink Linked Files to the Document"),
|
|
("copyFonts", "Copy Fonts Used in the Document"),
|
|
("report", "Create Report"),
|
|
];
|
|
|
|
pub(super) const SPEC: DialogSpec = DialogSpec {
|
|
heading: |_| tl!("Package").into(),
|
|
body,
|
|
confirm,
|
|
ok: Some("Package"),
|
|
min_width: 440.0,
|
|
max_width: Some(460.0),
|
|
..DialogSpec::FORM
|
|
};
|
|
|
|
/// Is there no file system to write a folder to (the web downloads a zip)?
|
|
fn downloads(app: &VectorcraftApp) -> bool {
|
|
app.services.download.is_some()
|
|
}
|
|
|
|
/// Open the dialog for the active document; one never saved is offered Save As first.
|
|
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
|
let st = app.session.active().ok_or("no document")?;
|
|
let Some(path) = st.path.clone() else {
|
|
ask(
|
|
app,
|
|
tl!("Save the document first?"),
|
|
tl!("Package collects a saved document with its linked files and fonts."),
|
|
"file.saveAs",
|
|
json!({}),
|
|
);
|
|
return Ok(Value::Null);
|
|
};
|
|
let p = std::path::Path::new(&path);
|
|
let stem = p.file_stem().map_or_else(|| "Untitled".into(), |s| s.to_string_lossy().into_owned());
|
|
let folder = p.parent().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default();
|
|
let mut fields = json!({ "folder": folder, "name": format!("{stem} Folder") });
|
|
for (k, _) in OPTIONS {
|
|
fields[k] = json!(true);
|
|
}
|
|
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
|
let t = Tokens::get(ui.ctx());
|
|
if !downloads(app) {
|
|
widgets::label_row(ui, tl!("Location:"), LABEL, |ui| {
|
|
form::text(ui, d, "folder", 230.0);
|
|
if let Some(pick) = app.services.pick_folder.as_mut()
|
|
&& ui.button(tl!("Choose…")).clicked()
|
|
&& let Some(f) = pick()
|
|
{
|
|
d.fields.insert("folder".into(), json!(f));
|
|
}
|
|
});
|
|
ui.add_space(4.0);
|
|
}
|
|
widgets::label_row(ui, tl!("Folder Name:"), LABEL, |ui| {
|
|
form::text(ui, d, "name", 230.0);
|
|
});
|
|
ui.add_space(10.0);
|
|
widgets::subheader(ui, tl!("Options"));
|
|
for (k, label) in OPTIONS {
|
|
// The link options apply only when the links are copied.
|
|
let enabled = !matches!(k, "linksFolder" | "relink") || d.bool("copyLinks");
|
|
let on = d.bool(k);
|
|
if widgets::check(ui, label, on, enabled) {
|
|
d.fields.insert(k.into(), json!(!on));
|
|
}
|
|
}
|
|
ui.add_space(6.0);
|
|
ui.label(egui::RichText::new(tl!("Fonts whose licence doesn't allow embedding are not copied.")).size(11.5).color(t.text_dim));
|
|
false
|
|
}
|
|
|
|
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
|
let mut params = Value::Object(d.fields.clone());
|
|
let web = downloads(app);
|
|
if web {
|
|
params["folder"] = Value::Null;
|
|
} else if d.str("folder").trim().is_empty() {
|
|
return Err("choose a location for the package".into());
|
|
}
|
|
// The package holds the document as saved.
|
|
if app.session.active().is_some_and(|st| st.is_dirty()) {
|
|
io::save(app, vectorcraft_engine::cmd::fileio::SaveMode::Save, &json!({}), false)?;
|
|
}
|
|
let r = app.run("file.package", params)?;
|
|
app.ui.dialog = None;
|
|
let n = r["files"].as_array().map_or(0, Vec::len);
|
|
let missing = r["missingLinks"].as_array().map_or(0, Vec::len);
|
|
let note = if missing > 0 { format!(" ({missing} linked file(s) not found)") } else { String::new() };
|
|
if web {
|
|
let (name, data) = (r["name"].as_str().unwrap_or("package.zip"), r["dataBase64"].as_str().unwrap_or_default());
|
|
let bytes = vectorcraft_format::base64_decode(data).ok_or("the package came back damaged")?;
|
|
io::write_named(app, None, name, &bytes)?;
|
|
app.status(format!("Packaged {n} file(s){note}"));
|
|
return Ok(r);
|
|
}
|
|
let folder = r["folder"].as_str().unwrap_or_default().to_string();
|
|
app.status(format!("Packaged {n} file(s) in {folder}{note}"));
|
|
let shown_note = if missing > 0 {
|
|
format!(" ({})", crate::i18n::fmt(tl!("{count} linked file(s) not found"), &[("count", &missing.to_string())]))
|
|
} else {
|
|
String::new()
|
|
};
|
|
let detail = crate::i18n::fmt(
|
|
tl!("{count} file(s) in {folder}{note}. Show the package folder?"),
|
|
&[("count", &n.to_string()), ("folder", &folder), ("note", &shown_note)],
|
|
);
|
|
ask(app, tl!("Package created"), &detail, "file.showPackage", json!({ "folder": folder }));
|
|
Ok(r)
|
|
}
|
|
|
|
/// `file.showPackage {folder}`: show a package folder in the file manager.
|
|
pub fn show_package(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
|
let folder = p.get("folder").and_then(Value::as_str).ok_or("missing `folder`")?.to_string();
|
|
io::open_in_app(app, &folder)?;
|
|
Ok(json!({ "folder": folder }))
|
|
}
|