Menus: leave installed plug-ins' names out of the strings to translate

menu_strings() lists the strings a complete language catalog has to
cover, and it leaves out names that are user data: fonts, recent files,
saved views and libraries. An installed plug-in's name stayed in.
Plug-ins are installed for the whole process, and a test that installs
one before complete_languages_translate_every_menu_string runs made that
test fail with the plug-in's name as an untranslated menu string.

lists_plugin() tells an Object › Plug-ins filter or an Effect ›
Plug-ins effect by its command and params. Those are part of the menu
item itself and stay right while another test installs or removes
plug-ins. menu_strings() uses it, and so does
menu_catalogs_translate_every_menu_label in place of its own copy of the
rule.

plugins_are_listed_in_the_object_and_effect_menus checks it. Before the
change it failed with "Desaturate… is a menu string to translate".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
This commit is contained in:
Louise Fox
2026-10-18 12:41:25 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent df1bf3fe32
commit dbb6b470b3
3 changed files with 24 additions and 8 deletions
+5 -2
View File
@@ -954,8 +954,11 @@ fn menu_catalogs_translate_every_menu_label() {
let language_name = |l: &str| Lang::all().any(|lang| lang.name() == l);
// Font names and sizes, and installed plug-ins' own names, are not interface text.
let interface = |(label, id, p): &(&str, &str, serde_json::Value)| {
let plugin = *id == "plugin.dialog" || p.get("effect").and_then(serde_json::Value::as_str).is_some_and(|e| e.starts_with("plugin."));
*id != "text.setStyle" && !plugin && shown.contains(*label) && !MENU_KEEP_AS_IS.contains(label) && !language_name(label)
*id != "text.setStyle"
&& !crate::menus::lists_plugin(id, p)
&& shown.contains(*label)
&& !MENU_KEEP_AS_IS.contains(label)
&& !language_name(label)
};
let mut all: Vec<String> = labels.iter().filter(|l| interface(l)).map(|(l, ..)| l.to_string()).collect();
all.extend(toggled_labels());
+13 -6
View File
@@ -1917,6 +1917,13 @@ fn shows_a_name(id: &str, label: &str) -> bool {
|| (matches!(id, "effect.apply" | "effect.dialog") && vectorcraft_effects::plugin_effects().iter().any(|e| e.label == label))
}
/// Does the menu item `id` with `params` list an installed plug-in (an Object › Plug-ins filter or
/// an Effect › Plug-ins effect), by the plug-in's own name? Told by the item itself, so it holds
/// while another thread installs or removes plug-ins.
pub(crate) fn lists_plugin(id: &str, params: &Value) -> bool {
id == "plugin.dialog" || params.get("effect").and_then(Value::as_str).and_then(vectorcraft_plugins::effect::plugin_id).is_some()
}
/// The label of a menu item as drawn: [`dynamic_label`] in the UI language. Labels assembled
/// around a name (Undo *Move*, Reset *Essentials*, Last Effect: *Drop Shadow*) are translated as
/// templates so the name can move; names that are user data (files, views, fonts, custom
@@ -3952,17 +3959,17 @@ pub const DYNAMIC_LABELS: &[&str] = &[
/// Every English string the menus, the command palette, the panel registry, the toolbar and the
/// Preferences dialog can show: what a complete language catalog has to cover. Names that are user
/// data (fonts, recent files, saved views, libraries), the point sizes and the view angles are
/// left out.
/// data (fonts, recent files, saved views, libraries, installed plug-ins), the point sizes and the
/// view angles are left out.
pub fn menu_strings() -> std::collections::BTreeSet<String> {
use std::collections::BTreeSet;
fn user_data(id: &str) -> bool {
hidden_when_disabled(id) || id.starts_with("type.recentFont")
fn user_data(id: &str, params: &Value) -> bool {
hidden_when_disabled(id) || id.starts_with("type.recentFont") || lists_plugin(id, params)
}
fn walk(items: &[Item], under: &str, out: &mut BTreeSet<String>) {
for it in items {
match it {
Item::Cmd(l, id, _) => {
Item::Cmd(l, id, params) => {
let size = l.strip_suffix(" pt").is_some_and(|n| n.chars().all(|c| c.is_ascii_digit()));
// View › Rotate View lists angles: an optional minus sign, digits and a degree sign.
let angle = l
@@ -3971,7 +3978,7 @@ pub fn menu_strings() -> std::collections::BTreeSet<String> {
.is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()));
// VectorCraft › Language lists each language by its own name.
let language_name = *id == "app.language" && crate::i18n::Lang::all().any(|lang| lang.name() == *l);
if !user_data(id) && !size && !angle && !language_name && *l != "—" {
if !user_data(id, params) && !size && !angle && !language_name && *l != "—" {
out.insert(l.to_string());
}
}
+6
View File
@@ -58,6 +58,12 @@ fn plugins_are_listed_in_the_object_and_effect_menus() {
assert_eq!((fx.command.as_deref(), &fx.params), (Some("effect.dialog"), &json!({"effect": "plugin.test.ui.echo"})));
// Menu clicks on items with params don't turn into the generic parameter dialog.
assert_eq!(menus::click_target("Desaturate…", "plugin.dialog", &filter.params).0, "plugin.dialog");
// Their names are the plug-ins' own, not interface text a language catalog has to cover (they
// are installed for the whole test run, while the catalog tests run).
let strings = menus::menu_strings();
for name in ["Desaturate…", "Test test.ui.echo…"] {
assert!(!strings.contains(name), "{name} is a menu string to translate");
}
}
#[test]