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:
@@ -120,6 +120,7 @@ docs/*_PLAN.md
|
||||
.envrc
|
||||
.direnv
|
||||
scripts/run_deep_swe.py
|
||||
.claude/
|
||||
|
||||
# Benchmark artifacts and caches re-included by !scripts/**
|
||||
results/
|
||||
|
||||
@@ -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 => "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ụ",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user