mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
Merge pull request #4222 from dotCooCoo/fix/i18n-scan-skips-comments-reads-disabled-reason
i18n: the message scan skips comments and reads EngineError::Disabled's reason
This commit is contained in:
@@ -608,7 +608,7 @@ fn undo_scan() -> UndoScan {
|
||||
UndoScan { labels: out, templates, formatted }
|
||||
}
|
||||
|
||||
/// The sources of a crate's `src` directory, test modules and files and `//` comments aside.
|
||||
/// The sources of a crate's `src` directory, test modules and files and comments aside.
|
||||
fn undo_sources(dir: &std::path::Path) -> Vec<String> {
|
||||
let mut out = vec![];
|
||||
let mut stack = vec![dir.to_path_buf()];
|
||||
@@ -629,7 +629,8 @@ fn undo_sources(dir: &std::path::Path) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
/// `text` with each `//` comment removed (a `//` inside a string literal stays).
|
||||
/// `text` with each comment removed: a `//` comment, and a `/* … */` comment (nested ones
|
||||
/// included), which becomes a space. A `//` or `/*` inside a string or character literal stays.
|
||||
fn without_comments(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut i = 0;
|
||||
@@ -639,6 +640,9 @@ fn without_comments(text: &str) -> String {
|
||||
i = end;
|
||||
} else if text[i..].starts_with("//") {
|
||||
i = text[i..].find('\n').map_or(text.len(), |n| i + n);
|
||||
} else if text[i..].starts_with("/*") {
|
||||
i = block_comment_end(text, i);
|
||||
out.push(' ');
|
||||
} else {
|
||||
out.push(c);
|
||||
i += c.len_utf8();
|
||||
@@ -647,6 +651,27 @@ fn without_comments(text: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// Where the `/* … */` comment starting at `text[at]` ends, past the comments nested in it; the
|
||||
/// end of `text` when the comment is not closed.
|
||||
fn block_comment_end(text: &str, at: usize) -> usize {
|
||||
let (mut depth, mut i) = (0usize, at);
|
||||
while let Some(c) = text[i..].chars().next() {
|
||||
if text[i..].starts_with("/*") {
|
||||
depth += 1;
|
||||
i += 2;
|
||||
} else if text[i..].starts_with("*/") {
|
||||
depth = depth.saturating_sub(1);
|
||||
i += 2;
|
||||
if depth == 0 {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
i += c.len_utf8();
|
||||
}
|
||||
}
|
||||
text.len()
|
||||
}
|
||||
|
||||
/// Where the string or character literal starting at `text[at]` (a `"` or `'`) ends, raw strings
|
||||
/// (`r#"…"#`) included; `None` for a lifetime's `'`.
|
||||
fn quoted_end(text: &str, at: usize) -> Option<usize> {
|
||||
@@ -1339,13 +1364,16 @@ fn package_status_message_templates_parse() {
|
||||
}
|
||||
|
||||
/// The scan reads the message of an error made with one of the engine's [`ERROR_HELPERS`] or with
|
||||
/// an error enum's variant: a literal or a `format!` string, on the call's line or wrapped over
|
||||
/// several lines, in the same template form as the catalogs' rows. It does not read a call whose
|
||||
/// name ends in a helper's name (`is_bad(`), a method call (`.err(`), `std::io::Error::new` or one
|
||||
/// of [`NAME_VARIANTS`]; without the helpers, it reads only the two variants in this source.
|
||||
/// an error enum's variant, `EngineError::Disabled`'s reason included: a literal or a `format!`
|
||||
/// string, on the call's line or wrapped over several lines, in the same template form as the
|
||||
/// catalogs' rows. It does not read a call whose name ends in a helper's name (`is_bad(`), a method
|
||||
/// call (`.err(`), `std::io::Error::new`, one of [`NAME_VARIANTS`], `EngineError::Disabled`'s
|
||||
/// command id or a call in a comment. A `//` or `/*` inside a string or a raw string, or after a
|
||||
/// character literal such as `'"'`, does not start a comment. Without the helpers, the scan reads
|
||||
/// only the four variants in this source.
|
||||
#[test]
|
||||
fn message_scan_reads_error_helpers_and_variants() {
|
||||
let code = r#"
|
||||
let code = r##"
|
||||
fn width(p: &Value, cmd: &str, key: &str) -> Result<f64> {
|
||||
let w = p.get(key).and_then(Value::as_f64).ok_or_else(|| bad(cmd, format!("`{key}` must be a number")))?;
|
||||
if w < 0.0 {
|
||||
@@ -1367,9 +1395,16 @@ fn width(p: &Value, cmd: &str, key: &str) -> Result<f64> {
|
||||
let _ = std::io::Error::new(kind, "not an error variant");
|
||||
let _ = PsError::Ps("not an error message", op);
|
||||
let _ = EngineError::UnknownCommand("not.an.error.message".into());
|
||||
let _ = EngineError::Disabled("not.a.reason".into(), "there is nothing to clear".into());
|
||||
let _ = EngineError::Disabled(C.into(), format!("the {kind} panel is locked"));
|
||||
// return Err(bad(cmd, "not an error: a commented-out call"));
|
||||
/* let _ = err("not an error: a call in a block comment");
|
||||
/* nested */ let _ = err("not an error: after a nested block comment"); */
|
||||
let _ = ('"', '\'', b'"', bad(cmd, "the width // the height"));
|
||||
let _ = (r#"a "/* raw string"#, |s: &'a str| -> Result<&'a str> { Err(bad(cmd, "the height is too large")) });
|
||||
Err(error("Select only dimensions"))
|
||||
}
|
||||
"#;
|
||||
"##;
|
||||
let engine: Vec<_> = ERROR_HELPERS.iter().filter(|(c, _, _)| *c == "engine").map(|&(_, call, before)| (call, before)).collect();
|
||||
let found = source_messages(code, &engine);
|
||||
for want in [
|
||||
@@ -1380,13 +1415,19 @@ fn width(p: &Value, cmd: &str, key: &str) -> Result<f64> {
|
||||
"can't open `{name}`: damaged",
|
||||
"the {what} is nested too deep",
|
||||
"the manifest has no name",
|
||||
"there is nothing to clear",
|
||||
"the {kind} panel is locked",
|
||||
"the width // the height",
|
||||
"the height is too large",
|
||||
"Select only dimensions",
|
||||
] {
|
||||
assert!(found.iter().any(|m| m == want), "{want} not in {found:#?}");
|
||||
}
|
||||
assert!(!found.iter().any(|m| m.contains("not an error") || m.contains("not.an.error")), "{found:#?}");
|
||||
let named = |m: &String| m.contains("not an error") || m.contains("not.an.error") || m.contains("not.a.reason");
|
||||
assert!(!found.iter().any(named), "{found:#?}");
|
||||
let elsewhere = source_messages(code, &[]);
|
||||
assert!(elsewhere.len() == 2 && elsewhere.iter().all(|m| m.contains("nested") || m.contains("manifest")), "{elsewhere:#?}");
|
||||
let in_variants = ["nested", "manifest", "nothing to clear", "panel is locked"];
|
||||
assert!(elsewhere.len() == 4 && elsewhere.iter().all(|m| in_variants.iter().any(|w| m.contains(w))), "{elsewhere:#?}");
|
||||
// A message read this way matches its row: `{key}` keeps its name.
|
||||
let c = Catalog::parse("@msg\t`{key}` must be a number\t`{key}` ok\n");
|
||||
assert_eq!(c.message("`width` must be a number").map(|m| m.1), Some(vec![("key", "width")]));
|
||||
@@ -1531,6 +1572,10 @@ const ERROR_HELPERS: &[(&str, &str, usize)] =
|
||||
/// (`EngineError::UnknownCommand`) and a PostScript error name (`PsError::Ps`).
|
||||
const NAME_VARIANTS: &[&str] = &["UnknownCommand", "Ps"];
|
||||
|
||||
/// Error variants whose message follows other arguments, with the number of arguments before it:
|
||||
/// `EngineError::Disabled(command id, reason)`.
|
||||
const LATER_MESSAGE_VARIANTS: &[(&str, usize)] = &[("Disabled", 1)];
|
||||
|
||||
/// Messages that are signals, not text for people.
|
||||
const NOT_MESSAGES: &[&str] = &["quit", "cancelled"];
|
||||
|
||||
@@ -1561,18 +1606,21 @@ pub fn message_literals() -> std::collections::BTreeSet<String> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The message literals in `code`, as catalog templates (see [`message_literals`]): the literal
|
||||
/// after each of [`MESSAGE_MARKERS`], the first argument of an error enum's variant
|
||||
/// (`PsError::Limit(…)`, `Error::Manifest(…)`; not [`NAME_VARIANTS`]),
|
||||
/// 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
|
||||
/// [`LATER_MESSAGE_VARIANTS`] gives; not [`NAME_VARIANTS`]),
|
||||
/// and the message argument of each call in `helpers` (a call and the number of arguments before
|
||||
/// its message), a literal or a `format!` string.
|
||||
fn source_messages(code: &str, helpers: &[(&str, usize)]) -> 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 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);
|
||||
if named { args.strip_prefix('(') } else { None }
|
||||
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_helpers = helpers.iter().flat_map(|&(call, before)| {
|
||||
code.match_indices(call)
|
||||
|
||||
+3
-2
@@ -280,8 +280,9 @@ other `sv-*` locale resolve to it), Georgian (`ka`, complete; `ka-GE` and `ka_GE
|
||||
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 and Turkish
|
||||
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(…)`,
|
||||
`#[error(…)]` and `status(…)` messages, messages given to an error enum's variant (`PsError::Limit(…)`), to the
|
||||
engine's `bad(cmd, …)`, `err(…)` or `error(…)`, and to the PDF writer's `bad(…)` or `failed(…)`
|
||||
`#[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
|
||||
(`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.
|
||||
|
||||
Reference in New Issue
Block a user