i18n: the History panel translates the step names that contain a style's name

The History panel and Edit › Undo and Redo did not translate the steps
that apply or redefine a character or paragraph style, or redefine a
graphic style. The engine built their names with format! ("Apply Body",
"Redefine Body", "Redefine Graphic Style “S”"), and the panel looks a
name up as a whole, so no catalog row matched a name with a style's
name in it. A name cannot be matched against template rows instead: a
character style named Brush gives "Apply Brush", which is also the name
of the brush step.

The engine now names these steps with Session::edit_named, and each
such step keeps its template and the style's name
(HistoryEntry::named) through undo and redo. The History panel and
Edit › Undo and Redo show the template's row with the name in it
(panels::history::entry_name). The name stays as the document holds
it, and the Normal styles' names are shown in the interface language.
Every complete catalog has rows for Apply {name} and Redefine {name},
and Redefine Graphic Style “{name}” had one.
every_undo_label_is_translated also requires a row for each template,
and the only step name it allows to be built with format! is the
Liquify tools' "{} Tool".

Docs: docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-18 12:34:39 +07:00
committed by bflatastic
parent 438d27ffbe
commit ef9e554dd2
31 changed files with 290 additions and 23 deletions
+6 -2
View File
@@ -380,7 +380,9 @@ fn undo(s: &mut Session, _: &Value) -> Result<Value> {
let doc = std::mem::replace(&mut st.doc, e.doc);
let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection));
let selection = shared_selection(selection, st.history.redo.last());
st.history.redo.push(crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs));
let mut turned = crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs);
turned.named = e.named;
st.history.redo.push(turned);
st.history.trim();
st.revision += 1;
st.selection.prune(&st.doc);
@@ -403,7 +405,9 @@ fn redo(s: &mut Session, _: &Value) -> Result<Value> {
let doc = std::mem::replace(&mut st.doc, e.doc);
let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection));
let selection = shared_selection(selection, st.history.undo.last());
st.history.undo.push(crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs));
let mut turned = crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs);
turned.named = e.named;
st.history.undo.push(turned);
st.history.trim();
st.revision += 1;
st.selection.prune(&st.doc);
+1 -1
View File
@@ -456,7 +456,7 @@ fn style_redefine(s: &mut Session, p: &Value) -> Result<Value> {
let look = GraphicStyle::of(String::new(), captured(n), n);
let name = d.graphic_styles[i].name.clone();
let chars = s.prefs.override_char_color;
s.edit(&format!("Redefine Graphic Style \u{201c}{name}\u{201d}"), |d, _| {
s.edit_named("Redefine Graphic Style “{name}”", &name, false, |d, _| {
let id = d.graphic_style_id(i);
let old = std::mem::replace(&mut d.graphic_styles[i], GraphicStyle { name: name.clone(), id, ..look });
// Objects still linked follow the new definition; edited ones are unlinked (they keep
+34 -3
View File
@@ -442,7 +442,7 @@ fn new(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
return Err(bad(&c, format!("a style named `{name}` already exists")));
}
let n2 = name.clone();
s.edit(&format!("New {base}"), |d, _| {
s.edit(if kind == Kind::Char { "New Character Style" } else { "New Paragraph Style" }, |d, _| {
kind.defs_mut(d).push(TextStyleDef { name: n2, attrs });
Ok(())
})?;
@@ -478,7 +478,7 @@ fn apply(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
Ok(st)
};
let count = ids.len();
s.edit(&format!("Apply {name}"), |d, _| {
s.edit_named("Apply {name}", &name, name == kind.normal(), |d, _| {
for id in &ids {
let Some(t) = text_mut(d, *id) else { continue };
match kind {
@@ -528,7 +528,7 @@ fn redefine_to(s: &mut Session, kind: Kind, name: &str, new_attrs: Map<String, V
let d = &s.doc()?.doc;
let old = kind.attrs(d, name).ok_or_else(|| bad(&kind.cmd(verb), format!("no style `{name}`")))?;
let name = name.to_string();
s.edit(&format!("Redefine {name}"), |d, _| {
s.edit_named("Redefine {name}", &name, name == kind.normal(), |d, _| {
match kind.defs_mut(d).iter_mut().find(|s| s.name == name) {
Some(def) => def.attrs = new_attrs.clone(),
None => kind.defs_mut(d).push(TextStyleDef { name: name.clone(), attrs: new_attrs.clone() }),
@@ -664,6 +664,37 @@ mod tests {
assert_eq!(list["styles"][1]["uses"], 1);
}
/// Applying and redefining a style record a step named from a template with the style's name
/// in it (`HistoryEntry::named`), and undo and redo keep it. The Normal styles' names are
/// marked as names VectorCraft gives. A new style's step has a name of its own.
#[test]
fn style_steps_are_named_from_a_template() {
let (mut s, id) = session_with_text("Hello");
let last = |s: &Session| {
let e = s.doc().unwrap().history.undo.last().unwrap();
(e.label.clone(), e.named().map(|n| (n.template, n.name.clone(), n.built_in)))
};
let named = |template, name: &str, built_in| Some((template, name.to_string(), built_in));
s.execute("charStyle.new", &json!({"name": "Body"})).unwrap();
assert_eq!(last(&s), ("New Character Style".into(), None));
s.execute("charStyle.apply", &json!({"name": "Body", "id": id})).unwrap();
assert_eq!(last(&s), ("Apply Body".into(), named("Apply {name}", "Body", false)));
s.execute("edit.undo", &json!({})).unwrap();
let redo = s.doc().unwrap().history.redo.last().unwrap().named().cloned();
assert_eq!(redo.map(|n| n.label()).as_deref(), Some("Apply Body"));
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(last(&s), ("Apply Body".into(), named("Apply {name}", "Body", false)));
s.execute("charStyle.apply", &json!({"name": NORMAL_CHAR, "id": id})).unwrap();
assert_eq!(last(&s), (format!("Apply {NORMAL_CHAR}"), named("Apply {name}", NORMAL_CHAR, true)));
s.execute("paraStyle.new", &json!({"name": "Lead"})).unwrap();
assert_eq!(last(&s), ("New Paragraph Style".into(), None));
s.execute("paraStyle.setAttrs", &json!({"name": "Lead", "attrs": {"direction": "rightToLeft"}})).unwrap();
assert_eq!(last(&s), ("Redefine Lead".into(), named("Redefine {name}", "Lead", false)));
// A step renamed since it was recorded is no longer named from the template.
s.doc_mut().unwrap().history.undo.last_mut().unwrap().label = "Typing".into();
assert_eq!(last(&s), ("Typing".into(), None));
}
#[test]
fn new_from_selection_rename_delete_and_undo() {
let (mut s, id) = session_with_text("Title");