mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
i18n: the untranslated report checks the effects, map_err messages and schema phrases
The untranslated report missed messages that reach the status bar:
- MESSAGE_CRATES now includes effects, color, render, three-d and pathops,
whose errors the engine and the effects pass on (SVG filters, swatch
libraries, raster sizes and encoders, 3D solids, path operations).
- The scan reads the messages a .map_err closure returns, from each if
branch, match arm and the last expression of a block, the messages of
a handler that .map_err(name) names (a closure bound with let, or a
function), and the message given to a variant of an enum whose name
ends in Fail (CompileFail::Syntax). The steps of a script result's
path ([{i}]{place}, .{k}{place}) are not messages.
- The report and the VECTORCRAFT_I18N_DUMP_MESSAGES dump include the
phrases a parameter schema's error builds at run time (SCHEMA_PHRASES).
Missing translations stay report-only. On main at 0b29b2b1f6 the report
lists 126 more messages, missing in all 23 languages that translate
every message: 43,084 missing rows before, 45,983 after.
This commit is contained in:
@@ -1486,7 +1486,8 @@ fn string_literals(e: &str, keep: impl Fn(&str, &str) -> bool) -> Vec<String> {
|
||||
|
||||
/// For translators, `cargo test -p vectorcraft-ui-egui dump_source_strings` writes every English
|
||||
/// interface string to `VECTORCRAFT_I18N_DUMP=<file>`, and every status and error message as a
|
||||
/// catalog template to `VECTORCRAFT_I18N_DUMP_MESSAGES=<file>`, one per line.
|
||||
/// catalog template that [`untranslated_report`] checks to `VECTORCRAFT_I18N_DUMP_MESSAGES=<file>`,
|
||||
/// one per line.
|
||||
#[test]
|
||||
fn dump_source_strings() {
|
||||
if let Ok(path) = std::env::var("VECTORCRAFT_I18N_DUMP") {
|
||||
@@ -1498,10 +1499,27 @@ fn dump_source_strings() {
|
||||
std::fs::write(path, text).expect("write dump");
|
||||
}
|
||||
if let Ok(path) = std::env::var("VECTORCRAFT_I18N_DUMP_MESSAGES") {
|
||||
std::fs::write(path, message_literals().iter().map(|m| format!("{m}\n")).collect::<String>()).expect("write dump");
|
||||
std::fs::write(path, message_dump()).expect("write dump");
|
||||
}
|
||||
}
|
||||
|
||||
/// The text [`dump_source_strings`] writes to `VECTORCRAFT_I18N_DUMP_MESSAGES`.
|
||||
fn message_dump() -> String {
|
||||
reported_messages().iter().map(|m| format!("{m}\n")).collect()
|
||||
}
|
||||
|
||||
/// The translators' message dump lists every message the untranslated report checks, the
|
||||
/// phrases a parameter schema's error builds included.
|
||||
#[test]
|
||||
fn message_dump_lists_the_reported_messages() {
|
||||
let dump = message_dump();
|
||||
let lines: std::collections::BTreeSet<&str> = dump.lines().collect();
|
||||
for phrase in SCHEMA_PHRASES {
|
||||
assert!(lines.contains(phrase), "the dump skips {phrase:?}");
|
||||
}
|
||||
assert!(lines.contains("the color book is cut short"));
|
||||
}
|
||||
|
||||
fn cs() -> Lang {
|
||||
Lang::from_code("cs").expect("cs registered")
|
||||
}
|
||||
@@ -1640,7 +1658,7 @@ fn untranslated_report() {
|
||||
undo.extend(liquify.iter().filter_map(|k| vectorcraft_tools::distort::liquify::LiquifyKind::parse(k)).map(|k| format!("{} Tool", k.label())));
|
||||
assert_eq!(scan.formatted.iter().collect::<Vec<_>>(), ["{} Tool"], "step names built with format! (make them with Session::edit_named)");
|
||||
assert!(scan.templates.len() >= 3, "scan found only the templates {:?}", scan.templates);
|
||||
let messages = message_literals();
|
||||
let messages = reported_messages();
|
||||
assert!(messages.len() > 300, "scan found only {} messages", messages.len());
|
||||
let (swatches, groups) = vectorcraft_color::default_swatches(vectorcraft_color::cms::Model::Rgb);
|
||||
// The greys' `K=` values read the same in any language.
|
||||
@@ -1741,6 +1759,36 @@ fn untranslated_report() {
|
||||
println!("{out}");
|
||||
}
|
||||
|
||||
/// The status and error messages [`untranslated_report`] checks: the message literals of
|
||||
/// [`MESSAGE_CRATES`] and the phrases a parameter schema's error builds at run time
|
||||
/// ([`SCHEMA_PHRASES`]), which no literal holds.
|
||||
fn reported_messages() -> std::collections::BTreeSet<String> {
|
||||
let mut messages = message_literals();
|
||||
messages.extend(SCHEMA_PHRASES.iter().map(|p| p.to_string()));
|
||||
messages
|
||||
}
|
||||
|
||||
/// The untranslated report checks the messages of the effects and of the crates whose errors the
|
||||
/// engine and the effects pass on, and the phrases a parameter schema's error builds.
|
||||
#[test]
|
||||
fn untranslated_report_checks_every_message_crate_and_schema_phrase() {
|
||||
let reported = reported_messages();
|
||||
let passed_on = [
|
||||
("effects", "the SVG filter has no primitives"),
|
||||
("color", "not a swatch library (.vcswatches or .gpl)"),
|
||||
("render", "invalid scale {scale}"),
|
||||
("three-d", "3D effects require one to ten lights"),
|
||||
("pathops", "the shapes are too degenerate to combine"),
|
||||
];
|
||||
for (krate, m) in passed_on {
|
||||
assert!(message_literals_in(&[krate]).contains(m), "{krate}: {m}");
|
||||
assert!(reported.contains(m), "the report skips {krate}: {m}");
|
||||
}
|
||||
for phrase in SCHEMA_PHRASES {
|
||||
assert!(reported.contains(*phrase), "the report skips the schema phrase {phrase:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The names [`MENU_KEEP_AS_IS`] lists are menu labels, and no catalog that keeps them in English
|
||||
/// translates them.
|
||||
#[test]
|
||||
@@ -2723,6 +2771,84 @@ fn f(i: &mut Interp, interp: &mut Interp) -> JsResult<Value> {
|
||||
assert_eq!(found, ["{what}: the object is not a File or a Folder", "File.seek: the mode must be 0, 1 or 2", "the path has an empty step"]);
|
||||
}
|
||||
|
||||
/// The scan reads the message a `.map_err` closure returns, a literal, a `format!` string or a
|
||||
/// constant, whatever the closure's parameter, from each branch of an `if` and each arm of a
|
||||
/// `match`, and given to a variant of an enum whose name ends in `Fail`, and not a closure that
|
||||
/// passes the error on, a `json!` value's keys, nor the steps of a script result's path
|
||||
/// ([`NOT_MESSAGES`]).
|
||||
#[test]
|
||||
fn message_scan_reads_map_err_closures() {
|
||||
let code = r#"
|
||||
fn f(text: &str, n: usize) -> Result<u32, String> {
|
||||
let doc = parse(text).map_err(|e| format!("the filter isn't valid XML: {e}"))?;
|
||||
let a = at(doc).map_err(|place| format!("[{i}]{place}"))?;
|
||||
let b = at(doc).map_err(|place| format!(".{k}{place}"))?;
|
||||
let c = at(doc).map_err(|e| format!("{name}: {e}"))?;
|
||||
let n = u32::try_from(n).map_err(|_| "too many blocks".to_string())?;
|
||||
let m = u16::try_from(n).map_err(move |(a, b)| LIMIT.into())?;
|
||||
let k = read(doc).map_err(|e| e.to_string())?;
|
||||
let j = read(doc).map_err(|e| bad(C, e))?;
|
||||
let i = read(doc).map_err(|e| if e == "{x}" { CUT_SHORT.into() } else { e })?;
|
||||
let h = read(doc).map_err(|e| match e {
|
||||
Error::Io(_) => "the file can't be read".to_string(),
|
||||
other => fail(json!({"objects": [other.to_string()]})),
|
||||
})?;
|
||||
let g = i32::try_from(n).map_err(|_| CompileFail::Syntax("the pattern is too large".into()))?;
|
||||
let f = read(doc).map_err(|e| {
|
||||
self.published = None;
|
||||
format!("the clipboard can't be read: {e}")
|
||||
})?;
|
||||
Ok(n)
|
||||
}
|
||||
"#;
|
||||
let consts =
|
||||
[("LIMIT", "the name is too long"), ("CUT_SHORT", "the book is cut short")].map(|(k, v)| (k.to_string(), vec![v.to_string()])).into();
|
||||
let want = [
|
||||
"the pattern is too large",
|
||||
"the filter isn't valid XML: {e}",
|
||||
"{name}: {e}",
|
||||
"too many blocks",
|
||||
"the name is too long",
|
||||
"the book is cut short",
|
||||
"the file can't be read",
|
||||
"the clipboard can't be read: {e}",
|
||||
];
|
||||
assert_eq!(source_messages(code, &[], &consts), want);
|
||||
assert!(message_literals_in(&["color"]).contains("the color book is cut short"));
|
||||
assert!(message_literals_in(&["color"]).contains("not a swatch library: {e}"));
|
||||
assert!(message_literals_in(&["js"]).contains("regular expression too large"));
|
||||
assert!(message_literals_in(&["effects"]).contains("the SVG filter isn't valid XML: {e}"));
|
||||
}
|
||||
|
||||
/// The scan reads the messages of an error handler that a `.map_err(name)` names: a closure bound
|
||||
/// to that name, up to its `;`, and a function of that name, each of its statements.
|
||||
#[test]
|
||||
fn message_scan_reads_named_map_err_handlers() {
|
||||
let code = r#"
|
||||
fn encode(img: &Image) -> Result<Vec<u8>, String> {
|
||||
let fail = |e: gif::EncodingError| format!("GIF encoding failed: {e}");
|
||||
let gone = move |_| GONE.to_string();
|
||||
let size = if img.is_empty() { WRONG.into() } else { 0 };
|
||||
let mut enc = Encoder::new(img).map_err(fail)?;
|
||||
enc.write(img).map_err(fail)?;
|
||||
enc.check().map_err(gone)?;
|
||||
enc.close().map_err(unreadable)?;
|
||||
enc.into_inner().map_err(Error::Io)
|
||||
}
|
||||
|
||||
fn unreadable(e: impl std::fmt::Display) -> String {
|
||||
let e = e.to_string();
|
||||
format!("the file can't be read: {e}")
|
||||
}
|
||||
"#;
|
||||
let consts = [("GONE", "the encoder is gone"), ("WRONG", "the image is wrong")].map(|(k, v)| (k.to_string(), vec![v.to_string()])).into();
|
||||
let want = ["GIF encoding failed: {e}", "the encoder is gone", "the file can't be read: {e}"];
|
||||
assert_eq!(source_messages(code, &[], &consts), want);
|
||||
let render = message_literals_in(&["render"]);
|
||||
assert!(render.contains("GIF encoding failed: {e}") && render.contains("JPEG encoding failed: {e}"));
|
||||
assert!(message_literals_in(&["engine"]).contains("Data Recovery can't keep copies in {path}: {e}"));
|
||||
}
|
||||
|
||||
/// The scan stops at a test module whose `cfg` is `test` or `all(test, …)`, and reads on past a
|
||||
/// declaration of a test file and a module that is not only for tests.
|
||||
#[test]
|
||||
@@ -3880,8 +4006,13 @@ const COMPLETE_MESSAGES: &[&str] = &[
|
||||
"nl",
|
||||
];
|
||||
|
||||
/// Crates whose error and status messages reach the status bar.
|
||||
const MESSAGE_CRATES: &[&str] = &["ui-egui", "engine", "doc", "format", "svg", "pdf", "eps", "text", "plugins", "metafile", "cad", "trace", "js"];
|
||||
/// Crates whose error and status messages reach the status bar, the crates whose errors the
|
||||
/// engine and the effects pass on included (effects, swatch libraries, raster sizes and encoders,
|
||||
/// 3D solids, path operations).
|
||||
const MESSAGE_CRATES: &[&str] = &[
|
||||
"ui-egui", "engine", "doc", "format", "svg", "pdf", "eps", "text", "plugins", "metafile", "cad", "trace", "js", "effects", "color", "render",
|
||||
"three-d", "pathops",
|
||||
];
|
||||
|
||||
/// Where a message literal starts: a status, an error value, a `thiserror` message.
|
||||
const MESSAGE_MARKERS: &[&str] = &[".status(", "ui.status = ", "status = ", "Other(", "Err(", "ok_or(", "ok_or_else(|| ", "#[error("];
|
||||
@@ -3926,9 +4057,10 @@ const NAME_VARIANTS: &[&str] = &["UnknownCommand", "Ps"];
|
||||
/// `EngineError::Disabled(command id, reason)`.
|
||||
const LATER_MESSAGE_VARIANTS: &[(&str, usize)] = &[("Disabled", 1)];
|
||||
|
||||
/// Messages that are signals, not text for people, and the name of a plug-in export that an
|
||||
/// error message holds (`vc_manifest points outside memory`).
|
||||
const NOT_MESSAGES: &[&str] = &["quit", "cancelled", "vc_manifest"];
|
||||
/// Messages that are signals, not text for people, the name of a plug-in export that an error
|
||||
/// message holds (`vc_manifest points outside memory`), and the steps of the path to a script
|
||||
/// result's part that an error message holds (`vc.run: the result{place} is too large for a script`).
|
||||
const NOT_MESSAGES: &[&str] = &["quit", "cancelled", "vc_manifest", "[{i}]{place}", ".{k}{place}"];
|
||||
|
||||
/// Every status and error message literal in [`MESSAGE_CRATES`] (test modules aside), as catalog
|
||||
/// templates: `{}` becomes `{_1}`, `{_2}` … and format specs are dropped (`{n:?}` → `{n}`).
|
||||
@@ -4046,8 +4178,11 @@ fn named_const<'a>(rest: &str, consts: &'a std::collections::BTreeMap<String, Ve
|
||||
|
||||
/// The message literals in `code` outside its comments, as catalog templates (see
|
||||
/// [`message_literals`]): the literal after each of [`MESSAGE_MARKERS`], the message argument of an
|
||||
/// error enum's variant (the first, as in `PsError::Limit(…)` and `Error::Manifest(…)`, or the one
|
||||
/// error enum's variant, an enum whose name ends in `Error` or `Fail` (the first argument, as in
|
||||
/// `PsError::Limit(…)`, `Error::Manifest(…)` and `CompileFail::Syntax(…)`, or the one
|
||||
/// [`LATER_MESSAGE_VARIANTS`] gives; not [`NAME_VARIANTS`]),
|
||||
/// the messages a `.map_err` closure returns (`.map_err(|e| format!("…: {e}"))`, see
|
||||
/// [`closure_returns`]) or the handler a `.map_err(name)` names returns (see [`handler_returns`]),
|
||||
/// and the message argument of each call in `helpers` (a call and the number of arguments before
|
||||
/// its message), a literal, a `format!` string or the name of one of `consts` (see
|
||||
/// [`str_consts`]), each one with or without a `&` before it. A key's name alone (`` `tint` ``)
|
||||
@@ -4055,20 +4190,34 @@ fn named_const<'a>(rest: &str, consts: &'a std::collections::BTreeMap<String, Ve
|
||||
fn source_messages(code: &str, helpers: &[(&str, usize)], consts: &std::collections::BTreeMap<String, Vec<String>>) -> Vec<String> {
|
||||
let code = &without_comments(code);
|
||||
let after_markers = MESSAGE_MARKERS.iter().flat_map(|marker| code.match_indices(marker).map(|(at, _)| &code[at + marker.len()..]));
|
||||
let in_variants = code.match_indices("Error::").filter_map(|(at, m)| {
|
||||
let in_variants = ["Error::", "Fail::"].into_iter().flat_map(|enum_end| code.match_indices(enum_end)).filter_map(|(at, m)| {
|
||||
let rest = &code[at + m.len()..];
|
||||
let (variant, args) = rest.split_at(rest.find(|c: char| !c.is_ascii_alphanumeric() && c != '_').unwrap_or(rest.len()));
|
||||
let named = variant.starts_with(|c: char| c.is_ascii_uppercase()) && !NAME_VARIANTS.contains(&variant);
|
||||
let before = LATER_MESSAGE_VARIANTS.iter().find(|(v, _)| *v == variant).map_or(0, |&(_, n)| n);
|
||||
if named { skip_args(args.strip_prefix('(')?, before) } else { None }
|
||||
});
|
||||
let in_map_err = code.match_indices(".map_err(").flat_map(|(at, m)| {
|
||||
let rest = code[at + m.len()..].trim_start();
|
||||
let params = rest.strip_prefix("move").map_or(rest, str::trim_start).strip_prefix('|');
|
||||
params.and_then(|p| p.find('|').map(|end| &p[end + 1..])).map(closure_returns).unwrap_or_default()
|
||||
});
|
||||
let mut handlers = vec![];
|
||||
for (at, m) in code.match_indices(".map_err(") {
|
||||
let rest = &code[at + m.len()..];
|
||||
let name = &rest[..rest.find(|c: char| !c.is_ascii_alphanumeric() && c != '_').unwrap_or(rest.len())];
|
||||
if name.starts_with(|c: char| c.is_ascii_lowercase()) && rest[name.len()..].starts_with(')') && !handlers.contains(&name) {
|
||||
handlers.push(name);
|
||||
}
|
||||
}
|
||||
let in_handlers = handlers.into_iter().flat_map(|name| handler_returns(code, name));
|
||||
let in_helpers = helpers.iter().flat_map(|&(call, before)| {
|
||||
code.match_indices(call)
|
||||
.filter(|&(at, _)| call.starts_with('.') || !code[..at].ends_with(|c: char| c.is_alphanumeric() || c == '_' || c == '.'))
|
||||
.filter_map(move |(at, _)| skip_args(&code[at + call.len()..], before))
|
||||
});
|
||||
let mut out = vec![];
|
||||
for rest in after_markers.chain(in_variants).chain(in_helpers) {
|
||||
for rest in after_markers.chain(in_variants).chain(in_map_err).chain(in_handlers).chain(in_helpers) {
|
||||
let rest = rest.trim_start();
|
||||
let rest = rest.strip_prefix('&').map_or(rest, str::trim_start);
|
||||
let rest = rest.strip_prefix("format!(").map_or(rest, str::trim_start);
|
||||
@@ -4119,6 +4268,94 @@ fn skip_args(args: &str, n: usize) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Where each value the error handler `name` of a `.map_err(name)` in `code` can return starts:
|
||||
/// the closure the file binds to that name (`let fail = |e| format!("…: {e}");`, up to its `;`)
|
||||
/// or the body of the function of that name (`fn unreadable(e: …) -> String { … }`), read as
|
||||
/// [`closure_returns`] reads a closure.
|
||||
fn handler_returns<'a>(code: &'a str, name: &str) -> Vec<&'a str> {
|
||||
let (bound, declared) = (format!("let {name} = "), format!("fn {name}("));
|
||||
let mut out = vec![];
|
||||
for (at, m) in code.match_indices(bound.as_str()).chain(code.match_indices(declared.as_str())) {
|
||||
if code[..at].ends_with(|c: char| c.is_alphanumeric() || c == '_') {
|
||||
continue;
|
||||
}
|
||||
let rest = &code[at + m.len()..];
|
||||
if m == bound {
|
||||
let rest = rest.strip_prefix("move").map_or(rest, str::trim_start);
|
||||
if let Some(body) = rest.strip_prefix('|').and_then(|p| p.find('|').map(|end| &p[end + 1..])) {
|
||||
out.extend(closure_returns(statement(body)));
|
||||
}
|
||||
} else if let Some(open) = rest.find('{') {
|
||||
out.extend(closure_returns(&rest[open + 1..]));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The statement `code` starts with, up to its `;` or the bracket that closes the block it is in.
|
||||
fn statement(code: &str) -> &str {
|
||||
let (mut depth, mut chars) = (0usize, code.char_indices());
|
||||
while let Some((at, c)) = chars.next() {
|
||||
match c {
|
||||
'(' | '[' | '{' => depth += 1,
|
||||
')' | ']' | '}' => match depth.checked_sub(1) {
|
||||
Some(d) => depth = d,
|
||||
None => return &code[..at],
|
||||
},
|
||||
';' if depth == 0 => return &code[..at],
|
||||
'"' => loop {
|
||||
match chars.next() {
|
||||
Some((_, '\\')) => {
|
||||
chars.next();
|
||||
}
|
||||
Some((_, '"')) | None => break,
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
/// Where each value a closure can return starts, `body` being the text after its parameters: the
|
||||
/// body itself, each block or `if`/`else` branch in it (`{ ACB_CUT_SHORT.into() }`, not the
|
||||
/// `{` of a `json!({…})` value), each `match` arm (`=>`) and each statement after a `;` (the
|
||||
/// last expression of a block or a function body), up to the bracket that closes the call the
|
||||
/// closure is given to or the function.
|
||||
fn closure_returns(body: &str) -> Vec<&str> {
|
||||
let mut out = vec![body];
|
||||
let (mut depth, mut chars) = (0usize, body.char_indices());
|
||||
while let Some((at, c)) = chars.next() {
|
||||
match c {
|
||||
'(' | '[' => depth += 1,
|
||||
'{' => {
|
||||
depth += 1;
|
||||
if !body[..at].trim_end().ends_with('(') {
|
||||
out.push(&body[at + 1..]);
|
||||
}
|
||||
}
|
||||
')' | ']' | '}' => match depth.checked_sub(1) {
|
||||
Some(d) => depth = d,
|
||||
None => break,
|
||||
},
|
||||
';' => out.push(&body[at + 1..]),
|
||||
'=' if body[at + 1..].starts_with('>') => out.push(&body[at + 2..]),
|
||||
'"' => loop {
|
||||
match chars.next() {
|
||||
Some((_, '\\')) => {
|
||||
chars.next();
|
||||
}
|
||||
Some((_, '"')) | None => break,
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Source up to its inline test module (`#[cfg(test)] mod tests {`, or a `cfg(all(test, …))` one);
|
||||
/// declarations of test files (`#[cfg(test)] mod tests_x;`) don't end it.
|
||||
fn without_test_module(text: &str) -> &str {
|
||||
|
||||
+6
-2
@@ -296,10 +296,14 @@ other `sv-*` locale resolve to it), Georgian (`ka`, complete; `ka-GE` and `ka_GE
|
||||
`Couldn't open {name}: {e}`; `{_1}`, `{_2}` … stand for the format string's `{}`, and the values in the
|
||||
placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Greek, Spanish, French, Georgian, Hungarian, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Traditional Chinese, Swedish, Ukrainian, Vietnamese, Finnish, Turkish, Catalan and Dutch
|
||||
translate status and error messages (`COMPLETE_MESSAGES` in `i18n::tests`); the untranslated report lists the
|
||||
message literals the scan finds that one of them has no row for. The scan reads `Err("…")`, `Other(…)`,
|
||||
message literals the scan finds, and the phrases a parameter schema's error builds at run time
|
||||
(`SCHEMA_PHRASES`), that one of them has no row for. The scan covers the crates in `MESSAGE_CRATES`, the
|
||||
effects and the crates whose errors the engine passes on included. It reads `Err("…")`, `Other(…)`,
|
||||
`#[error(…)]` and `status(…)` messages, messages given to an error enum's variant (`PsError::Limit(…)`, the reason
|
||||
in `EngineError::Disabled(id, …)`), to the engine's `bad(cmd, …)`, `err(…)` or `error(…)`, and to the PDF writer's
|
||||
`bad(…)` or `failed(…)`, and skips calls inside comments
|
||||
`bad(…)` or `failed(…)`, and the messages a `.map_err(|e| …)` closure returns (from each `if` branch,
|
||||
`match` arm and statement of a block) or the handler a `.map_err(name)` names returns (a closure bound with `let name = |e| …;` or a
|
||||
function `fn name(…)`), and skips calls inside comments
|
||||
(`VECTORCRAFT_I18N_DUMP_MESSAGES=messages.txt cargo test -p vectorcraft-ui-egui dump_source_strings` lists them
|
||||
all). A missing row shows the English message. Other languages show messages in English until they add `@msg`
|
||||
rows.
|
||||
|
||||
@@ -907,6 +907,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- The errors of the 3D effects (Extrude & Bevel, Revolve, Inflate, Rotate and their Classic forms) show in the status bar in the interface language: the art a 3D effect cannot build a solid from, invalid light, graphics and surface settings, and the notes for an object that draws no 3D faces or more wireframe edges than the limit. Each has a row in every catalog that translates all status and error messages, and the effect's name and the values in the message are translated or kept as written (`three_d_refusals_are_translated_in_every_complete_catalog`, `three_d_refusals_translate_with_their_values`).
|
||||
- The errors of the parameter checks that many commands share show in the status bar in the interface language: a value that is not a number where a number goes (`object.move` with `"dx": "60 mm"`, and `object.rotate`, `object.setBounds` and the other commands that call `check_numbers`), a missing number or index, `ids` that are not a list of object ids, an object id that is not a non-negative integer or names no object, and a date that is not Unix seconds. Each has a row in every catalog that translates all status and error messages, and in Bulgarian, Danish, Indonesian and Dutch (`shared_param_check_errors_are_translated`).
|
||||
- A parameter schema's errors show in the status bar with what the value must be in the interface language, for every type a schema has (a number with each kind of range and with a unit, an integer and its range, true or false, a string, a list of names, an object id, a list of object ids, a color, a point, an object and a list), in every catalog that translates all status and error messages; the path, the refused value, the bounds, the unit and the names stay as the English gives them. The error for a value that is not one of its names is also translated in Bulgarian, Bosnian, Danish, Indonesian and Dutch (`parameter_schema_expected_phrases_are_translated`, `bracketed_message_parts_translate_only_as_exact_messages`).
|
||||
- The untranslated report lists the status and error messages of the effects, swatch libraries, raster sizes and encoders, 3D solids and path operations that a catalog lacks, the messages that a `.map_err` closure or a named error handler returns, and the phrases a parameter schema's error builds for what a value must be (`untranslated_report_checks_every_message_crate_and_schema_phrase`, `message_scan_reads_map_err_closures`, `message_scan_reads_named_map_err_handlers`).
|
||||
- Export commands run through MCP, the control channel or the CLI refuse a key that no format takes, and warn about a key that only another format takes (`{"format": "pdf", "quality": 80}` writes the PDF with the warning "`quality` was not used: PDF files don't read it"; `an_option_only_another_format_reads_gets_a_note`, `control_export_notes_an_option_the_format_does_not_read`, `convert_warns_about_an_unused_option_only_when_one_was_given`), and a command that writes one format (`document.exportPdf`, `document.exportEps`, `document.exportDxf`) or takes one format's options (`document.pdfSettings`, `pdf.preset.save`) refuses a key that format doesn't take. An unknown key inside `marks`, `bleed`, `compression`, `output`, `advanced`, `security` or `flattener` is refused by its path.
|
||||
- Save commands run through MCP, the control channel or the CLI (`document.save`, `file.saveAs`, `file.saveCopy`, `file.saveAsTemplate`, `app.save`) refuse a key that no save format takes, beside the path or in `options`. Export for Screens refuses a key it doesn't take, and a key that no format takes in a format row or in a format's settings, by its path (`formats[1].marks.useDocumentBleed`); Export Assets (`assets.export`, `assets.settings.set`) checks the rows and settings it is given the same way.
|
||||
- Commands that read their `ids` (or `slices`) list themselves refuse a list with an entry that is not a non-negative integer, and a value that is not a list, and change nothing; they no longer drop the entry and act on the rest (Copy Appearance From, the Edit Colors commands and Overprint Black, Find and Replace, `layer.highlight`, `layer.move`, `library.add`, the Links panel commands, `object.transform`, Make Pattern, the Repeat commands, the slice commands, Join, Average and Join, `plugin.run`, `symbol.edit`, Make Opacity Mask; `tests::ids_without_selection::an_ids_entry_that_is_not_an_id_is_refused_and_changes_nothing`). The error for an entry of an object id list names the key, the entry and its value ("`ids` entry 1 is true, not a non-negative integer object id"), with a row in every catalog that has the other object id errors.
|
||||
|
||||
Reference in New Issue
Block a user