diff --git a/.gitignore b/.gitignore index 284e6dea5..217154bd4 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ src-tauri/gen # lwmacct preference .task Taskfile.yml +nul diff --git a/src/components/flow-monitor/ReplayDialog.tsx b/src/components/flow-monitor/ReplayDialog.tsx index 93db0e8bd..2142bc2d3 100644 --- a/src/components/flow-monitor/ReplayDialog.tsx +++ b/src/components/flow-monitor/ReplayDialog.tsx @@ -23,6 +23,7 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; import type { LLMFlow, Message } from "@/lib/api/flowMonitor"; +import { t, formatDuration } from "@/i18n"; // ============================================================================ // 类型定义 @@ -562,7 +563,7 @@ function ReplayResultDisplay({
- 总耗时: {(batchResult.total_duration_ms / 1000).toFixed(2)}s + {t("total_duration", { duration: ((batchResult.total_duration_ms / 1000).toFixed(2)) + "s" })}
@@ -603,21 +604,21 @@ function ReplayResultDisplay({ singleResult.success ? "text-green-600" : "text-red-600", )} > - {singleResult.success ? "重放成功" : "重放失败"} + {singleResult.success ? t("success_with_message", { action: "重放" }) : t("error_with_message", { action: "重放" })} {singleResult.success && singleResult.replay_flow_id && (
- 新 Flow ID: + {t("new_flow_id", { id: "" })} {singleResult.replay_flow_id.slice(0, 16)}...
- 耗时: {singleResult.duration_ms}ms + {t("duration", { duration: formatDuration(singleResult.duration_ms) })}
{onNavigateToFlow && ( + +// After patch (English) + +``` + +### Setup + +```tsx +import { I18nPatchProvider } from '@/i18n'; + +function App() { + return ( + + + + ); +} +``` + +### Language Switching + +```tsx +import { useI18nPatch } from '@/i18n'; + +function LanguageSwitcher() { + const { language, setLanguage } = useI18nPatch(); + + return ( + + ); +} +``` + +--- + +## Dynamic Translations + +### Basic Usage + +```tsx +import { t } from '@/i18n'; + +function FlowDetail({ flow }: { flow: Flow }) { + return ( +
+ {/* Simple translation */} +

{t("new_flow_id", { id: flow.id })}

+ + {/* With formatted duration */} +

{t("total_duration", { duration: formatDuration(flow.duration_ms) })}

+
+ ); +} +``` + +### React Hooks + +#### `useT` - Auto-updating translation + +```tsx +import { useT } from '@/i18n'; + +function StatusIndicator({ status }: { status: string }) { + const statusText = useT("filter_state", { state: status }); + return {statusText}; +} +``` + +#### `useTranslator` - Bound function + +```tsx +import { useTranslator } from '@/i18n'; + +function MyComponent() { + const t = useTranslator(); + + return ( +
+

{t("total_duration", { duration: "1.23s" })}

+

{t("model_name", { model: "claude-sonnet-4" })}

+
+ ); +} +``` + +### Utility Functions + +#### `formatDuration` - Format milliseconds to human-readable string + +```tsx +import { formatDuration, tDuration } from '@/i18n'; + +formatDuration(500); // "500ms" +formatDuration(1234); // "1.23s" +formatDuration(65000); // "1m 5s" + +// Shorthand functions +tDuration(500); // "Duration: 500ms" / "耗时: 500ms" +tTotalDuration(65000); // "Total duration: 1m 5s" / "总耗时: 1m 5s" +``` + +--- + +## Usage Examples + +### Example 1: Flow Monitor + +```tsx +import { t, formatDuration, useT } from '@/i18n'; + +function FlowListItem({ flow }: { flow: Flow }) { + const durationText = useT("total_duration", { + duration: formatDuration(flow.duration_ms) + }); + + return ( +
+

{t("flow_detail_title", { id: flow.id })}

+

{durationText}

+

{t("model_name", { model: flow.model })}

+
+ ); +} +``` + +### Example 2: Error Messages + +```tsx +import { t } from '@/i18n'; + +function showError(action: string, error: Error) { + toast.error(t("error_with_message", { + action, + error: error.message + })); +} + +// Usage: +showError("保存", new Error("网络连接失败")); +// zh: "保存失败: 网络连接失败" +// en: "Save failed: Network connection failed" +``` + +### Example 3: Batch Operations + +```tsx +import { t } from '@/i18n'; + +function BatchOperationBar({ selected }: { selected: Flow[] }) { + return ( +
+ {t("selected_flow_count", { count: selected.length })} + {t("selected_total", { + selected: selected.length, + total: 100 + })} +
+ ); +} +``` + +### Example 4: Configuration Display + +```tsx +import { t } from '@/i18n'; + +function ConfigItem({ name, type, provider }: Config) { + return ( +
+

{t("credential_label", { + type, + provider, + id: uuid.slice(0, 8) + })}

+

{t("credential_pool_label", { provider })}

+
+ ); +} +``` + +--- + +## Adding New Translations + +### Adding Static Translations + +1. **Add to `zh.json`** (Chinese - identity mapping): +```json +{ + "我的新文本": "我的新文本" +} +``` + +2. **Add to `en.json`** (English translation): +```json +{ + "我的新文本": "My New Text" +} +``` + +3. **Use in component** (no changes needed!): +```tsx + +``` + +### Adding Dynamic Templates + +1. **Add to `dynamic-translation.ts`**: +```typescript +const TEMPLATES: Record = { + "my_new_template": { + zh: "你好 {name}, 今天是 {date}", + en: "Hello {name}, today is {date}", + }, + // ... +}; +``` + +2. **Use in component**: +```tsx +import { t } from '@/i18n'; + +function Greeting({ name, date }: Props) { + return

{t("my_new_template", { name, date })}

; +} +``` + +3. **Add to JSON files** (optional, for documentation): +```json +// zh.json +"TEMPLATE:my_new_template": "你好 {name}, 今天是 {date}" + +// en.json +"TEMPLATE:my_new_template": "Hello {name}, today is {date}" +``` + +--- + +## Available Template Keys + +### Flow Monitor +| Key | Chinese | English | +|-----|---------|---------| +| `new_flow_id` | 新 Flow ID: {id} | New Flow ID: {id} | +| `total_duration` | 总耗时: {duration} | Total duration: {duration} | +| `duration` | 耗时: {duration} | Duration: {duration} | +| `model_name` | 模型: {model} | Model: {model} | +| `flow_detail_title` | Flow #{id} | Flow #{id} | +| `selected_flow_count` | 已选择 {count} 个 Flow | Selected {count} Flow(s) | +| `confirm_delete_flows` | 确定要删除选中的 {count} 个 Flow 吗? | Are you sure you want to delete the selected {count} Flow(s)? | + +### Provider/Clients +| Key | Chinese | English | +|-----|---------|---------| +| `config_mismatch` | 实际生效的配置与当前选中的 '{provider}' 不一致 | Actual effective configuration differs from currently selected '{provider}' | +| `actual_value` | 实际: {value} | Actual: {value} | +| `current_value` | 当前: {value} | Current: {value} | +| `credential_label` | {type} 凭证: {provider} - {id} | {type} credential: {provider} - {id} | +| `credential_pool_label` | 凭证池: {provider} | Credential pool: {provider} | +| `confirm_delete_config` | 确定要删除配置 "{name}" 吗?此操作无法撤销。 | Are you sure you want to delete configuration "{name}"? This action cannot be undone. | + +### General Patterns +| Key | Chinese | English | +|-----|---------|---------| +| `items_count` | 共 {count} 个{item} | Total {count} {item}(s) | +| `loading_with_item` | 正在加载{item}... | Loading {item}... | +| `error_with_message` | {action}失败: {error} | {action} failed: {error} | +| `success_with_message` | {action}成功: {message} | {action} successful: {message} | +| `selected_total` | 已选择 {selected} / {total} | Selected {selected} of {total} | +| `total_records` | 共 {total} 条记录 | Total {total} record(s) | + +### API Server +| Key | Chinese | English | +|-----|---------|---------| +| `start_failed` | 启动失败: {error} | Start failed: {error} | +| `stop_failed` | 停止失败: {error} | Stop failed: {error} | +| `switched_to` | 已切换到 {provider} | Switched to {provider} | +| `switch_failed` | 切换失败: {error} | Switch failed: {error} | +| `request_failed` | 请求失败: {error} | Request failed: {error} | + +### And 40+ more templates... + +See `src/i18n/dynamic-translation.ts` for the complete list. + +--- + +## Best Practices + +### 1. Use Static Translations for Simple Text + +```tsx +// ✅ Good - Static text + + +// ❌ Bad - Unnecessary dynamic translation + +``` + +### 2. Use Dynamic Templates for Variable Text + +```tsx +// ✅ Good - Dynamic translation +

{t("total_duration", { duration: formatDuration(ms) })}

+ +// ❌ Bad - Hardcoded Chinese +

总耗时: {(ms / 1000).toFixed(2)}s

+``` + +### 3. Use `useT` for Text That Changes + +```tsx +// ✅ Good - Auto-updates on language change +const title = useT("total_duration", { duration: formatDuration(ms) }); + +// ⚠️ Okay - Won't auto-update +const title = t("total_duration", { duration: formatDuration(ms) }); +``` + +### 4. Extract Common Patterns + +```tsx +// ❌ Bad - Repeated code +

{count} 个模型

+

{count} 个请求

+ +// ✅ Good - Use template +

{t("items_count", { count, item: "模型" })}

+

{t("items_count", { count, item: "请求" })}

+``` + +### 5. Format Numbers and Dates Properly + +```tsx +import { t } from '@/i18n'; + +// ✅ Good - Format before passing to t() +

{t("total_duration", { duration: formatDuration(ms) })}

+

{t("date_display", { date: formatDate(new Date()) })}

+ +// ❌ Bad - Format in template +

{t("total_duration_ms", { ms })}

+``` + +--- + +## Troubleshooting + +### Text Not Translating + +**Problem:** Chinese text still appears in English mode + +**Solutions:** +1. Check if text is in `zh.json` and `en.json` +2. Ensure `I18nPatchProvider` wraps your app +3. Check browser console for i18n errors +4. Verify text is not in `` or `