From 74a4a9120430fe41b68ca990e7e5af9967b34ea7 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Fri, 12 Jun 2026 02:12:58 -0700 Subject: [PATCH] feat(i18n): localize config editor labels Harvests PR #2919 by @gordonlu, preserving Codex-aware reasoning effort display while localizing the config editor chrome and default placeholders. Co-authored-by: gordonlu <3125629+gordonlu@users.noreply.github.com> --- CHANGELOG.md | 3 + crates/tui/CHANGELOG.md | 3 + crates/tui/src/localization.rs | 113 ++++++++++++++++++++++++++++++ crates/tui/src/tui/views/mod.rs | 117 +++++++++++++++++++++++--------- 4 files changed, 205 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4dd54f..7adf38e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Localized config section labels (#2918).** The interactive config view now localizes section and session/saved scope labels while preserving English search terms. Thanks @gordonlu for the PR. +- **Localized config editor labels (#2919).** The config editor modal now + localizes edit labels, default/unavailable placeholders, and effective + currency hints. Thanks @gordonlu for the PR. ### Fixed diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 97c51be2..5ca93b72 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -39,6 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Localized config section labels (#2918).** The interactive config view now localizes section and session/saved scope labels while preserving English search terms. Thanks @gordonlu for the PR. +- **Localized config editor labels (#2919).** The config editor modal now + localizes edit labels, default/unavailable placeholders, and effective + currency hints. Thanks @gordonlu for the PR. ### Fixed diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index fac6f838..98516f52 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -257,6 +257,17 @@ pub enum MessageId { ConfigSectionMcp, ConfigScopeSession, ConfigScopeSaved, + ConfigEditCancelled, + ConfigEditTitlePrefix, + ConfigEditScopeLabel, + ConfigEditCurrentLabel, + ConfigEditHintLabel, + ConfigEditNewLabel, + ConfigEditFooter, + ConfigRowEffective, + ConfigDefaultValue, + ConfigDefaultReasoning, + ConfigUnavailable, HelpTitle, HelpFilterPlaceholder, HelpFilterPrefix, @@ -663,6 +674,17 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::ConfigSectionMcp, MessageId::ConfigScopeSession, MessageId::ConfigScopeSaved, + MessageId::ConfigEditCancelled, + MessageId::ConfigEditTitlePrefix, + MessageId::ConfigEditScopeLabel, + MessageId::ConfigEditCurrentLabel, + MessageId::ConfigEditHintLabel, + MessageId::ConfigEditNewLabel, + MessageId::ConfigEditFooter, + MessageId::ConfigRowEffective, + MessageId::ConfigDefaultValue, + MessageId::ConfigDefaultReasoning, + MessageId::ConfigUnavailable, MessageId::HelpTitle, MessageId::HelpFilterPlaceholder, MessageId::HelpFilterPrefix, @@ -1242,6 +1264,19 @@ fn english(id: MessageId) -> &'static str { MessageId::ConfigSectionMcp => "MCP", MessageId::ConfigScopeSession => "SESSION", MessageId::ConfigScopeSaved => "SAVED", + MessageId::ConfigEditCancelled => "Edit cancelled", + MessageId::ConfigEditTitlePrefix => "Edit ", + MessageId::ConfigEditScopeLabel => "Scope: ", + MessageId::ConfigEditCurrentLabel => "Current: ", + MessageId::ConfigEditHintLabel => "Hint: ", + MessageId::ConfigEditNewLabel => "New: ", + MessageId::ConfigEditFooter => { + " Enter=apply, Esc=cancel, Ctrl+U=clear, Ctrl+A=all, \u{2190}/\u{2192}=move " + } + MessageId::ConfigRowEffective => " (effective {currency})", + MessageId::ConfigDefaultValue => "(default)", + MessageId::ConfigDefaultReasoning => "(config/default)", + MessageId::ConfigUnavailable => "(unavailable)", MessageId::HelpTitle => "Help", MessageId::HelpFilterPlaceholder => "Type to filter", MessageId::HelpFilterPrefix => "Filter: ", @@ -1794,6 +1829,19 @@ fn vietnamese(id: MessageId) -> Option<&'static str> { MessageId::ConfigSectionMcp => "MCP", MessageId::ConfigScopeSession => "PHIÊN", MessageId::ConfigScopeSaved => "ĐÃ LƯU", + MessageId::ConfigEditCancelled => "Đã hủy chỉnh sửa", + MessageId::ConfigEditTitlePrefix => "Sửa ", + MessageId::ConfigEditScopeLabel => "Phạm vi: ", + MessageId::ConfigEditCurrentLabel => "Hiện tại: ", + MessageId::ConfigEditHintLabel => "Gợi ý: ", + MessageId::ConfigEditNewLabel => "Mới: ", + MessageId::ConfigEditFooter => { + " Enter=áp dụng, Esc=hủy, Ctrl+U=xóa, Ctrl+A=tất cả, \u{2190}/\u{2192}=di chuyển " + } + MessageId::ConfigRowEffective => " (hiệu lực {currency})", + MessageId::ConfigDefaultValue => "(mặc định)", + MessageId::ConfigDefaultReasoning => "(cấu hình/mặc định)", + MessageId::ConfigUnavailable => "(không khả dụng)", MessageId::HelpTitle => "Trợ giúp", MessageId::HelpFilterPlaceholder => "Nhập để lọc", MessageId::HelpFilterPrefix => "Bộ lọc: ", @@ -2447,6 +2495,19 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> { MessageId::ConfigSectionMcp => "MCP", MessageId::ConfigScopeSession => "會話", MessageId::ConfigScopeSaved => "已儲存", + MessageId::ConfigEditCancelled => "編輯已取消", + MessageId::ConfigEditTitlePrefix => "編輯 ", + MessageId::ConfigEditScopeLabel => "範圍: ", + MessageId::ConfigEditCurrentLabel => "目前: ", + MessageId::ConfigEditHintLabel => "提示: ", + MessageId::ConfigEditNewLabel => "新值: ", + MessageId::ConfigEditFooter => { + " Enter=套用, Esc=取消, Ctrl+U=清除, Ctrl+A=全選, \u{2190}/\u{2192}=移動 " + } + MessageId::ConfigRowEffective => " (實際 {currency})", + MessageId::ConfigDefaultValue => "(預設)", + MessageId::ConfigDefaultReasoning => "(設定/預設)", + MessageId::ConfigUnavailable => "(無法使用)", MessageId::StatusPickerTitle => " 狀態列 ", MessageId::StatusPickerInstruction => "選擇要在底部顯示的項目:", MessageId::StatusPickerActionToggle => "切換 ", @@ -2511,6 +2572,19 @@ fn japanese(id: MessageId) -> Option<&'static str> { MessageId::ConfigSectionMcp => "MCP", MessageId::ConfigScopeSession => "セッション", MessageId::ConfigScopeSaved => "保存済み", + MessageId::ConfigEditCancelled => "編集をキャンセルしました", + MessageId::ConfigEditTitlePrefix => "編集 ", + MessageId::ConfigEditScopeLabel => "スコープ: ", + MessageId::ConfigEditCurrentLabel => "現在: ", + MessageId::ConfigEditHintLabel => "ヒント: ", + MessageId::ConfigEditNewLabel => "新規: ", + MessageId::ConfigEditFooter => { + " Enter=適用, Esc=キャンセル, Ctrl+U=クリア, Ctrl+A=全選択, \u{2190}/\u{2192}=移動 " + } + MessageId::ConfigRowEffective => " (実効 {currency})", + MessageId::ConfigDefaultValue => "(デフォルト)", + MessageId::ConfigDefaultReasoning => "(設定/デフォルト)", + MessageId::ConfigUnavailable => "(利用不可)", MessageId::HelpTitle => "ヘルプ", MessageId::HelpFilterPlaceholder => "入力して絞り込み", MessageId::HelpFilterPrefix => "絞り込み: ", @@ -3054,6 +3128,19 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> { MessageId::ConfigSectionMcp => "MCP", MessageId::ConfigScopeSession => "会话", MessageId::ConfigScopeSaved => "已保存", + MessageId::ConfigEditCancelled => "编辑已取消", + MessageId::ConfigEditTitlePrefix => "编辑 ", + MessageId::ConfigEditScopeLabel => "范围: ", + MessageId::ConfigEditCurrentLabel => "当前: ", + MessageId::ConfigEditHintLabel => "提示: ", + MessageId::ConfigEditNewLabel => "新值: ", + MessageId::ConfigEditFooter => { + " Enter=应用, Esc=取消, Ctrl+U=清除, Ctrl+A=全选, \u{2190}/\u{2192}=移动 " + } + MessageId::ConfigRowEffective => " (实际 {currency})", + MessageId::ConfigDefaultValue => "(默认)", + MessageId::ConfigDefaultReasoning => "(配置/默认)", + MessageId::ConfigUnavailable => "(不可用)", MessageId::HelpTitle => "帮助", MessageId::HelpFilterPlaceholder => "输入以筛选", MessageId::HelpFilterPrefix => "筛选: ", @@ -3537,6 +3624,19 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> { MessageId::ConfigSectionMcp => "MCP", MessageId::ConfigScopeSession => "SESSÃO", MessageId::ConfigScopeSaved => "SALVO", + MessageId::ConfigEditCancelled => "Edição cancelada", + MessageId::ConfigEditTitlePrefix => "Editar ", + MessageId::ConfigEditScopeLabel => "Escopo: ", + MessageId::ConfigEditCurrentLabel => "Atual: ", + MessageId::ConfigEditHintLabel => "Dica: ", + MessageId::ConfigEditNewLabel => "Novo: ", + MessageId::ConfigEditFooter => { + " Enter=aplicar, Esc=cancelar, Ctrl+U=limpar, Ctrl+A=tudo, \u{2190}/\u{2192}=mover " + } + MessageId::ConfigRowEffective => " (efetivo {currency})", + MessageId::ConfigDefaultValue => "(padrão)", + MessageId::ConfigDefaultReasoning => "(config/padrão)", + MessageId::ConfigUnavailable => "(indisponível)", MessageId::HelpTitle => "Ajuda", MessageId::HelpFilterPlaceholder => "Digite para filtrar", MessageId::HelpFilterPrefix => "Filtro: ", @@ -4108,6 +4208,19 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> { MessageId::ConfigSectionMcp => "MCP", MessageId::ConfigScopeSession => "SESIÓN", MessageId::ConfigScopeSaved => "GUARDADO", + MessageId::ConfigEditCancelled => "Edición cancelada", + MessageId::ConfigEditTitlePrefix => "Editar ", + MessageId::ConfigEditScopeLabel => "Ámbito: ", + MessageId::ConfigEditCurrentLabel => "Actual: ", + MessageId::ConfigEditHintLabel => "Pista: ", + MessageId::ConfigEditNewLabel => "Nuevo: ", + MessageId::ConfigEditFooter => { + " Enter=aplicar, Esc=cancelar, Ctrl+U=limpiar, Ctrl+A=todo, \u{2190}/\u{2192}=mover " + } + MessageId::ConfigRowEffective => " (efectivo {currency})", + MessageId::ConfigDefaultValue => "(predeterminado)", + MessageId::ConfigDefaultReasoning => "(config/predeterminado)", + MessageId::ConfigUnavailable => "(no disponible)", MessageId::HelpTitle => "Ayuda", MessageId::HelpFilterPlaceholder => "Escribe para filtrar", MessageId::HelpFilterPrefix => "Filtro: ", diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 19df67ee..b59a56b4 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -490,7 +490,7 @@ impl ConfigView { value: settings .default_model .as_deref() - .unwrap_or("(default)") + .unwrap_or(tr(app.ui_locale, MessageId::ConfigDefaultValue)) .to_string(), editable: true, scope: ConfigScope::Saved, @@ -499,7 +499,7 @@ impl ConfigView { section: ConfigSection::Model, key: "reasoning_effort".to_string(), value: settings.reasoning_effort.as_deref().map_or_else( - || "(config/default)".to_string(), + || tr(app.ui_locale, MessageId::ConfigDefaultReasoning).to_string(), |value| { crate::tui::app::ReasoningEffort::from_setting_for_provider( value, @@ -557,10 +557,9 @@ impl ConfigView { ConfigRow { section: ConfigSection::Display, key: "background_color".to_string(), - value: settings - .background_color - .clone() - .unwrap_or_else(|| "(default)".to_string()), + value: settings.background_color.clone().unwrap_or_else(|| { + tr(app.ui_locale, MessageId::ConfigDefaultValue).to_string() + }), editable: true, scope: ConfigScope::Saved, }, @@ -938,7 +937,7 @@ impl ConfigView { match key.code { KeyCode::Esc => { self.editing = None; - self.status = Some("Edit cancelled".to_string()); + self.status = Some(self.tr(MessageId::ConfigEditCancelled).to_string()); ViewAction::None } KeyCode::Enter => { @@ -1056,12 +1055,14 @@ impl ConfigView { }; let key = row.key.clone(); let original_value = row.value.clone(); - let initial_value = if (key == "default_model" && original_value == "(default)") - || (key == "reasoning_effort" && original_value == "(config/default)") - { - String::new() - } else { - original_value.clone() + let initial_value = match config_default_placeholder_message(&key) { + Some(message_id) + if original_value == tr(self.locale, message_id) + || original_value == tr(Locale::En, message_id) => + { + String::new() + } + _ => original_value.clone(), }; let buffer: Vec = initial_value.chars().collect(); @@ -1090,7 +1091,12 @@ impl ConfigView { let effective_cost_currency = crate::pricing::CostCurrency::from_setting(&self.effective_cost_currency); if saved_cost_currency != effective_cost_currency { - return format!("{} (effective {})", row.value, self.effective_cost_currency); + return format!( + "{}{}", + row.value, + self.tr(MessageId::ConfigRowEffective) + .replace("{currency}", &self.effective_cost_currency) + ); } } @@ -1112,7 +1118,7 @@ fn config_base_url_row_value(app: &App) -> String { config.provider = Some(app.api_provider.as_str().to_string()); config.deepseek_base_url() }) - .unwrap_or_else(|_| "(unavailable)".to_string()) + .unwrap_or_else(|_| tr(app.ui_locale, MessageId::ConfigUnavailable).to_string()) } fn cost_currency_config_value(app: &App) -> String { @@ -1160,7 +1166,18 @@ fn config_hint_for_key(key: &str) -> &'static str { } } -fn render_config_editor_value_line(edit: &ConfigEdit) -> ratatui::text::Line<'static> { +fn config_default_placeholder_message(key: &str) -> Option { + match key { + "default_model" | "background_color" => Some(MessageId::ConfigDefaultValue), + "reasoning_effort" => Some(MessageId::ConfigDefaultReasoning), + _ => None, + } +} + +fn render_config_editor_value_line( + edit: &ConfigEdit, + locale: Locale, +) -> ratatui::text::Line<'static> { use ratatui::{ style::Style, text::{Line, Span}, @@ -1168,7 +1185,7 @@ fn render_config_editor_value_line(edit: &ConfigEdit) -> ratatui::text::Line<'st let mut spans = Vec::new(); spans.push(Span::styled( - "New: ", + tr(locale, MessageId::ConfigEditNewLabel), Style::default().fg(palette::TEXT_MUTED), )); @@ -1357,33 +1374,38 @@ impl ModalView for ConfigView { let (lines, footer) = if let Some(edit) = self.editing.as_ref() { let mut lines: Vec = Vec::new(); lines.push(Line::from(vec![Span::styled( - format!("Edit {}", edit.key), + format!("{}{}", self.tr(MessageId::ConfigEditTitlePrefix), edit.key), Style::default().fg(palette::DEEPSEEK_SKY).bold(), )])); lines.push(Line::from("")); lines.push(Line::from(vec![ - Span::styled("Scope: ", Style::default().fg(palette::TEXT_MUTED)), + Span::styled( + self.tr(MessageId::ConfigEditScopeLabel), + Style::default().fg(palette::TEXT_MUTED), + ), Span::raw(edit.scope.label(self.locale)), ])); lines.push(Line::from(vec![ - Span::styled("Current: ", Style::default().fg(palette::TEXT_MUTED)), + Span::styled( + self.tr(MessageId::ConfigEditCurrentLabel), + Style::default().fg(palette::TEXT_MUTED), + ), Span::raw(truncate_view_text(&edit.original_value, 60)), ])); lines.push(Line::from("")); - lines.push(render_config_editor_value_line(edit)); + lines.push(render_config_editor_value_line(edit, self.locale)); lines.push(Line::from("")); let hint = config_hint_for_key(&edit.key); if !hint.is_empty() { lines.push(Line::from(vec![ - Span::styled("Hint: ", Style::default().fg(palette::TEXT_MUTED)), + Span::styled( + self.tr(MessageId::ConfigEditHintLabel), + Style::default().fg(palette::TEXT_MUTED), + ), Span::raw(hint), ])); } - ( - lines, - " Enter=apply, Esc=cancel, Ctrl+U=clear, Ctrl+A=all, \u{2190}/\u{2192}=move " - .to_string(), - ) + (lines, self.tr(MessageId::ConfigEditFooter).to_string()) } else { let content_height = usize::from(inner.height); let header_lines = 5usize; @@ -2061,7 +2083,7 @@ mod tests { ViewStack, subagent_view_agents, truncate_view_text, }; use crate::config::Config; - use crate::localization::Locale; + use crate::localization::{Locale, MessageId, tr}; use crate::settings::Settings; use crate::tools::subagent::{ SubAgentAssignment, SubAgentResult, SubAgentStatus, SubAgentType, @@ -2410,7 +2432,7 @@ base_url = "https://api.xiaomimimo.com/v1" .expect("cost_currency row"); assert_eq!(row.value, "usd"); - assert_eq!(view.row_display_value(row), "usd (effective cny)"); + assert_eq!(view.row_display_value(row), "usd (实际 cny)"); assert_eq!(Settings::load().expect("settings").cost_currency, "usd"); } @@ -2479,6 +2501,35 @@ base_url = "https://api.xiaomimimo.com/v1" assert_eq!(row.value, "xhigh"); } + #[test] + fn config_view_editing_localized_default_placeholders_starts_blank() { + let _guard = ConfigSettingsEnvGuard::new("locale = \"zh-Hans\"\n"); + let app = create_test_app(); + let mut view = ConfigView::new_for_app(&app); + + for (key, message_id) in [ + ("default_model", MessageId::ConfigDefaultValue), + ("reasoning_effort", MessageId::ConfigDefaultReasoning), + ("background_color", MessageId::ConfigDefaultValue), + ] { + view.selected = view + .rows + .iter() + .position(|row| row.key == key) + .unwrap_or_else(|| panic!("{key} row missing")); + view.start_edit(); + + let edit = view.editing.as_ref().expect("editing should start"); + assert_eq!(edit.original_value, tr(Locale::ZhHans, message_id)); + assert!( + edit.buffer.is_empty(), + "localized default placeholder should not become edit text for {key}" + ); + + view.editing = None; + } + } + #[test] fn config_view_filter_matches_group_and_rows() { let mut view = create_config_view(Locale::En); @@ -2717,7 +2768,8 @@ base_url = "https://api.xiaomimimo.com/v1" #[test] fn config_view_escape_cancels_editing() { - let app = create_test_app(); + let mut app = create_test_app(); + app.ui_locale = Locale::En; let mut view = ConfigView::new_for_app(&app); let _ = view.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert!(view.editing.is_some()); @@ -2725,7 +2777,10 @@ base_url = "https://api.xiaomimimo.com/v1" let cancel = view.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert!(matches!(cancel, ViewAction::None)); assert!(view.editing.is_none()); - assert_eq!(view.status.as_deref(), Some("Edit cancelled")); + assert_eq!( + view.status.as_deref(), + Some(tr(Locale::En, MessageId::ConfigEditCancelled)) + ); } /// A modal that doesn't override `handle_paste` must report