feat(ui): localize tool call display via useToolLabel

This commit is contained in:
matevip 2026-04-23 08:09:37 +08:00
parent 4168e34961
commit 95dd500a16
6 changed files with 206 additions and 6 deletions

View File

@ -64,7 +64,7 @@
<el-icon><WarningFilled /></el-icon>
</span>
<span class="approval-bar__label">{{ t('chat.approvalAllow') }}</span>
<span class="approval-bar__tool">{{ pendingApproval.toolName }}</span>
<span class="approval-bar__tool">{{ getToolLabel(pendingApproval.toolName) }}</span>
<span class="approval-bar__label">{{ t('chat.approvalExecute') }}</span>
</div>
<div class="approval-bar__actions">
@ -203,6 +203,7 @@
import { ref, computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { CloseBold, MagicStick, Microphone, Paperclip, Promotion, Select, Timer, WarningFilled } from '@element-plus/icons-vue'
import { useToolLabel } from '@/composables/useToolLabel'
import type { ChatAttachment, PendingApprovalMeta, StreamPhase, QueuedMessage } from '@/types'
interface Props {
@ -272,6 +273,7 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const { getToolLabel } = useToolLabel()
//
const containerRef = ref<HTMLElement | null>(null)

View File

@ -89,7 +89,7 @@
<el-icon v-else-if="tc.success !== false" class="tc-icon--success"><Select /></el-icon>
<el-icon v-else class="tc-icon--error"><CloseBold /></el-icon>
</span>
<span class="tool-call__name">{{ tc.name }}</span>
<span class="tool-call__name">{{ getToolLabel(tc.name) }}</span>
<span class="tool-call__args" v-if="tc.arguments">{{ truncateArgs(tc.arguments) }}</span>
</div>
</div>
@ -108,13 +108,13 @@
<div v-if="pendingApproval" class="approval-inline">
<el-icon class="approval-inline__icon"><WarningFilled /></el-icon>
<span v-if="pendingApproval.status === 'pending_approval'" class="approval-inline__text">
{{ $t('chat.approvalWaiting') }} <code>{{ pendingApproval.toolName }}</code>
{{ $t('chat.approvalWaiting') }} <code>{{ getToolLabel(pendingApproval.toolName) }}</code>
</span>
<span v-else-if="pendingApproval.status === 'approved'" class="approval-inline__text approval-inline--approved">
{{ $t('chat.approved') }}: <code>{{ pendingApproval.toolName }}</code>
{{ $t('chat.approved') }}: <code>{{ getToolLabel(pendingApproval.toolName) }}</code>
</span>
<span v-else class="approval-inline__text approval-inline--denied">
{{ $t('chat.denied') }}: <code>{{ pendingApproval.toolName }}</code>
{{ $t('chat.denied') }}: <code>{{ getToolLabel(pendingApproval.toolName) }}</code>
</span>
</div>
@ -279,6 +279,7 @@ import {
} from '@element-plus/icons-vue'
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
import { useAuthenticatedAttachment } from '@/composables/useAuthenticatedAttachment'
import { useToolLabel } from '@/composables/useToolLabel'
import { http } from '@/api'
import TypingCursor from './TypingCursor.vue'
import BrowserTimeline from './BrowserTimeline.vue'
@ -293,6 +294,7 @@ import type { ChatErrorInfo } from '@/types/chatError'
const { renderMarkdown } = useMarkdownRenderer()
const { t } = useI18n()
const { getToolLabel } = useToolLabel()
const { blobUrls, loadAllImages, loadAllVideos, downloadFile, openImage, getDisplayUrl, revokeAll } = useAuthenticatedAttachment()
interface Props {

View File

@ -1,12 +1,15 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Loading, Select, CloseBold, ArrowDown, Document, Setting } from '@element-plus/icons-vue'
import { useToolLabel } from '@/composables/useToolLabel'
import type { MessageSegment } from '@/types'
const props = defineProps<{
segment: MessageSegment
}>()
const { getToolLabel } = useToolLabel()
const expanded = ref(props.segment.status === 'running')
// running completed
@ -14,7 +17,7 @@ watch(() => props.segment.status, (val) => {
if (val !== 'running') expanded.value = false
})
const displayName = computed(() => (props.segment.toolName || '').replace(/_/g, ' '))
const displayName = computed(() => getToolLabel(props.segment.toolName || ''))
const truncatedArgs = computed(() => {
const args = props.segment.toolArgs || ''

View File

@ -0,0 +1,43 @@
import { useI18n } from 'vue-i18n'
/**
* Title-case a raw camelCase / snake_case / kebab-case tool name.
* Last-resort fallback for unknown tools; the i18n table is authoritative
* for built-ins (see toolLabels in locale files).
*
* wiki_search_pages "Wiki Search Pages"
* delegateToAgent "Delegate To Agent"
* browser-use "Browser Use"
*/
export function humanizeToolName(raw: string): string {
if (!raw) return ''
return raw
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/[-_]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.replace(/\b\w/g, (c) => c.toUpperCase())
}
type TFn = (key: string) => string
type TeFn = (key: string) => boolean
/**
* Pure resolver callable outside Vue setup (pass t/te from a useI18n() instance).
* Resolution: toolLabels.<rawName> in active locale humanizeToolName(rawName).
*/
export function resolveToolLabel(raw: string, t: TFn, te: TeFn): string {
if (!raw) return ''
const key = `toolLabels.${raw}`
return te(key) ? t(key) : humanizeToolName(raw)
}
/**
* Composable must be called inside a Vue setup context.
*/
export function useToolLabel() {
const { t, te } = useI18n()
return {
getToolLabel: (raw: string) => resolveToolLabel(raw, t, te),
}
}

View File

@ -1865,4 +1865,79 @@ export default {
skipped: 'Nothing to consolidate',
},
},
// Tool display labels consumed by useToolLabel() — keys must mirror zh-CN.ts exactly.
// Unknown tools (MCP / custom skills) fall back to humanizeToolName() and are not listed here.
toolLabels: {
// Delegation
delegateToAgent: 'Delegate to Agent',
delegateParallel: 'Parallel Delegation',
listAvailableAgents: 'List Agents',
// Wiki
wiki_search_pages: 'Search Knowledge Base',
wiki_semantic_search: 'Semantic Search KB',
wiki_read_page: 'Read KB Page',
wiki_list_pages: 'List KB Pages',
wiki_related_pages: 'Find Related Pages',
wiki_trace_source: 'Trace Knowledge Source',
wiki_create_page: 'Create KB Page',
wiki_delete_page: 'Delete KB Page',
wiki_enrich_page: 'Enrich KB Page',
wiki_explain_relation: 'Explain Relation',
// Web search
search: 'Web Search',
// Memory
recall_structured: 'Recall Memory',
remember_structured: 'Save Memory',
forget_structured: 'Clear Memory',
fact_probe: 'Probe Facts',
fact_related: 'Query Related Facts',
fact_list_contradictions: 'Check Contradictions',
session_search: 'Search Sessions',
read_workspace_memory_file: 'Read Memory File',
write_workspace_memory_file: 'Write Memory File',
edit_workspace_memory_file: 'Edit Memory File',
list_workspace_memory_files: 'List Memory Files',
// Files & shell
read_file: 'Read File',
execute_shell_command: 'Execute Command',
extract_document_text: 'Extract Document',
extract_pdf_text: 'Extract PDF',
extract_docx_text: 'Extract Document',
detect_file_type: 'Detect File Type',
readMateClawDoc: 'Read System Docs',
// Generative
image_generate: 'Generate Image',
video_generate: 'Generate Video',
music_generate: 'Generate Music',
// Data
query_datasource: 'Query Datasource',
execute_sql: 'Execute SQL',
// Browser
browser_use: 'Browser Action',
// Cron
create_cron_job: 'Create Cron Job',
list_cron_jobs: 'List Cron Jobs',
toggle_cron_job: 'Toggle Cron Job',
delete_cron_job: 'Delete Cron Job',
// Time
getCurrentDateTime: 'Get Current Time',
getCurrentTime: 'Get Current Time',
getCurrentDate: 'Get Current Date',
// Skills
skill_manage: 'Manage Skills',
runSkillScript: 'Run Skill',
listSkillFiles: 'List Skill Files',
listAvailableSkills: 'List Skills',
readSkillFile: 'Read Skill File',
},
} as const

View File

@ -1875,4 +1875,79 @@ export default {
skipped: '没有需要整合的内容',
},
},
// Tool display labels consumed by useToolLabel() — keys must mirror en-US.ts exactly.
// Unknown tools (MCP / custom skills) fall back to humanizeToolName() and are not listed here.
toolLabels: {
// Delegation
delegateToAgent: '委派给智能体',
delegateParallel: '并行委派',
listAvailableAgents: '列出可用智能体',
// Wiki
wiki_search_pages: '搜索知识库',
wiki_semantic_search: '语义检索知识库',
wiki_read_page: '读取知识库页面',
wiki_list_pages: '列出知识库页面',
wiki_related_pages: '查找相关页面',
wiki_trace_source: '溯源知识库',
wiki_create_page: '创建知识库页面',
wiki_delete_page: '删除知识库页面',
wiki_enrich_page: '丰富知识库页面',
wiki_explain_relation: '解释关联关系',
// Web search
search: '联网搜索',
// Memory
recall_structured: '检索记忆',
remember_structured: '保存记忆',
forget_structured: '清除记忆',
fact_probe: '探查事实',
fact_related: '查询关联事实',
fact_list_contradictions: '检查矛盾事实',
session_search: '搜索会话',
read_workspace_memory_file: '读取记忆文件',
write_workspace_memory_file: '写入记忆文件',
edit_workspace_memory_file: '编辑记忆文件',
list_workspace_memory_files: '列出记忆文件',
// Files & shell
read_file: '读取文件',
execute_shell_command: '执行命令',
extract_document_text: '提取文档内容',
extract_pdf_text: '提取 PDF 内容',
extract_docx_text: '提取文档内容',
detect_file_type: '识别文件类型',
readMateClawDoc: '查阅系统文档',
// Generative
image_generate: '生成图片',
video_generate: '生成视频',
music_generate: '生成音乐',
// Data
query_datasource: '查询数据源',
execute_sql: '执行 SQL',
// Browser
browser_use: '浏览器操作',
// Cron
create_cron_job: '创建定时任务',
list_cron_jobs: '查看定时任务',
toggle_cron_job: '启停定时任务',
delete_cron_job: '删除定时任务',
// Time
getCurrentDateTime: '获取当前时间',
getCurrentTime: '获取当前时间',
getCurrentDate: '获取当前日期',
// Skills
skill_manage: '管理技能',
runSkillScript: '运行技能',
listSkillFiles: '列出技能文件',
listAvailableSkills: '列出可用技能',
readSkillFile: '读取技能文件',
},
} as const