From 6626b40ec60d6592df03837a8a04d0badba6450a Mon Sep 17 00:00:00 2001 From: kirillDevPro <113171057+kirillDevPro@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:59:40 +0200 Subject: [PATCH] feat(strategies): make the human captions a preference, on by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The localized name under every section title and field identifier (#441) is useful to someone reading a strategy in their own language and noise to someone who thinks in Moonbot's identifiers and wants a denser pane. Both are real users of the same window, so the caption becomes a preference in the Strategies settings gear rather than a fixed part of the row. It ships ON: the captions arrived on by construction, so keeping them is what leaves an upgraded pane unchanged. Off, the sections pane keeps one line per section, the parameters header reads `Main` instead of `Main · Основные`, and a field row keeps only the name the core speaks — the help tooltip is unaffected, it never was a translation of the name. Persisted as one optional lenient key in layout.toml next to the other Strategies preferences, resolved through the same PrefRow table, so an absent key keeps the default and a malformed hand edit cannot discard the surrounding layout. --- crates/moon-core/src/config/layout.rs | 7 +++ crates/moon-ui-gpui/src/strategies/params.rs | 13 +++- .../moon-ui-gpui/src/strategies/sections.rs | 62 ++++++++++++------- .../moon-ui-gpui/src/strategies/settings.rs | 24 ++++++- .../src/strategies/settings/tests.rs | 1 + locales/strategies.yml | 4 ++ 6 files changed, 83 insertions(+), 28 deletions(-) diff --git a/crates/moon-core/src/config/layout.rs b/crates/moon-core/src/config/layout.rs index a509d2cc..c15e165f 100644 --- a/crates/moon-core/src/config/layout.rs +++ b/crates/moon-core/src/config/layout.rs @@ -650,6 +650,13 @@ pub struct WindowLayout { /// discard the complete window layout. #[serde(default, deserialize_with = "de_lenient")] pub strategies_params_full: Option, + /// Strategies: whether section and field rows carry the localized human name under + /// Moonbot's own identifier. + /// + /// `None` keeps the Strategies-owned default. Read leniently so a malformed hand edit cannot + /// discard the complete window layout. + #[serde(default, deserialize_with = "de_lenient")] + pub strategies_human_labels: Option, /// Global "Assets" window geometry (singleton), so it reopens in its previous position. #[serde(default)] pub assets_window: Option, diff --git a/crates/moon-ui-gpui/src/strategies/params.rs b/crates/moon-ui-gpui/src/strategies/params.rs index 3ba7e596..c5b393f1 100644 --- a/crates/moon-ui-gpui/src/strategies/params.rs +++ b/crates/moon-ui-gpui/src/strategies/params.rs @@ -1127,7 +1127,7 @@ impl StrategiesView { differ, param_entries::ParamLabels { orphans: &orphans, - section_title: &|raw| section_display_title(raw), + section_title: &|raw| section_display_title(raw, self.prefs.human_labels), }, ); ParamsBody::Full(Rc::new(flat)) @@ -1292,7 +1292,10 @@ impl StrategiesView { // Title and field total come from the body; the multi selection-count branch keeps // priority exactly as before the body could also be a full-mode list. let (title, field_total) = match &body { - ParamsBody::Section(s) => (section_display_title(&s.title), s.fields.len()), + ParamsBody::Section(s) => ( + section_display_title(&s.title, self.prefs.human_labels), + s.fields.len(), + ), ParamsBody::Full(f) => (t!("strat.params_full_title").to_string(), f.field_count), }; let count = if multi { @@ -1694,10 +1697,14 @@ impl StrategiesView { .any(|(core, id)| self.field_edits.contains_key(&(*core, *id, f.name.clone()))); let field_name = f.name.clone(); let row_id = editor_state_id(keys, &field_name); + // The human name is a preference (`StrategiesPrefs::human_labels`); off, the row keeps + // only the name the core speaks, while the help tooltip is unaffected. let (field_tooltip, field_label) = match field_keys(&field_name) { Some((help, label)) => ( help.map(|key| t!(key).to_string()), - label.map(|key| t!(key).to_string()), + label + .filter(|_| self.prefs.human_labels) + .map(|key| t!(key).to_string()), ), None => (None, None), }; diff --git a/crates/moon-ui-gpui/src/strategies/sections.rs b/crates/moon-ui-gpui/src/strategies/sections.rs index 9312a3aa..61e2cd74 100644 --- a/crates/moon-ui-gpui/src/strategies/sections.rs +++ b/crates/moon-ui-gpui/src/strategies/sections.rs @@ -109,11 +109,12 @@ pub(super) fn section_label_key(raw_title: &str) -> Option<&'static str> { /// /// Args: /// raw_title: Section title exactly as the streamed schema produced it. +/// human_labels: The `StrategiesPrefs::human_labels` preference; off, the gloss is dropped. /// /// Returns: -/// `" · "` when a label exists, or `raw_title` unchanged. -pub(super) fn section_display_title(raw_title: &str) -> String { - match section_label_key(raw_title) { +/// `" · "` when a label exists and labels are on, or `raw_title` unchanged. +pub(super) fn section_display_title(raw_title: &str, human_labels: bool) -> String { + match section_label_key(raw_title).filter(|_| human_labels) { Some(key) => format!("{raw_title} · {}", t!(key)), None => raw_title.to_string(), } @@ -121,16 +122,18 @@ pub(super) fn section_display_title(raw_title: &str) -> String { /// Two-line caption for a table-of-contents row: the schema title, then its human name under it. /// -/// A section with no label keeps one line, so an unrecognised section looks exactly as it did. +/// A section with no label keeps one line, so an unrecognised section looks exactly as it did — +/// and so does every section once the human-labels preference is off. /// /// Args: /// raw_title: Section title exactly as the streamed schema produced it. +/// human_labels: The `StrategiesPrefs::human_labels` preference; off, no second line. /// muted: Colour of the localized second line. /// cx: Application context providing active text metrics. /// /// Returns: /// A width-owning column that truncates each line on its own. -fn section_caption(raw_title: &str, muted: Hsla, cx: &App) -> impl IntoElement { +fn section_caption(raw_title: &str, human_labels: bool, muted: Hsla, cx: &App) -> impl IntoElement { v_flex() .flex_1() .min_w_0() @@ -141,18 +144,21 @@ fn section_caption(raw_title: &str, muted: Hsla, cx: &App) -> impl IntoElement { .truncate() .child(raw_title.to_string()), ) - .when_some(section_label_key(raw_title), |col, key| { - col.child( - div() - .w_full() - .min_w_0() - .truncate() - .text_size(design::t_caption(cx)) - .line_height(design::line_px(cx, 12.0)) - .text_color(muted) - .child(t!(key).to_string()), - ) - }) + .when_some( + section_label_key(raw_title).filter(|_| human_labels), + |col, key| { + col.child( + div() + .w_full() + .min_w_0() + .truncate() + .text_size(design::t_caption(cx)) + .line_height(design::line_px(cx, 12.0)) + .text_color(muted) + .child(t!(key).to_string()), + ) + }, + ) } impl StrategiesView { @@ -175,9 +181,11 @@ impl StrategiesView { .iter() .map(|section| { let raw = design::ui_body_text_width(cx, §ion.title, 400.0); - let label = section_label_key(§ion.title).map_or(0.0, |key| { - design::ui_caption_text_width(cx, &t!(key).to_string(), 400.0) - }); + let label = section_label_key(§ion.title) + .filter(|_| self.prefs.human_labels) + .map_or(0.0, |key| { + design::ui_caption_text_width(cx, &t!(key).to_string(), 400.0) + }); raw.max(label) }) .reduce(f32::max) @@ -306,7 +314,12 @@ impl StrategiesView { .text_color(moon(p.text)) // The count badge beside it cannot shrink, so the caption owns the width // and degrades to an ellipsis instead of painting over the badge. - .child(section_caption(&sec.title, moon(p.text_muted), cx)) + .child(section_caption( + &sec.title, + self.prefs.human_labels, + moon(p.text_muted), + cx, + )) .child( h_flex().ml_auto().flex_none().child( MoonBadge::new(n.to_string()) @@ -376,7 +389,12 @@ impl StrategiesView { .text_color(moon(tcol)) // The pane is user-resizable down to a width no section name fits, so each line // of the caption degrades to an ellipsis rather than spilling into the splitter. - .child(section_caption(&sec.title, moon(p.text_muted), cx)) + .child(section_caption( + &sec.title, + self.prefs.human_labels, + moon(p.text_muted), + cx, + )) .on_click(cx.listener(move |this, _, _, cx| { if this.selected_section != i { this.selected_section = i; diff --git a/crates/moon-ui-gpui/src/strategies/settings.rs b/crates/moon-ui-gpui/src/strategies/settings.rs index 61f9c928..c9078b40 100644 --- a/crates/moon-ui-gpui/src/strategies/settings.rs +++ b/crates/moon-ui-gpui/src/strategies/settings.rs @@ -60,6 +60,8 @@ pub(super) struct StrategiesPrefs { pub(super) tree_text_step: f32, /// Whether the parameters pane shows every section at once instead of one. pub(super) params_full: bool, + /// Whether section and field rows carry the localized human name under Moonbot's identifier. + pub(super) human_labels: bool, } impl Default for StrategiesPrefs { @@ -69,13 +71,15 @@ impl Default for StrategiesPrefs { /// does not silently hide strategies that were visible before the preference existed, the tree /// text step ships at zero so the pane renders at exactly the theme base, and full mode stays /// off for the same reason as active-only: an upgrade must not silently change what the pane - /// shows. + /// shows. Human labels ship ON: they arrived on by construction, so keeping them is what + /// leaves the pane unchanged. fn default() -> Self { Self { group_by_venue: true, active_only: false, tree_text_step: STRATEGIES_TREE_TEXT_STEP_DEFAULT, params_full: false, + human_labels: true, } } } @@ -161,12 +165,26 @@ const PARAMS_FULL: PrefRow = PrefRow { store: |layout, value| layout.strategies_params_full = Some(value), }; +/// Show the localized human name under Moonbot's own identifier on section and field rows. +/// +/// Off, a section reads `Main` instead of `Main · Основные` and a field row keeps only the name +/// the core speaks; the help tooltip stays either way. +const HUMAN_LABELS: PrefRow = PrefRow { + id: "human-labels", + group: DISPLAY_GROUP, + label: "strat.settings.human_labels", + read: |prefs| prefs.human_labels, + set: |prefs, value| prefs.human_labels = value, + saved: |layout| layout.strategies_human_labels, + store: |layout, value| layout.strategies_human_labels = Some(value), +}; + /// Every preference, in the order `restore` resolves them. Persistence covers all of them wherever /// their control lives. -const PREF_ROWS: [&PrefRow; 3] = [&GROUP_BY_VENUE, &ACTIVE_ONLY, &PARAMS_FULL]; +const PREF_ROWS: [&PrefRow; 4] = [&GROUP_BY_VENUE, &ACTIVE_ONLY, &PARAMS_FULL, &HUMAN_LABELS]; /// The subset the settings popup renders, in display order. -const POPUP_ROWS: [&PrefRow; 1] = [&GROUP_BY_VENUE]; +const POPUP_ROWS: [&PrefRow; 2] = [&GROUP_BY_VENUE, &HUMAN_LABELS]; impl StrategiesView { /// Apply and persist one Strategies display preference. diff --git a/crates/moon-ui-gpui/src/strategies/settings/tests.rs b/crates/moon-ui-gpui/src/strategies/settings/tests.rs index e73509ae..692c5b36 100644 --- a/crates/moon-ui-gpui/src/strategies/settings/tests.rs +++ b/crates/moon-ui-gpui/src/strategies/settings/tests.rs @@ -106,6 +106,7 @@ fn saved_preferences_restore_independently() { active_only: true, tree_text_step: 0.0, params_full: false, + human_labels: true, } ); } diff --git a/locales/strategies.yml b/locales/strategies.yml index 6558547c..c3eb3107 100644 --- a/locales/strategies.yml +++ b/locales/strategies.yml @@ -49,6 +49,10 @@ strat.settings.params_full: ru: "Показывать все разделы параметров сразу" en: "Show every parameter section at once" es: "Mostrar todas las secciones de parámetros a la vez" +strat.settings.human_labels: + ru: "Подписывать разделы и поля на моём языке" + en: "Caption sections and fields in my language" + es: "Rotular secciones y campos en mi idioma" strat.params_mode_sections: ru: "по разделам" en: "by section"