feat(i18n): localize ToolFamily labels (10 MessageIds) (#2901)

* 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

* chore: add .claude/ to gitignore

* fixup: make tool_display_label_for_name private + deduplicate family→MessageId mapping

---------

Co-authored-by: gordonlu <gordonlu@users.noreply.github.com>
This commit is contained in:
Gordon
2026-06-13 01:53:22 +08:00
committed by GitHub
parent f8e6de926c
commit 5be5cd5a79
5 changed files with 219 additions and 15 deletions
+1
View File
@@ -120,6 +120,7 @@ docs/*_PLAN.md
.envrc .envrc
.direnv .direnv
scripts/run_deep_swe.py scripts/run_deep_swe.py
.claude/
# Benchmark artifacts and caches re-included by !scripts/** # Benchmark artifacts and caches re-included by !scripts/**
results/ results/
+91
View File
@@ -610,6 +610,17 @@ pub enum MessageId {
CtxInspChangesByTurn, CtxInspChangesByTurn,
CtxInspStablePrefixOnly, CtxInspStablePrefixOnly,
CtxInspCacheTip, CtxInspCacheTip,
// Tool family labels (card headers, sidebar, footer).
ToolFamilyRead,
ToolFamilyPatch,
ToolFamilyRun,
ToolFamilyFind,
ToolFamilyDelegate,
ToolFamilyFanout,
ToolFamilyRlm,
ToolFamilyVerify,
ToolFamilyThink,
ToolFamilyGeneric,
} }
#[allow(dead_code)] #[allow(dead_code)]
@@ -990,6 +1001,16 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CtxInspChangesByTurn, MessageId::CtxInspChangesByTurn,
MessageId::CtxInspStablePrefixOnly, MessageId::CtxInspStablePrefixOnly,
MessageId::CtxInspCacheTip, 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 { pub fn tr(locale: Locale, id: MessageId) -> &'static str {
@@ -1696,6 +1717,16 @@ fn english(id: MessageId) -> &'static str {
"Tip: Stable prefix blocks are DeepSeek V4 prefix-cache eligible. \ "Tip: Stable prefix blocks are DeepSeek V4 prefix-cache eligible. \
Volatile working-set changes break the cache only for the tail." 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",
} }
} }
@@ -2270,6 +2301,16 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => { 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." "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 => "",
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ụ",
}) })
} }
@@ -2398,6 +2439,16 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> {
MessageId::StatusPickerActionNone => "", MessageId::StatusPickerActionNone => "",
MessageId::StatusPickerActionSave => "儲存 ", MessageId::StatusPickerActionSave => "儲存 ",
MessageId::StatusPickerActionCancel => "取消 ", 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)?, other => chinese_simplified(other)?,
}) })
} }
@@ -2931,6 +2982,16 @@ fn japanese(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => { MessageId::CtxInspCacheTip => {
"ヒント:安定プレフィックスブロックはDeepSeek V4プレフィックスキャッシュの対象です。揮発性ワーキングセットの変更は末尾のキャッシュのみを破壊します。" "ヒント:安定プレフィックスブロックはDeepSeek V4プレフィックスキャッシュの対象です。揮発性ワーキングセットの変更は末尾のキャッシュのみを破壊します。"
} }
MessageId::ToolFamilyRead => "読込",
MessageId::ToolFamilyPatch => "パッチ",
MessageId::ToolFamilyRun => "実行",
MessageId::ToolFamilyFind => "検索",
MessageId::ToolFamilyDelegate => "委任",
MessageId::ToolFamilyFanout => "ファンアウト",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "検証",
MessageId::ToolFamilyThink => "思考",
MessageId::ToolFamilyGeneric => "ツール",
}) })
} }
@@ -3399,6 +3460,16 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => { MessageId::CtxInspCacheTip => {
"提示:稳定前缀区块符合 DeepSeek V4 前缀缓存条件。易变工作集的更改仅会破坏缓存尾部。" "提示:稳定前缀区块符合 DeepSeek V4 前缀缓存条件。易变工作集的更改仅会破坏缓存尾部。"
} }
MessageId::ToolFamilyRead => "读取",
MessageId::ToolFamilyPatch => "修补",
MessageId::ToolFamilyRun => "运行",
MessageId::ToolFamilyFind => "搜索",
MessageId::ToolFamilyDelegate => "委派",
MessageId::ToolFamilyFanout => "扇出",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "验证",
MessageId::ToolFamilyThink => "思考",
MessageId::ToolFamilyGeneric => "工具",
}) })
} }
@@ -3957,6 +4028,16 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => { 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." "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",
}) })
} }
@@ -4525,6 +4606,16 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => { 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." "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",
}) })
} }
+8 -4
View File
@@ -4,7 +4,7 @@ use std::time::Instant;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::core::coherence::CoherenceState; use crate::core::coherence::CoherenceState;
use crate::localization::MessageId; use crate::localization::{Locale, MessageId};
use crate::palette; use crate::palette;
use crate::tools::subagent::SubAgentStatus; use crate::tools::subagent::SubAgentStatus;
use crate::tui::app::App; use crate::tui::app::App;
@@ -314,7 +314,7 @@ pub(crate) fn active_tool_status_label(app: &App) -> Option<String> {
let mut snapshot = ActiveToolStatusSnapshot::default(); let mut snapshot = ActiveToolStatusSnapshot::default();
for cell in active.entries() { 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 { if snapshot.total() == 0 {
return None; return None;
@@ -345,7 +345,11 @@ pub(crate) fn active_tool_status_label(app: &App) -> Option<String> {
Some(parts.join(" \u{00B7} ")) 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 { let HistoryCell::Tool(tool) = cell else {
return; return;
}; };
@@ -401,7 +405,7 @@ fn collect_active_tool_status(cell: &HistoryCell, snapshot: &mut ActiveToolStatu
return; return;
} }
snapshot.record( snapshot.record(
tool_activity_label_for_name(&generic.name), tool_activity_label_for_name(&generic.name, locale),
generic.status, generic.status,
None, None,
); );
+10 -4
View File
@@ -9523,7 +9523,10 @@ fn activity_cell_label(app: &App, cell_index: usize, cell: &HistoryCell) -> Stri
HistoryCell::Error { .. } => "error".to_string(), HistoryCell::Error { .. } => "error".to_string(),
HistoryCell::SubAgent(_) => "sub-agent".to_string(), HistoryCell::SubAgent(_) => "sub-agent".to_string(),
HistoryCell::Tool(ToolCell::Generic(generic)) => { 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(_) => { HistoryCell::Tool(_) => {
detail_target_label(app, cell_index).unwrap_or_else(|| "tool activity".to_string()) detail_target_label(app, cell_index).unwrap_or_else(|| "tool activity".to_string())
@@ -9958,9 +9961,12 @@ pub(crate) fn detail_target_label(app: &App, cell_index: usize) -> Option<String
Some(format!("image {}", image.path.display())) Some(format!("image {}", image.path.display()))
} }
HistoryCell::Tool(ToolCell::WebSearch(search)) => Some(format!("search {}", search.query)), HistoryCell::Tool(ToolCell::WebSearch(search)) => Some(format!("search {}", search.query)),
HistoryCell::Tool(ToolCell::Generic(generic)) => { HistoryCell::Tool(ToolCell::Generic(generic)) => Some(
Some(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::SubAgent(_) => Some("sub-agent".to_string()), HistoryCell::SubAgent(_) => Some("sub-agent".to_string()),
_ => None, _ => None,
} }
+109 -7
View File
@@ -23,6 +23,8 @@
//! module is the vocabulary, not the layout engine. Keeping it small means //! module is the vocabulary, not the layout engine. Keeping it small means
//! a future visual refresh only has to touch the constants here. //! 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 /// Tool family — the verb the agent is performing. Used to pick a glyph
/// and label for the card header. /// and label for the card header.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -97,8 +99,9 @@ pub fn tool_family_for_name(name: &str) -> ToolFamily {
/// User-facing label for an arbitrary tool name. Known tools collapse to the /// User-facing label for an arbitrary tool name. Known tools collapse to the
/// semantic verb; unknown tools keep their exact name for debugging. /// semantic verb; unknown tools keep their exact name for debugging.
#[cfg(test)]
#[must_use] #[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); let family = tool_family_for_name(name);
if matches!(family, ToolFamily::Generic) { if matches!(family, ToolFamily::Generic) {
name.to_string() name.to_string()
@@ -107,15 +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 /// Compact activity/status label for arbitrary tool names. Known built-ins use
/// the semantic verb; unknown tools keep the `tool NAME` form. /// the semantic verb; unknown tools keep the `tool NAME` form.
#[must_use] #[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); let family = tool_family_for_name(name);
let mid = family_message_id(family);
if matches!(family, ToolFamily::Generic) { if matches!(family, ToolFamily::Generic) {
format!("tool {name}") format!("{} {name}", crate::localization::tr(locale, mid))
} else { } else {
tool_display_label_for_name(name) crate::localization::tr(locale, mid).to_string()
} }
} }
@@ -237,6 +256,7 @@ mod tests {
tool_display_label_for_name, tool_family_for_name, tool_family_for_title, tool_display_label_for_name, tool_family_for_name, tool_family_for_title,
tool_header_summary_for_name, tool_header_summary_for_name,
}; };
use crate::localization::{Locale, MessageId, tr};
#[test] #[test]
fn legacy_titles_route_to_expected_families() { fn legacy_titles_route_to_expected_families() {
@@ -275,10 +295,16 @@ mod tests {
"future_private_tool" "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!( 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" "tool future_private_tool"
); );
} }
@@ -344,4 +370,80 @@ mod tests {
assert_eq!(rail_glyph(CardRail::Bottom), "\u{2570}"); assert_eq!(rail_glyph(CardRail::Bottom), "\u{2570}");
assert!(rail_glyph(CardRail::Single).is_empty()); 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,
);
}
}
}
} }