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>
This commit is contained in:
Hunter B
2026-06-12 02:12:58 -07:00
parent b0b7317688
commit 74a4a91204
4 changed files with 205 additions and 31 deletions
+3
View File
@@ -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
+3
View File
@@ -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
+113
View File
@@ -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: ",
+86 -31
View File
@@ -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<char> = 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<MessageId> {
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<Line> = 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