From b93230070b94d10fe74d5323740c6e65217f8ecc Mon Sep 17 00:00:00 2001 From: gordonlu Date: Mon, 8 Jun 2026 17:35:32 +0800 Subject: [PATCH 1/3] feat(i18n): localize ToolFamily labels (10 MessageIds) - localization.rs: Add 10 ToolFamily* MessageId variants + ALL_MESSAGE_IDS + all 7 locales - tool_card.rs: tool_activity_label_for_name() accepts locale, uses tr() for labels - footer_ui.rs, ui.rs: thread locale to tool_activity_label_for_name() callers - Tests: 2 negative i18n tests + updated existing tests for new signatures --- .claude/settings.json | 27 +++++ crates/tui/src/localization.rs | 91 ++++++++++++++++ crates/tui/src/tui/footer_ui.rs | 12 ++- crates/tui/src/tui/ui.rs | 14 ++- crates/tui/src/tui/widgets/tool_card.rs | 131 ++++++++++++++++++++++-- 5 files changed, 261 insertions(+), 14 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..46789ddd --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,27 @@ +{ + "permissions": { + "allow": [ + "Bash(cargo build *)", + "Bash(cargo check *)", + "Bash(cargo test *)", + "Bash(cargo fmt *)", + "Bash(cargo clippy *)", + "Bash(cargo run *)", + "Bash(ls *)", + "Bash(mkdir *)", + "Bash(cp *)", + "Bash(mv *)", + "Bash(rm *)", + "Bash(source *)", + "Bash(which *)", + "Bash(rustc *)", + "Bash(cargo doc *)", + "Bash(cargo update *)", + "Bash(cargo clean *)", + "Bash(git checkout *)", + "Bash(git fetch *)", + "Bash(git pull *)", + "Bash(git stash *)" + ] + } +} diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index d5f61c0d..69702b7b 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -602,6 +602,17 @@ pub enum MessageId { CtxInspChangesByTurn, CtxInspStablePrefixOnly, CtxInspCacheTip, + // Tool family labels (card headers, sidebar, footer). + ToolFamilyRead, + ToolFamilyPatch, + ToolFamilyRun, + ToolFamilyFind, + ToolFamilyDelegate, + ToolFamilyFanout, + ToolFamilyRlm, + ToolFamilyVerify, + ToolFamilyThink, + ToolFamilyGeneric, } #[allow(dead_code)] @@ -974,6 +985,16 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::CtxInspChangesByTurn, MessageId::CtxInspStablePrefixOnly, MessageId::CtxInspCacheTip, + MessageId::ToolFamilyRead, + MessageId::ToolFamilyPatch, + MessageId::ToolFamilyRun, + MessageId::ToolFamilyFind, + MessageId::ToolFamilyDelegate, + MessageId::ToolFamilyFanout, + MessageId::ToolFamilyRlm, + MessageId::ToolFamilyVerify, + MessageId::ToolFamilyThink, + MessageId::ToolFamilyGeneric, ]; pub fn tr(locale: Locale, id: MessageId) -> &'static str { @@ -1672,6 +1693,16 @@ fn english(id: MessageId) -> &'static str { "Tip: Stable prefix blocks are DeepSeek V4 prefix-cache eligible. \ Volatile working-set changes break the cache only for the tail." } + MessageId::ToolFamilyRead => "read", + MessageId::ToolFamilyPatch => "patch", + MessageId::ToolFamilyRun => "run", + MessageId::ToolFamilyFind => "find", + MessageId::ToolFamilyDelegate => "delegate", + MessageId::ToolFamilyFanout => "fanout", + MessageId::ToolFamilyRlm => "rlm", + MessageId::ToolFamilyVerify => "verify", + MessageId::ToolFamilyThink => "think", + MessageId::ToolFamilyGeneric => "tool", } } @@ -2238,6 +2269,16 @@ fn vietnamese(id: MessageId) -> Option<&'static str> { MessageId::CtxInspCacheTip => { "Gợi ý: Các khối ổn định đủ điều kiện cho bộ nhớ đệm tiền tố DeepSeek V4. Thay đổi vùng làm việc chỉ phá vỡ bộ nhớ đệm ở phần cuối." } + MessageId::ToolFamilyRead => "đọc", + MessageId::ToolFamilyPatch => "vá", + MessageId::ToolFamilyRun => "chạy", + MessageId::ToolFamilyFind => "tìm", + MessageId::ToolFamilyDelegate => "ủy quyền", + MessageId::ToolFamilyFanout => "fanout", + MessageId::ToolFamilyRlm => "rlm", + MessageId::ToolFamilyVerify => "xác minh", + MessageId::ToolFamilyThink => "suy nghĩ", + MessageId::ToolFamilyGeneric => "công cụ", }) } @@ -2358,6 +2399,16 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> { MessageId::StatusPickerActionNone => "無 ", MessageId::StatusPickerActionSave => "儲存 ", MessageId::StatusPickerActionCancel => "取消 ", + MessageId::ToolFamilyRead => "讀取", + MessageId::ToolFamilyPatch => "修補", + MessageId::ToolFamilyRun => "執行", + MessageId::ToolFamilyFind => "搜尋", + MessageId::ToolFamilyDelegate => "委派", + MessageId::ToolFamilyFanout => "扇出", + MessageId::ToolFamilyRlm => "rlm", + MessageId::ToolFamilyVerify => "驗證", + MessageId::ToolFamilyThink => "思考", + MessageId::ToolFamilyGeneric => "工具", other => chinese_simplified(other)?, }) } @@ -2883,6 +2934,16 @@ fn japanese(id: MessageId) -> Option<&'static str> { MessageId::CtxInspCacheTip => { "ヒント:安定プレフィックスブロックはDeepSeek V4プレフィックスキャッシュの対象です。揮発性ワーキングセットの変更は末尾のキャッシュのみを破壊します。" } + MessageId::ToolFamilyRead => "読込", + MessageId::ToolFamilyPatch => "パッチ", + MessageId::ToolFamilyRun => "実行", + MessageId::ToolFamilyFind => "検索", + MessageId::ToolFamilyDelegate => "委任", + MessageId::ToolFamilyFanout => "ファンアウト", + MessageId::ToolFamilyRlm => "rlm", + MessageId::ToolFamilyVerify => "検証", + MessageId::ToolFamilyThink => "思考", + MessageId::ToolFamilyGeneric => "ツール", }) } @@ -3343,6 +3404,16 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> { MessageId::CtxInspCacheTip => { "提示:稳定前缀区块符合 DeepSeek V4 前缀缓存条件。易变工作集的更改仅会破坏缓存尾部。" } + MessageId::ToolFamilyRead => "读取", + MessageId::ToolFamilyPatch => "修补", + MessageId::ToolFamilyRun => "运行", + MessageId::ToolFamilyFind => "搜索", + MessageId::ToolFamilyDelegate => "委派", + MessageId::ToolFamilyFanout => "扇出", + MessageId::ToolFamilyRlm => "rlm", + MessageId::ToolFamilyVerify => "验证", + MessageId::ToolFamilyThink => "思考", + MessageId::ToolFamilyGeneric => "工具", }) } @@ -3893,6 +3964,16 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> { MessageId::CtxInspCacheTip => { "Dica: Blocos de prefixo estável são elegíveis para cache de prefixo DeepSeek V4. Alterações no conjunto de trabalho volátil quebram o cache apenas no final." } + MessageId::ToolFamilyRead => "ler", + MessageId::ToolFamilyPatch => "corrigir", + MessageId::ToolFamilyRun => "executar", + MessageId::ToolFamilyFind => "buscar", + MessageId::ToolFamilyDelegate => "delegar", + MessageId::ToolFamilyFanout => "fanout", + MessageId::ToolFamilyRlm => "rlm", + MessageId::ToolFamilyVerify => "verificar", + MessageId::ToolFamilyThink => "pensar", + MessageId::ToolFamilyGeneric => "ferramenta", }) } @@ -4453,6 +4534,16 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> { MessageId::CtxInspCacheTip => { "Consejo: Los bloques de prefijo estable son elegibles para caché de prefijo DeepSeek V4. Los cambios en el conjunto de trabajo volátil solo rompen la caché al final." } + MessageId::ToolFamilyRead => "leer", + MessageId::ToolFamilyPatch => "parchear", + MessageId::ToolFamilyRun => "ejecutar", + MessageId::ToolFamilyFind => "buscar", + MessageId::ToolFamilyDelegate => "delegar", + MessageId::ToolFamilyFanout => "fanout", + MessageId::ToolFamilyRlm => "rlm", + MessageId::ToolFamilyVerify => "verificar", + MessageId::ToolFamilyThink => "pensar", + MessageId::ToolFamilyGeneric => "herramienta", }) } diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index 02dc8ce5..463dec13 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -4,7 +4,7 @@ use std::time::Instant; use unicode_width::UnicodeWidthStr; use crate::core::coherence::CoherenceState; -use crate::localization::MessageId; +use crate::localization::{Locale, MessageId}; use crate::palette; use crate::tools::subagent::SubAgentStatus; use crate::tui::app::App; @@ -314,7 +314,7 @@ pub(crate) fn active_tool_status_label(app: &App) -> Option { let mut snapshot = ActiveToolStatusSnapshot::default(); for cell in active.entries() { - collect_active_tool_status(cell, &mut snapshot); + collect_active_tool_status(cell, &mut snapshot, app.ui_locale); } if snapshot.total() == 0 { return None; @@ -345,7 +345,11 @@ pub(crate) fn active_tool_status_label(app: &App) -> Option { Some(parts.join(" \u{00B7} ")) } -fn collect_active_tool_status(cell: &HistoryCell, snapshot: &mut ActiveToolStatusSnapshot) { +fn collect_active_tool_status( + cell: &HistoryCell, + snapshot: &mut ActiveToolStatusSnapshot, + locale: Locale, +) { let HistoryCell::Tool(tool) = cell else { return; }; @@ -401,7 +405,7 @@ fn collect_active_tool_status(cell: &HistoryCell, snapshot: &mut ActiveToolStatu return; } snapshot.record( - tool_activity_label_for_name(&generic.name), + tool_activity_label_for_name(&generic.name, locale), generic.status, None, ); diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 09c13443..69b0f70a 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -9366,7 +9366,10 @@ fn activity_cell_label(app: &App, cell_index: usize, cell: &HistoryCell) -> Stri HistoryCell::Error { .. } => "error".to_string(), HistoryCell::SubAgent(_) => "sub-agent".to_string(), HistoryCell::Tool(ToolCell::Generic(generic)) => { - crate::tui::widgets::tool_card::tool_activity_label_for_name(&generic.name) + crate::tui::widgets::tool_card::tool_activity_label_for_name( + &generic.name, + app.ui_locale, + ) } HistoryCell::Tool(_) => { detail_target_label(app, cell_index).unwrap_or_else(|| "tool activity".to_string()) @@ -9801,9 +9804,12 @@ pub(crate) fn detail_target_label(app: &App, cell_index: usize) -> Option Some(format!("search {}", search.query)), - HistoryCell::Tool(ToolCell::Generic(generic)) => { - Some(crate::tui::widgets::tool_card::tool_activity_label_for_name(&generic.name)) - } + HistoryCell::Tool(ToolCell::Generic(generic)) => Some( + crate::tui::widgets::tool_card::tool_activity_label_for_name( + &generic.name, + app.ui_locale, + ), + ), HistoryCell::SubAgent(_) => Some("sub-agent".to_string()), _ => None, } diff --git a/crates/tui/src/tui/widgets/tool_card.rs b/crates/tui/src/tui/widgets/tool_card.rs index d525551b..8aeb055e 100644 --- a/crates/tui/src/tui/widgets/tool_card.rs +++ b/crates/tui/src/tui/widgets/tool_card.rs @@ -23,6 +23,8 @@ //! module is the vocabulary, not the layout engine. Keeping it small means //! a future visual refresh only has to touch the constants here. +use crate::localization::Locale; + /// Tool family — the verb the agent is performing. Used to pick a glyph /// and label for the card header. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -97,6 +99,7 @@ pub fn tool_family_for_name(name: &str) -> ToolFamily { /// User-facing label for an arbitrary tool name. Known tools collapse to the /// semantic verb; unknown tools keep their exact name for debugging. +#[allow(dead_code)] #[must_use] pub fn tool_display_label_for_name(name: &str) -> String { let family = tool_family_for_name(name); @@ -110,12 +113,45 @@ pub fn tool_display_label_for_name(name: &str) -> String { /// Compact activity/status label for arbitrary tool names. Known built-ins use /// the semantic verb; unknown tools keep the `tool NAME` form. #[must_use] -pub fn tool_activity_label_for_name(name: &str) -> String { +pub fn tool_activity_label_for_name(name: &str, locale: Locale) -> String { let family = tool_family_for_name(name); if matches!(family, ToolFamily::Generic) { - format!("tool {name}") + format!( + "{} {name}", + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyGeneric) + ) } else { - tool_display_label_for_name(name) + let label = match family { + ToolFamily::Read => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyRead) + } + ToolFamily::Patch => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyPatch) + } + ToolFamily::Run => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyRun) + } + ToolFamily::Find => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyFind) + } + ToolFamily::Delegate => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyDelegate) + } + ToolFamily::Fanout => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyFanout) + } + ToolFamily::Rlm => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyRlm) + } + ToolFamily::Verify => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyVerify) + } + ToolFamily::Think => { + crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyThink) + } + ToolFamily::Generic => unreachable!(), + }; + label.to_string() } } @@ -237,6 +273,7 @@ mod tests { tool_display_label_for_name, tool_family_for_name, tool_family_for_title, tool_header_summary_for_name, }; + use crate::localization::{Locale, MessageId, tr}; #[test] fn legacy_titles_route_to_expected_families() { @@ -275,10 +312,16 @@ mod tests { "future_private_tool" ); - assert_eq!(tool_activity_label_for_name("exec_shell"), "run"); - assert_eq!(tool_activity_label_for_name("run_verifiers"), "verify"); assert_eq!( - tool_activity_label_for_name("future_private_tool"), + tool_activity_label_for_name("exec_shell", Locale::En), + "run" + ); + assert_eq!( + tool_activity_label_for_name("run_verifiers", Locale::En), + "verify" + ); + assert_eq!( + tool_activity_label_for_name("future_private_tool", Locale::En), "tool future_private_tool" ); } @@ -344,4 +387,80 @@ mod tests { assert_eq!(rail_glyph(CardRail::Bottom), "\u{2570}"); assert!(rail_glyph(CardRail::Single).is_empty()); } + + #[test] + fn tool_family_labels_localized_no_english_leak() { + let checks: &[(MessageId, &str, &str)] = &[ + (MessageId::ToolFamilyRead, "read", "đọc,读,読,读取,ler,leer"), + ( + MessageId::ToolFamilyPatch, + "patch", + "vá,補,パ,修补,corrigir,parchear", + ), + ( + MessageId::ToolFamilyRun, + "run", + "chạy,執,実,运行,executar,ejecutar", + ), + ( + MessageId::ToolFamilyFind, + "find", + "tìm,搜,検,搜索,buscar,buscar", + ), + ( + MessageId::ToolFamilyVerify, + "verify", + "xác minh,驗,検,验,verificar,verificar", + ), + ]; + for locale in [ + Locale::Ja, + Locale::ZhHans, + Locale::ZhHant, + Locale::PtBr, + Locale::Es419, + Locale::Vi, + ] { + for (id, eng, _) in checks { + let msg = tr(locale, *id); + assert!( + !msg.eq_ignore_ascii_case(eng), + "{} leaked exact English '{}' for '{:?}': {msg}", + locale.tag(), + eng, + id + ); + } + } + } + + #[test] + fn tool_family_activity_label_localized_no_english_leak() { + let known = [ + "exec_shell", + "read_file", + "apply_patch", + "grep_files", + "run_verifiers", + ]; + let english_labels = ["run", "read", "patch", "find", "verify"]; + for locale in [ + Locale::Ja, + Locale::ZhHans, + Locale::ZhHant, + Locale::PtBr, + Locale::Es419, + Locale::Vi, + ] { + for (tool, eng) in known.iter().zip(english_labels.iter()) { + let label = tool_activity_label_for_name(tool, locale); + assert!( + !label.eq_ignore_ascii_case(eng), + "{} leaked English '{}' for tool '{tool}': {label}", + locale.tag(), + eng, + ); + } + } + } } From 64ca327e4aad050a2272a559214f3e7514d54e11 Mon Sep 17 00:00:00 2001 From: gordonlu Date: Mon, 8 Jun 2026 17:35:40 +0800 Subject: [PATCH 2/3] chore: add .claude/ to gitignore --- .claude/settings.json | 27 --------------------------- .gitignore | 1 + 2 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 46789ddd..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(cargo build *)", - "Bash(cargo check *)", - "Bash(cargo test *)", - "Bash(cargo fmt *)", - "Bash(cargo clippy *)", - "Bash(cargo run *)", - "Bash(ls *)", - "Bash(mkdir *)", - "Bash(cp *)", - "Bash(mv *)", - "Bash(rm *)", - "Bash(source *)", - "Bash(which *)", - "Bash(rustc *)", - "Bash(cargo doc *)", - "Bash(cargo update *)", - "Bash(cargo clean *)", - "Bash(git checkout *)", - "Bash(git fetch *)", - "Bash(git pull *)", - "Bash(git stash *)" - ] - } -} diff --git a/.gitignore b/.gitignore index ab1cf906..4e037e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -120,6 +120,7 @@ docs/*_PLAN.md .envrc .direnv scripts/run_deep_swe.py +.claude/ # Benchmark artifacts and caches re-included by !scripts/** results/ From 1e8f6e3b905bec7481d97183223852235e86b4b6 Mon Sep 17 00:00:00 2001 From: gordonlu Date: Mon, 8 Jun 2026 17:45:32 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fixup:=20make=20tool=5Fdisplay=5Flabel=5Ffo?= =?UTF-8?q?r=5Fname=20private=20+=20deduplicate=20family=E2=86=92MessageId?= =?UTF-8?q?=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/tui/src/tui/widgets/tool_card.rs | 57 +++++++++---------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/crates/tui/src/tui/widgets/tool_card.rs b/crates/tui/src/tui/widgets/tool_card.rs index 8aeb055e..5e4c2e6a 100644 --- a/crates/tui/src/tui/widgets/tool_card.rs +++ b/crates/tui/src/tui/widgets/tool_card.rs @@ -99,9 +99,9 @@ pub fn tool_family_for_name(name: &str) -> ToolFamily { /// User-facing label for an arbitrary tool name. Known tools collapse to the /// semantic verb; unknown tools keep their exact name for debugging. -#[allow(dead_code)] +#[cfg(test)] #[must_use] -pub fn tool_display_label_for_name(name: &str) -> String { +fn tool_display_label_for_name(name: &str) -> String { let family = tool_family_for_name(name); if matches!(family, ToolFamily::Generic) { name.to_string() @@ -110,48 +110,31 @@ pub fn tool_display_label_for_name(name: &str) -> String { } } +fn family_message_id(family: ToolFamily) -> crate::localization::MessageId { + match family { + ToolFamily::Read => crate::localization::MessageId::ToolFamilyRead, + ToolFamily::Patch => crate::localization::MessageId::ToolFamilyPatch, + ToolFamily::Run => crate::localization::MessageId::ToolFamilyRun, + ToolFamily::Find => crate::localization::MessageId::ToolFamilyFind, + ToolFamily::Delegate => crate::localization::MessageId::ToolFamilyDelegate, + ToolFamily::Fanout => crate::localization::MessageId::ToolFamilyFanout, + ToolFamily::Rlm => crate::localization::MessageId::ToolFamilyRlm, + ToolFamily::Verify => crate::localization::MessageId::ToolFamilyVerify, + ToolFamily::Think => crate::localization::MessageId::ToolFamilyThink, + ToolFamily::Generic => crate::localization::MessageId::ToolFamilyGeneric, + } +} + /// Compact activity/status label for arbitrary tool names. Known built-ins use /// the semantic verb; unknown tools keep the `tool NAME` form. #[must_use] pub fn tool_activity_label_for_name(name: &str, locale: Locale) -> String { let family = tool_family_for_name(name); + let mid = family_message_id(family); if matches!(family, ToolFamily::Generic) { - format!( - "{} {name}", - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyGeneric) - ) + format!("{} {name}", crate::localization::tr(locale, mid)) } else { - let label = match family { - ToolFamily::Read => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyRead) - } - ToolFamily::Patch => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyPatch) - } - ToolFamily::Run => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyRun) - } - ToolFamily::Find => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyFind) - } - ToolFamily::Delegate => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyDelegate) - } - ToolFamily::Fanout => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyFanout) - } - ToolFamily::Rlm => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyRlm) - } - ToolFamily::Verify => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyVerify) - } - ToolFamily::Think => { - crate::localization::tr(locale, crate::localization::MessageId::ToolFamilyThink) - } - ToolFamily::Generic => unreachable!(), - }; - label.to_string() + crate::localization::tr(locale, mid).to_string() } }