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"