feat: improve i18n for panel

This commit is contained in:
Simon
2025-10-15 16:19:52 +08:00
parent 926686c578
commit c6453584db
4 changed files with 119 additions and 138 deletions

View File

@@ -1,5 +1,10 @@
import { type SupportedLanguage, locales } from './locales' import {
import type { TranslationKey, TranslationParams, TranslationSchema } from './types' type SupportedLanguage,
type TranslationKey,
type TranslationParams,
type TranslationSchema,
locales,
} from './locales'
export class I18n { export class I18n {
private language: SupportedLanguage private language: SupportedLanguage

View File

@@ -1,7 +1,55 @@
import type { TranslationSchema } from './types' // English translations (base/reference language)
const enUS = {
ui: {
panel: {
ready: 'Ready',
thinking: 'Thinking...',
paused: 'Paused',
taskInput: 'Enter new task, describe steps in detail, press Enter to submit',
userAnswerPrompt: 'Please answer the question above, press Enter to submit',
taskTerminated: 'Task terminated',
taskCompleted: 'Task completed',
continueExecution: 'Continue execution',
userAnswer: 'User answer: {{input}}',
question: 'Question: {{question}}',
waitingPlaceholder: 'Waiting for task to start...',
pause: 'Pause',
continue: 'Continue',
stop: 'Stop',
expand: 'Expand history',
collapse: 'Collapse history',
step: 'Step {{number}} · {{time}}{{duration}}',
},
tools: {
clicking: 'Clicking element [{{index}}]...',
inputting: 'Inputting text to element [{{index}}]...',
selecting: 'Selecting option "{{text}}"...',
scrolling: 'Scrolling page...',
waiting: 'Waiting {{seconds}} seconds...',
done: 'Task done',
clicked: '🖱️ Clicked element [{{index}}]',
inputted: '⌨️ Inputted text "{{text}}"',
selected: '☑️ Selected option "{{text}}"',
scrolled: '🛞 Page scrolled',
waited: '⌛️ Wait completed',
executing: 'Executing {{toolName}}...',
resultSuccess: 'success',
resultFailure: 'failed',
resultError: 'error',
},
errors: {
elementNotFound: 'No interactive element found at index {{index}}',
taskRequired: 'Task description is required',
executionFailed: 'Task execution failed',
notInputElement: 'Element is not an input or textarea',
notSelectElement: 'Element is not a select element',
optionNotFound: 'Option "{{text}}" not found',
},
},
} as const
// 中文翻译(作为基准) // Chinese translations (must match the structure of enUS)
const zhCN: TranslationSchema = { const zhCN = {
ui: { ui: {
panel: { panel: {
ready: '准备就绪', ready: '准备就绪',
@@ -13,6 +61,8 @@ const zhCN: TranslationSchema = {
taskCompleted: '任务结束', taskCompleted: '任务结束',
continueExecution: '继续执行', continueExecution: '继续执行',
userAnswer: '用户回答: {{input}}', userAnswer: '用户回答: {{input}}',
question: '询问: {{question}}',
waitingPlaceholder: '等待任务开始...',
pause: '暂停', pause: '暂停',
continue: '继续', continue: '继续',
stop: '终止', stop: '终止',
@@ -33,6 +83,9 @@ const zhCN: TranslationSchema = {
scrolled: '🛞 页面滚动完成', scrolled: '🛞 页面滚动完成',
waited: '⌛️ 等待完成', waited: '⌛️ 等待完成',
executing: '正在执行 {{toolName}}...', executing: '正在执行 {{toolName}}...',
resultSuccess: '成功',
resultFailure: '失败',
resultError: '错误',
}, },
errors: { errors: {
elementNotFound: '未找到索引为 {{index}} 的交互元素', elementNotFound: '未找到索引为 {{index}} 的交互元素',
@@ -45,54 +98,29 @@ const zhCN: TranslationSchema = {
}, },
} as const } as const
// 英文翻译(必须符合相同的结构) // Type definitions generated from English base structure (but with string values)
const enUS: TranslationSchema = { type DeepStringify<T> = {
ui: { [K in keyof T]: T[K] extends string ? string : T[K] extends object ? DeepStringify<T[K]> : T[K]
panel: { }
ready: 'Ready',
thinking: 'Thinking...', export type TranslationSchema = DeepStringify<typeof enUS>
paused: 'Paused',
taskInput: 'Enter new task, describe steps in detail, press Enter to submit', // Utility type: Extract all nested paths from translation object
userAnswerPrompt: 'Please answer the question above, press Enter to submit', type NestedKeyOf<ObjectType extends object> = {
taskTerminated: 'Task terminated', [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
taskCompleted: 'Task completed', ? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
continueExecution: 'Continue execution', : `${Key}`
userAnswer: 'User answer: {{input}}', }[keyof ObjectType & (string | number)]
pause: 'Pause',
continue: 'Continue', // Extract all possible key paths from translation structure
stop: 'Stop', export type TranslationKey = NestedKeyOf<TranslationSchema>
expand: 'Expand history',
collapse: 'Collapse history', // Parameterized translation types
step: 'Step {{number}} · {{time}}{{duration}}', export type TranslationParams = Record<string, string | number>
},
tools: {
clicking: 'Clicking element [{{index}}]...',
inputting: 'Inputting text to element [{{index}}]...',
selecting: 'Selecting option "{{text}}"...',
scrolling: 'Scrolling page...',
waiting: 'Waiting {{seconds}} seconds...',
done: 'Task done',
clicked: '🖱️ Clicked element [{{index}}]',
inputted: '⌨️ Inputted text "{{text}}"',
selected: '☑️ Selected option "{{text}}"',
scrolled: '🛞 Page scrolled',
waited: '⌛️ Wait completed',
executing: '正在执行 {{toolName}}...',
},
errors: {
elementNotFound: 'No interactive element found at index {{index}}',
taskRequired: 'Task description is required',
executionFailed: 'Task execution failed',
notInputElement: 'Element is not an input or textarea',
notSelectElement: 'Element is not a select element',
optionNotFound: 'Option "{{text}}" not found',
},
},
} as const
export const locales = { export const locales = {
'zh-CN': zhCN,
'en-US': enUS, 'en-US': enUS,
'zh-CN': zhCN,
} as const } as const
export type SupportedLanguage = keyof typeof locales export type SupportedLanguage = keyof typeof locales

View File

@@ -1,57 +0,0 @@
// 定义翻译数据的结构类型
export interface TranslationSchema {
ui: {
panel: {
ready: string
thinking: string
paused: string
taskInput: string
userAnswerPrompt: string
taskTerminated: string
taskCompleted: string
continueExecution: string
userAnswer: string
pause: string
continue: string
stop: string
expand: string
collapse: string
step: string
}
tools: {
clicking: string
inputting: string
selecting: string
scrolling: string
waiting: string
done: string
clicked: string
inputted: string
selected: string
scrolled: string
waited: string
executing: string
}
errors: {
elementNotFound: string
taskRequired: string
executionFailed: string
notInputElement: string
notSelectElement: string
optionNotFound: string
}
}
}
// 工具类型:提取嵌套对象的所有路径
type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: `${Key}`
}[keyof ObjectType & (string | number)]
// 从翻译结构中提取所有可能的key路径
export type TranslationKey = NestedKeyOf<TranslationSchema>
// 参数化翻译的类型
export type TranslationParams = Record<string, string | number>

View File

@@ -70,10 +70,8 @@ export class Panel {
// Update state to `running` // Update state to `running`
this.#update({ this.#update({
type: 'output', type: 'output',
displayText: `询问: ${question}`, displayText: this.#pageAgent.i18n.t('ui.panel.question', { question }),
}) }) // Expand history panel
// Expand history panel
if (!this.#isExpanded) { if (!this.#isExpanded) {
this.#expand() this.#expand()
} }
@@ -132,7 +130,7 @@ export class Panel {
} }
/** /**
* 隐藏面板 * Hide panel
*/ */
#hide(): void { #hide(): void {
this.wrapper.style.opacity = '0' this.wrapper.style.opacity = '0'
@@ -141,7 +139,7 @@ export class Panel {
} }
/** /**
* 重置状态 * Reset state
*/ */
#reset(): void { #reset(): void {
this.#state.reset() this.#state.reset()
@@ -168,44 +166,44 @@ export class Panel {
// Update status display // Update status display
if (this.#pageAgent.paused) { if (this.#pageAgent.paused) {
this.#statusText.textContent = '暂停中,稍后' this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.paused')
this.#updateStatusIndicator('thinking') // Use existing thinking state this.#updateStatusIndicator('thinking') // Use existing thinking state
} else { } else {
this.#statusText.textContent = '继续执行' this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.continueExecution')
this.#updateStatusIndicator('tool_executing') // Restore to execution state this.#updateStatusIndicator('tool_executing') // Restore to execution state
} }
} }
/** /**
* 更新暂停按钮状态 * Update pause button state
*/ */
#updatePauseButton(): void { #updatePauseButton(): void {
if (this.#pageAgent.paused) { if (this.#pageAgent.paused) {
this.#pauseButton.textContent = '▶' this.#pauseButton.textContent = '▶'
this.#pauseButton.title = '继续' this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.continue')
this.#pauseButton.classList.add(styles.paused) this.#pauseButton.classList.add(styles.paused)
} else { } else {
this.#pauseButton.textContent = '⏸︎' this.#pauseButton.textContent = '⏸︎'
this.#pauseButton.title = '暂停' this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.pause')
this.#pauseButton.classList.remove(styles.paused) this.#pauseButton.classList.remove(styles.paused)
} }
} }
/** /**
* 终止 Agent * Stop Agent
*/ */
#stopAgent(): void { #stopAgent(): void {
// Update status display // Update status display
this.#update({ this.#update({
type: 'error', type: 'error',
displayText: '任务已终止', displayText: this.#pageAgent.i18n.t('ui.panel.taskTerminated'),
}) })
this.#pageAgent.dispose() this.#pageAgent.dispose()
} }
/** /**
* 提交任务 * Submit task
*/ */
#submitTask() { #submitTask() {
const input = this.#taskInput.value.trim() const input = this.#taskInput.value.trim()
@@ -223,13 +221,13 @@ export class Panel {
} }
/** /**
* 处理用户回答 * Handle user answer
*/ */
#handleUserAnswer(input: string): void { #handleUserAnswer(input: string): void {
// Add user input to history // Add user input to history
this.#update({ this.#update({
type: 'input', type: 'input',
displayText: `用户回答: ${input}`, displayText: this.#pageAgent.i18n.t('ui.panel.userAnswer', { input }),
}) })
// Reset state // Reset state
@@ -243,12 +241,12 @@ export class Panel {
} }
/** /**
* 显示输入区域 * Show input area
*/ */
#showInputArea(placeholder?: string): void { #showInputArea(placeholder?: string): void {
// Clear input field // Clear input field
this.#taskInput.value = '' this.#taskInput.value = ''
this.#taskInput.placeholder = placeholder || '输入新任务,详细描述步骤,回车提交' this.#taskInput.placeholder = placeholder || this.#pageAgent.i18n.t('ui.panel.taskInput')
this.#inputSection.classList.remove(styles.hidden) this.#inputSection.classList.remove(styles.hidden)
// Focus on input field // Focus on input field
setTimeout(() => { setTimeout(() => {
@@ -257,14 +255,14 @@ export class Panel {
} }
/** /**
* 隐藏输入区域 * Hide input area
*/ */
#hideInputArea(): void { #hideInputArea(): void {
this.#inputSection.classList.add(styles.hidden) this.#inputSection.classList.add(styles.hidden)
} }
/** /**
* 检查是否应该显示输入区域 * Check if input area should be shown
*/ */
#shouldShowInputArea(): boolean { #shouldShowInputArea(): boolean {
// Always show input area if waiting for user input // Always show input area if waiting for user input
@@ -294,23 +292,23 @@ export class Panel {
stepNumber: 0, stepNumber: 0,
timestamp: new Date(), timestamp: new Date(),
type: 'thinking', type: 'thinking',
displayText: '等待任务开始...', displayText: this.#pageAgent.i18n.t('ui.panel.waitingPlaceholder'),
})} })}
</div> </div>
</div> </div>
<div class="${styles.header}"> <div class="${styles.header}">
<div class="${styles.statusSection}"> <div class="${styles.statusSection}">
<div class="${styles.indicator} ${styles.thinking}"></div> <div class="${styles.indicator} ${styles.thinking}"></div>
<div class="${styles.statusText}">准备就绪</div> <div class="${styles.statusText}">${this.#pageAgent.i18n.t('ui.panel.ready')}</div>
</div> </div>
<div class="${styles.controls}"> <div class="${styles.controls}">
<button class="${styles.controlButton} ${styles.expandButton}" title="展开历史"> <button class="${styles.controlButton} ${styles.expandButton}" title="${this.#pageAgent.i18n.t('ui.panel.expand')}">
</button> </button>
<button class="${styles.controlButton} ${styles.pauseButton}" title="暂停"> <button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#pageAgent.i18n.t('ui.panel.pause')}">
⏸︎ ⏸︎
</button> </button>
<button class="${styles.controlButton} ${styles.stopButton}" title="终止"> <button class="${styles.controlButton} ${styles.stopButton}" title="${this.#pageAgent.i18n.t('ui.panel.stop')}">
X X
</button> </button>
</div> </div>
@@ -459,11 +457,12 @@ export class Panel {
if (step.type === 'completed') { if (step.type === 'completed') {
// Check if this is a result from done tool // Check if this is a result from done tool
if (step.toolName === 'done') { if (step.toolName === 'done') {
// @todo not right
// Judge success or failure based on result // Judge success or failure based on result
const failureKeyword = this.#pageAgent.i18n.t('ui.tools.resultFailure')
const errorKeyword = this.#pageAgent.i18n.t('ui.tools.resultError')
const isSuccess = const isSuccess =
!step.toolResult || !step.toolResult ||
(!step.toolResult.includes('失败') && !step.toolResult.includes('错误')) (!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword))
typeClass = isSuccess ? styles.doneSuccess : styles.doneError typeClass = isSuccess ? styles.doneSuccess : styles.doneError
statusIcon = isSuccess ? '🎉' : '❌' statusIcon = isSuccess ? '🎉' : '❌'
} else { } else {
@@ -488,6 +487,13 @@ export class Panel {
statusIcon = '🧠' statusIcon = '🧠'
} }
const durationText = step.duration ? ` · ${step.duration}ms` : ''
const stepLabel = this.#pageAgent.i18n.t('ui.panel.step', {
number: step.stepNumber.toString(),
time,
duration: durationText,
})
return ` return `
<div class="${styles.historyItem} ${typeClass}"> <div class="${styles.historyItem} ${typeClass}">
<div class="${styles.historyContent}"> <div class="${styles.historyContent}">
@@ -495,8 +501,7 @@ export class Panel {
<span>${step.displayText}</span> <span>${step.displayText}</span>
</div> </div>
<div class="${styles.historyMeta}"> <div class="${styles.historyMeta}">
步骤 ${step.stepNumber} · ${time} ${stepLabel}
${step.duration ? ` · ${step.duration}ms` : ''}
</div> </div>
</div> </div>
` `
@@ -504,7 +509,7 @@ export class Panel {
} }
/** /**
* 获取工具执行时的显示文本 * Get display text for tool execution
*/ */
export function getToolExecutingText(toolName: string, args: any, i18n: I18n): string { export function getToolExecutingText(toolName: string, args: any, i18n: I18n): string {
switch (toolName) { switch (toolName) {
@@ -526,7 +531,7 @@ export function getToolExecutingText(toolName: string, args: any, i18n: I18n): s
} }
/** /**
* 获取工具完成时的显示文本 * Get display text for tool completion
*/ */
export function getToolCompletedText(toolName: string, args: any, i18n: I18n): string | null { export function getToolCompletedText(toolName: string, args: any, i18n: I18n): string | null {
switch (toolName) { switch (toolName) {