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
.direnv
scripts/run_deep_swe.py
.claude/
# Benchmark artifacts and caches re-included by !scripts/**
results/
+91
View File
@@ -610,6 +610,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)]
@@ -990,6 +1001,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 {
@@ -1696,6 +1717,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",
}
}
@@ -2270,6 +2301,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 => "",
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::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)?,
})
}
@@ -2931,6 +2982,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 => "ツール",
})
}
@@ -3399,6 +3460,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 => "工具",
})
}
@@ -3957,6 +4028,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",
})
}
@@ -4525,6 +4606,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",
})
}
+8 -4
View File
@@ -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<String> {
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<String> {
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,
);
+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::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())
@@ -9958,9 +9961,12 @@ pub(crate) fn detail_target_label(app: &App, cell_index: usize) -> Option<String
Some(format!("image {}", image.path.display()))
}
HistoryCell::Tool(ToolCell::WebSearch(search)) => 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,
}
+109 -7
View File
@@ -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,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
/// semantic verb; unknown tools keep their exact name for debugging.
#[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()
@@ -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
/// 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);
let mid = family_message_id(family);
if matches!(family, ToolFamily::Generic) {
format!("tool {name}")
format!("{} {name}", crate::localization::tr(locale, mid))
} 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_header_summary_for_name,
};
use crate::localization::{Locale, MessageId, tr};
#[test]
fn legacy_titles_route_to_expected_families() {
@@ -275,10 +295,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 +370,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,
);
}
}
}
}