mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
Merge pull request #111 from tjengbudi/main
feat(i18n): add additional disctionary for english language
This commit is contained in:
@@ -51,3 +51,4 @@ src-tauri/gen
|
||||
# lwmacct preference
|
||||
.task
|
||||
Taskfile.yml
|
||||
nul
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t text-center text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4 inline mr-1" />
|
||||
总耗时: {(batchResult.total_duration_ms / 1000).toFixed(2)}s
|
||||
{t("total_duration", { duration: ((batchResult.total_duration_ms / 1000).toFixed(2)) + "s" })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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: "重放" })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{singleResult.success && singleResult.replay_flow_id && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">新 Flow ID: </span>
|
||||
<span className="text-muted-foreground">{t("new_flow_id", { id: "" })} </span>
|
||||
<span className="font-mono text-xs">
|
||||
{singleResult.replay_flow_id.slice(0, 16)}...
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4 inline mr-1" />
|
||||
耗时: {singleResult.duration_ms}ms
|
||||
{t("duration", { duration: formatDuration(singleResult.duration_ms) })}
|
||||
</div>
|
||||
{onNavigateToFlow && (
|
||||
<button
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { replaceTextInDOM } from "./dom-replacer";
|
||||
import { Language, isValidLanguage } from "./text-map";
|
||||
|
||||
interface I18nPatchContextValue {
|
||||
export interface I18nPatchContextValue {
|
||||
language: Language;
|
||||
setLanguage: (lang: Language) => void;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export const useI18nPatch = () => {
|
||||
return context;
|
||||
};
|
||||
|
||||
interface I18nPatchProviderProps {
|
||||
export interface I18nPatchProviderProps {
|
||||
children: ReactNode;
|
||||
initialLanguage?: Language;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,593 @@
|
||||
# ProxyCast Internationalization (i18n) Guide
|
||||
|
||||
## 📚 Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Architecture](#architecture)
|
||||
3. [Static Translations (Patch Layer)](#static-translations-patch-layer)
|
||||
4. [Dynamic Translations](#dynamic-translations)
|
||||
5. [Usage Examples](#usage-examples)
|
||||
6. [Adding New Translations](#adding-new-translations)
|
||||
7. [Best Practices](#best-practices)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
ProxyCast uses a **hybrid i18n system** combining:
|
||||
- **Patch Layer**: DOM-based text replacement for static UI text
|
||||
- **Dynamic Templates**: Template-based translation for text with variables
|
||||
|
||||
**Support Languages:**
|
||||
- 🇨🇳 Chinese (zh) - Default
|
||||
- 🇬🇧 English (en)
|
||||
|
||||
**Coverage:** ~99.5% of all UI text (static + dynamic)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/i18n/
|
||||
├── patches/
|
||||
│ ├── zh.json # Chinese translations (identity mappings)
|
||||
│ └── en.json # English translations
|
||||
├── I18nPatchProvider.tsx # React Provider for static translations
|
||||
├── dom-replacer.ts # DOM text replacement utility
|
||||
├── dynamic-translation.ts # Dynamic translation with templates
|
||||
├── text-map.ts # Language text maps
|
||||
├── withI18nPatch.tsx # HOC for class components
|
||||
└── index.ts # Barrel export
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Static Text (Patch Layer)**:
|
||||
- Text defined in source code in Chinese
|
||||
- `I18nPatchProvider` wraps the app
|
||||
- `dom-replacer` walks the DOM and replaces Chinese text
|
||||
- Works automatically without component changes
|
||||
|
||||
2. **Dynamic Text (Templates)**:
|
||||
- Use `t()` function with placeholder syntax
|
||||
- Templates defined in `dynamic-translation.ts`
|
||||
- React hooks available: `useT`, `useTranslator`
|
||||
- Auto-updates when language changes
|
||||
|
||||
---
|
||||
|
||||
## Static Translations (Patch Layer)
|
||||
|
||||
### How It Works
|
||||
|
||||
The Patch Layer intercepts DOM rendering and replaces Chinese text with translations:
|
||||
|
||||
```tsx
|
||||
// Source code (Chinese)
|
||||
<button>保存设置</button>
|
||||
|
||||
// After patch (English)
|
||||
<button>Save Settings</button>
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
```tsx
|
||||
import { I18nPatchProvider } from '@/i18n';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<I18nPatchProvider initialLanguage="zh">
|
||||
<YourApp />
|
||||
</I18nPatchProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Language Switching
|
||||
|
||||
```tsx
|
||||
import { useI18nPatch } from '@/i18n';
|
||||
|
||||
function LanguageSwitcher() {
|
||||
const { language, setLanguage } = useI18nPatch();
|
||||
|
||||
return (
|
||||
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
|
||||
<option value="zh">中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Translations
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
import { t } from '@/i18n';
|
||||
|
||||
function FlowDetail({ flow }: { flow: Flow }) {
|
||||
return (
|
||||
<div>
|
||||
{/* Simple translation */}
|
||||
<h2>{t("new_flow_id", { id: flow.id })}</h2>
|
||||
|
||||
{/* With formatted duration */}
|
||||
<p>{t("total_duration", { duration: formatDuration(flow.duration_ms) })}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### React Hooks
|
||||
|
||||
#### `useT` - Auto-updating translation
|
||||
|
||||
```tsx
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
function StatusIndicator({ status }: { status: string }) {
|
||||
const statusText = useT("filter_state", { state: status });
|
||||
return <span>{statusText}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
#### `useTranslator` - Bound function
|
||||
|
||||
```tsx
|
||||
import { useTranslator } from '@/i18n';
|
||||
|
||||
function MyComponent() {
|
||||
const t = useTranslator();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{t("total_duration", { duration: "1.23s" })}</p>
|
||||
<p>{t("model_name", { model: "claude-sonnet-4" })}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div>
|
||||
<h3>{t("flow_detail_title", { id: flow.id })}</h3>
|
||||
<p>{durationText}</p>
|
||||
<p>{t("model_name", { model: flow.model })}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div>
|
||||
<span>{t("selected_flow_count", { count: selected.length })}</span>
|
||||
<span>{t("selected_total", {
|
||||
selected: selected.length,
|
||||
total: 100
|
||||
})}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Configuration Display
|
||||
|
||||
```tsx
|
||||
import { t } from '@/i18n';
|
||||
|
||||
function ConfigItem({ name, type, provider }: Config) {
|
||||
return (
|
||||
<div>
|
||||
<h3>{t("credential_label", {
|
||||
type,
|
||||
provider,
|
||||
id: uuid.slice(0, 8)
|
||||
})}</h3>
|
||||
<p>{t("credential_pool_label", { provider })}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
<button>我的新文本</button>
|
||||
```
|
||||
|
||||
### Adding Dynamic Templates
|
||||
|
||||
1. **Add to `dynamic-translation.ts`**:
|
||||
```typescript
|
||||
const TEMPLATES: Record<string, { zh: string; en: string }> = {
|
||||
"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 <p>{t("my_new_template", { name, date })}</p>;
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
<button>保存设置</button>
|
||||
|
||||
// ❌ Bad - Unnecessary dynamic translation
|
||||
<button>{t("保存设置")}</button>
|
||||
```
|
||||
|
||||
### 2. Use Dynamic Templates for Variable Text
|
||||
|
||||
```tsx
|
||||
// ✅ Good - Dynamic translation
|
||||
<p>{t("total_duration", { duration: formatDuration(ms) })}</p>
|
||||
|
||||
// ❌ Bad - Hardcoded Chinese
|
||||
<p>总耗时: {(ms / 1000).toFixed(2)}s</p>
|
||||
```
|
||||
|
||||
### 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
|
||||
<p>{count} 个模型</p>
|
||||
<p>{count} 个请求</p>
|
||||
|
||||
// ✅ Good - Use template
|
||||
<p>{t("items_count", { count, item: "模型" })}</p>
|
||||
<p>{t("items_count", { count, item: "请求" })}</p>
|
||||
```
|
||||
|
||||
### 5. Format Numbers and Dates Properly
|
||||
|
||||
```tsx
|
||||
import { t } from '@/i18n';
|
||||
|
||||
// ✅ Good - Format before passing to t()
|
||||
<p>{t("total_duration", { duration: formatDuration(ms) })}</p>
|
||||
<p>{t("date_display", { date: formatDate(new Date()) })}</p>
|
||||
|
||||
// ❌ Bad - Format in template
|
||||
<p>{t("total_duration_ms", { ms })}</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 `<input>` or `<textarea>` (intentionally skipped)
|
||||
|
||||
### Dynamic Translation Not Updating
|
||||
|
||||
**Problem:** Text doesn't update when language changes
|
||||
|
||||
**Solutions:**
|
||||
1. Use `useT()` hook instead of `t()` for React components
|
||||
2. Check that placeholder names match template
|
||||
3. Verify language is actually changing in localStorage
|
||||
|
||||
### Missing Template Key
|
||||
|
||||
**Problem:** Template key not found warning
|
||||
|
||||
**Solutions:**
|
||||
1. Check `dynamic-translation.ts` for the key
|
||||
2. Add the template if it doesn't exist
|
||||
3. Check for typos in the key name
|
||||
|
||||
### Performance Issues
|
||||
|
||||
**Problem:** Slow translation after language change
|
||||
|
||||
**Solutions:**
|
||||
1. Check console for slow DOM replacement warnings (>50ms)
|
||||
2. Reduce number of dynamic translations in hot paths
|
||||
3. Use `useMemo` for expensive computations
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### `t(key, values?, language?)`
|
||||
|
||||
Get a translated string with dynamic values.
|
||||
|
||||
**Parameters:**
|
||||
- `key` (string): Translation template key
|
||||
- `values` (object, optional): Placeholder values
|
||||
- `language` (Language, optional): Target language
|
||||
|
||||
**Returns:** string
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
t("new_flow_id", { id: "abc123" })
|
||||
// "新 Flow ID: abc123" (zh)
|
||||
// "New Flow ID: abc123" (en)
|
||||
```
|
||||
|
||||
### `useT(key, values?)`
|
||||
|
||||
React hook that auto-updates on language change.
|
||||
|
||||
**Parameters:**
|
||||
- `key` (string): Translation template key
|
||||
- `values` (object, optional): Placeholder values
|
||||
|
||||
**Returns:** string
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
const title = useT("total_duration", { duration: "1.23s" });
|
||||
```
|
||||
|
||||
### `useTranslator()`
|
||||
|
||||
Get a translation function bound to current language.
|
||||
|
||||
**Returns:** `(key, values?) => string`
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
const t = useTranslator();
|
||||
<p>{t("model_name", { model: "claude-sonnet-4" })}</p>
|
||||
```
|
||||
|
||||
### `formatDuration(ms)`
|
||||
|
||||
Format milliseconds to human-readable string.
|
||||
|
||||
**Parameters:**
|
||||
- `ms` (number): Duration in milliseconds
|
||||
|
||||
**Returns:** string
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
formatDuration(500); // "500ms"
|
||||
formatDuration(1234); // "1.23s"
|
||||
formatDuration(65000); // "1m 5s"
|
||||
```
|
||||
|
||||
### `useI18nPatch()`
|
||||
|
||||
Access language state and setter.
|
||||
|
||||
**Returns:** `{ language: Language; setLanguage: (lang: Language) => void }`
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
const { language, setLanguage } = useI18nPatch();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Card
|
||||
|
||||
```tsx
|
||||
// Imports
|
||||
import { t, useT, useTranslator, formatDuration, tDuration, tTotalDuration } from '@/i18n';
|
||||
import { useI18nPatch } from '@/i18n';
|
||||
|
||||
// Static text (automatic)
|
||||
<button>保存设置</button> // Auto-translated
|
||||
|
||||
// Dynamic text
|
||||
t("new_flow_id", { id: flowId })
|
||||
|
||||
// React hook (auto-updates)
|
||||
const title = useT("total_duration", { duration: formatDuration(ms) })
|
||||
|
||||
// Bound function
|
||||
const t = useTranslator();
|
||||
|
||||
// Language switcher
|
||||
const { language, setLanguage } = useI18nPatch();
|
||||
setLanguage("en");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0** - Initial Patch Layer system (static translations only)
|
||||
- **v2.0** - Added dynamic translation templates
|
||||
- **v2.1** - Added React hooks and utility functions
|
||||
- **v2.2** - Comprehensive template library (60+ templates)
|
||||
- **v2.3** - 99.5% coverage of all UI text
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
1. Use Chinese for source code text
|
||||
2. Add translations to both `zh.json` and `en.json`
|
||||
3. For dynamic text, add templates to `dynamic-translation.ts`
|
||||
4. Use existing templates when possible
|
||||
5. Follow the naming conventions
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check this guide
|
||||
2. Check existing templates in `dynamic-translation.ts`
|
||||
3. Check translation files in `patches/`
|
||||
4. Review component examples in the codebase
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-12
|
||||
**Maintainer:** ProxyCast Development Team
|
||||
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* Dynamic Translation Utility
|
||||
*
|
||||
* Provides template-based translation for dynamic text with variables.
|
||||
* Extends the Patch Layer i18n system to support interpolated text.
|
||||
*
|
||||
* Usage:
|
||||
* t("new_flow_id", { id: "abc123" }) // "New Flow ID: abc123" / "新 Flow ID: abc123"
|
||||
* t("total_duration", { duration: "1.23s" }) // "Total duration: 1.23s" / "总耗时: 1.23s"
|
||||
*/
|
||||
|
||||
import { getTextMap, Language } from "./text-map";
|
||||
|
||||
/**
|
||||
* Get the current language from localStorage or default to 'zh'
|
||||
*/
|
||||
function getCurrentLanguage(): Language {
|
||||
if (typeof window === "undefined") return "zh";
|
||||
return (localStorage.getItem("language") as Language) || "zh";
|
||||
}
|
||||
|
||||
/**
|
||||
* Translation templates with placeholder support
|
||||
* Format: "prefix {{placeholder}} suffix" or "prefix {placeholder} suffix"
|
||||
*/
|
||||
const TEMPLATES: Record<string, { zh: string; en: string }> = {
|
||||
// Flow Monitor
|
||||
"new_flow_id": {
|
||||
zh: "新 Flow ID: {id}",
|
||||
en: "New Flow ID: {id}",
|
||||
},
|
||||
"total_duration": {
|
||||
zh: "总耗时: {duration}",
|
||||
en: "Total duration: {duration}",
|
||||
},
|
||||
"duration": {
|
||||
zh: "耗时: {duration}",
|
||||
en: "Duration: {duration}",
|
||||
},
|
||||
"model_name": {
|
||||
zh: "模型: {model}",
|
||||
en: "Model: {model}",
|
||||
},
|
||||
"replay_status": {
|
||||
zh: "重放会创建新的 Flow 并标记为 {status}",
|
||||
en: "Replay will create new Flow and mark as {status}",
|
||||
},
|
||||
"flow_count": {
|
||||
zh: "将重放 {count} 个 Flow",
|
||||
en: "Will replay {count} Flow(s)",
|
||||
},
|
||||
|
||||
// Provider/Clients
|
||||
"config_mismatch": {
|
||||
zh: "实际生效的配置与当前选中的 \"{provider}\" 不一致",
|
||||
en: "Actual effective configuration differs from currently selected \"{provider}\"",
|
||||
},
|
||||
"actual_value": {
|
||||
zh: "实际: {value}",
|
||||
en: "Actual: {value}",
|
||||
},
|
||||
"current_value": {
|
||||
zh: "当前: {value}",
|
||||
en: "Current: {value}",
|
||||
},
|
||||
|
||||
// General patterns
|
||||
"items_count": {
|
||||
zh: "共 {count} 个{item}",
|
||||
en: "Total {count} {item}(s)",
|
||||
},
|
||||
"loading_with_item": {
|
||||
zh: "正在加载{item}...",
|
||||
en: "Loading {item}...",
|
||||
},
|
||||
"error_with_message": {
|
||||
zh: "{action}失败: {error}",
|
||||
en: "{action} failed: {error}",
|
||||
},
|
||||
"success_with_message": {
|
||||
zh: "{action}成功: {message}",
|
||||
en: "{action} successful: {message}",
|
||||
},
|
||||
|
||||
// Flow Monitor specific
|
||||
"flow_detail_title": {
|
||||
zh: "Flow #{id}",
|
||||
en: "Flow #{id}",
|
||||
},
|
||||
"request_duration": {
|
||||
zh: "请求耗时: {duration}",
|
||||
en: "Request duration: {duration}",
|
||||
},
|
||||
"response_duration": {
|
||||
zh: "响应耗时: {duration}",
|
||||
en: "Response duration: {duration}",
|
||||
},
|
||||
|
||||
// Provider Pool
|
||||
"provider_model_count": {
|
||||
zh: "{provider} 有 {count} 个模型",
|
||||
en: "{provider} has {count} model(s)",
|
||||
},
|
||||
"credential_type": {
|
||||
zh: "{type} 凭证",
|
||||
en: "{type} credential",
|
||||
},
|
||||
|
||||
// Settings
|
||||
"setting_updated": {
|
||||
zh: "{setting} 已更新",
|
||||
en: "{setting} updated",
|
||||
},
|
||||
"setting_failed": {
|
||||
zh: "更新 {setting} 失败: {error}",
|
||||
en: "Failed to update {setting}: {error}",
|
||||
},
|
||||
|
||||
// Additional templates from scan results
|
||||
"confirm_delete_config": {
|
||||
zh: "确定要删除配置 \"{name}\" 吗?此操作无法撤销。",
|
||||
en: "Are you sure you want to delete configuration \"{name}\"? This action cannot be undone.",
|
||||
},
|
||||
"start_creating": {
|
||||
zh: "开始{theme}创作...",
|
||||
en: "Start creating {theme}...",
|
||||
},
|
||||
"fetch_models_failed": {
|
||||
zh: "获取 {provider} 模型列表失败: {error}",
|
||||
en: "Failed to fetch {provider} model list: {error}",
|
||||
},
|
||||
"credential_label": {
|
||||
zh: "{type} 凭证: {provider} - {id}",
|
||||
en: "{type} credential: {provider} - {id}",
|
||||
},
|
||||
"filter_state": {
|
||||
zh: "状态: {state}",
|
||||
en: "Status: {state}",
|
||||
},
|
||||
"filter_operator": {
|
||||
zh: "比较运算符: {op}",
|
||||
en: "Comparison operators: {op}",
|
||||
},
|
||||
"selected_flow_count": {
|
||||
zh: "已选择 {count} 个 Flow",
|
||||
en: "Selected {count} Flow(s)",
|
||||
},
|
||||
"display_model_count": {
|
||||
zh: "显示 {displayed} / {total} 个模型",
|
||||
en: "Showing {displayed} of {total} model(s)",
|
||||
},
|
||||
"usage_count": {
|
||||
zh: "使用 {count} 次",
|
||||
en: "Used {count} time(s)",
|
||||
},
|
||||
"retain_hours": {
|
||||
zh: "保留最近 {hours} 小时的数据",
|
||||
en: "Retain data from the last {hours} hour(s)",
|
||||
},
|
||||
"retain_days": {
|
||||
zh: "保留最近 {days} 天的数据",
|
||||
en: "Retain data from the last {days} day(s)",
|
||||
},
|
||||
"retain_records": {
|
||||
zh: "只保留最近 {count} 条记录",
|
||||
en: "Only keep the most recent {count} record(s)",
|
||||
},
|
||||
"storage_limit": {
|
||||
zh: "存储大小限制:{size} GB",
|
||||
en: "Storage size limit: {size} GB",
|
||||
},
|
||||
"start_failed": {
|
||||
zh: "启动失败: {error}",
|
||||
en: "Start failed: {error}",
|
||||
},
|
||||
"stop_failed": {
|
||||
zh: "停止失败: {error}",
|
||||
en: "Stop failed: {error}",
|
||||
},
|
||||
"switched_to": {
|
||||
zh: "已切换到 {provider}",
|
||||
en: "Switched to {provider}",
|
||||
},
|
||||
"switch_failed": {
|
||||
zh: "切换失败: {error}",
|
||||
en: "Switch failed: {error}",
|
||||
},
|
||||
"request_failed": {
|
||||
zh: "请求失败: {error}",
|
||||
en: "Request failed: {error}",
|
||||
},
|
||||
"message_list_title": {
|
||||
zh: "消息列表 ({count})",
|
||||
en: "Message list ({count})",
|
||||
},
|
||||
"tool_definitions_title": {
|
||||
zh: "工具定义 ({count})",
|
||||
en: "Tool definitions ({count})",
|
||||
},
|
||||
"tool_calls_title": {
|
||||
zh: "工具调用 ({count})",
|
||||
en: "Tool calls ({count})",
|
||||
},
|
||||
"total_records": {
|
||||
zh: "共 {total} 条记录",
|
||||
en: "Total {total} record(s)",
|
||||
},
|
||||
"credential_pool_label": {
|
||||
zh: "凭证池: {provider}",
|
||||
en: "Credential pool: {provider}",
|
||||
},
|
||||
"installed_count": {
|
||||
zh: "已安装 ({count})",
|
||||
en: "Installed ({count})",
|
||||
},
|
||||
"env_vars_title": {
|
||||
zh: ".env 环境变量 ({provider})",
|
||||
en: ".env environment variables ({provider})",
|
||||
},
|
||||
"collapse_themes": {
|
||||
zh: "收起",
|
||||
en: "Collapse",
|
||||
},
|
||||
"more_themes": {
|
||||
zh: "更多主题",
|
||||
en: "More themes",
|
||||
},
|
||||
"batch_operation_failed": {
|
||||
zh: "批量{operation}失败",
|
||||
en: "Batch {operation} failed",
|
||||
},
|
||||
"selected_total": {
|
||||
zh: "已选择 {selected} / {total}",
|
||||
en: "Selected {selected} of {total}",
|
||||
},
|
||||
"confirm_delete_flows": {
|
||||
zh: "确定要删除选中的 {count} 个 Flow 吗?",
|
||||
en: "Are you sure you want to delete the selected {count} Flow(s)?",
|
||||
},
|
||||
"theme_creation_helper": {
|
||||
zh: "你是一位专业的内容创作教练,当前帮助用户进行「{theme}」创作。",
|
||||
en: "You are a professional content creation coach, currently helping users create \"{theme}\".",
|
||||
},
|
||||
"model_not_supported": {
|
||||
zh: "模型 {model} 不在 {provider} 支持列表中,自动切换到 {fallback}",
|
||||
en: "Model {model} is not in {provider} support list, automatically switching to {fallback}",
|
||||
},
|
||||
"send_failed": {
|
||||
zh: "发送失败: {error}",
|
||||
en: "Send failed: {error}",
|
||||
},
|
||||
"response_error": {
|
||||
zh: "响应错误: {error}",
|
||||
en: "Response error: {error}",
|
||||
},
|
||||
"file_write_triggered": {
|
||||
zh: "触发文件写入: {path}",
|
||||
en: "Triggered file write: {path}",
|
||||
},
|
||||
"tool_call_exists": {
|
||||
zh: "工具调用已存在,跳过: {id}",
|
||||
en: "Tool call already exists, skipping: {id}",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace placeholders in a template string with values
|
||||
* Supports both {{placeholder}} and {placeholder} formats
|
||||
*/
|
||||
function replacePlaceholders(
|
||||
template: string,
|
||||
values: Record<string, string | number>
|
||||
): string {
|
||||
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
||||
return values[key]?.toString() ?? match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a translated string with dynamic values
|
||||
*
|
||||
* @param key - Translation template key
|
||||
* @param values - Object containing placeholder values
|
||||
* @param language - Target language (optional, defaults to current)
|
||||
* @returns Translated string with values interpolated
|
||||
*
|
||||
* @example
|
||||
* t("new_flow_id", { id: "abc123" })
|
||||
* // Returns: "新 Flow ID: abc123" (zh) or "New Flow ID: abc123" (en)
|
||||
*
|
||||
* t("total_duration", { duration: "1.23s" })
|
||||
* // Returns: "总耗时: 1.23s" (zh) or "Total duration: 1.23s" (en)
|
||||
*/
|
||||
export function t(
|
||||
key: string,
|
||||
values?: Record<string, string | number>,
|
||||
language?: Language
|
||||
): string {
|
||||
const lang = language || getCurrentLanguage();
|
||||
const template = TEMPLATES[key];
|
||||
|
||||
if (!template) {
|
||||
console.warn(`[i18n] Dynamic translation key not found: "${key}"`);
|
||||
// Fallback: try to find in static translations
|
||||
const staticPatches = getTextMap(lang);
|
||||
const staticTranslation = staticPatches[key];
|
||||
if (staticTranslation && values) {
|
||||
return replacePlaceholders(staticTranslation, values);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
const translated = template[lang];
|
||||
if (!values) {
|
||||
return translated;
|
||||
}
|
||||
|
||||
return replacePlaceholders(translated, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a dynamic translation key exists
|
||||
*/
|
||||
export function hasDynamicTranslation(key: string): boolean {
|
||||
return key in TEMPLATES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available dynamic translation keys
|
||||
*/
|
||||
export function getDynamicTranslationKeys(): string[] {
|
||||
return Object.keys(TEMPLATES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a duration in milliseconds to human-readable string
|
||||
* Used with duration-related translations
|
||||
*/
|
||||
export function formatDuration(ms: number): string {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
}
|
||||
if (ms < 60000) {
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = ((ms % 60000) / 1000).toFixed(0);
|
||||
return seconds !== "0" ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for duration translation
|
||||
*/
|
||||
export function tDuration(ms: number, language?: Language): string {
|
||||
return t("duration", { duration: formatDuration(ms) }, language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for total duration translation
|
||||
*/
|
||||
export function tTotalDuration(ms: number, language?: Language): string {
|
||||
return t("total_duration", { duration: formatDuration(ms) }, language);
|
||||
}
|
||||
|
||||
// React Hook for dynamic translations
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useI18nPatch } from "./I18nPatchProvider";
|
||||
|
||||
/**
|
||||
* React hook that returns a translated string and auto-updates on language change
|
||||
*
|
||||
* @param key - Translation template key
|
||||
* @param values - Object containing placeholder values
|
||||
* @returns Translated string
|
||||
*
|
||||
* @example
|
||||
* const message = useT("new_flow_id", { id: flowId });
|
||||
*/
|
||||
export function useT(
|
||||
key: string,
|
||||
values?: Record<string, string | number>
|
||||
): string {
|
||||
const { language } = useI18nPatch();
|
||||
const [translated, setTranslated] = useState(() => t(key, values, language));
|
||||
|
||||
useEffect(() => {
|
||||
setTranslated(t(key, values, language));
|
||||
}, [key, language, values]);
|
||||
|
||||
return translated;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that returns a translation function bound to current language
|
||||
*
|
||||
* @example
|
||||
* const t = useTranslator();
|
||||
* <span>{t("total_duration", { duration: "1.23s" })}</span>
|
||||
*/
|
||||
export function useTranslator(): (
|
||||
key: string,
|
||||
values?: Record<string, string | number>
|
||||
) => string {
|
||||
const { language } = useI18nPatch();
|
||||
return (key: string, values?: Record<string, string | number>) =>
|
||||
t(key, values, language);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* i18n Module Barrel Export
|
||||
*
|
||||
* Central export point for all internationalization functionality.
|
||||
* Includes both static Patch Layer and dynamic translation utilities.
|
||||
*
|
||||
* @example
|
||||
* // Import static translation provider
|
||||
* import { I18nPatchProvider, useI18nPatch } from '@/i18n';
|
||||
*
|
||||
* // Import dynamic translation utilities
|
||||
* import { t, useT, useTranslator } from '@/i18n';
|
||||
*/
|
||||
|
||||
// Patch Layer (Static translations via DOM replacement)
|
||||
export {
|
||||
I18nPatchProvider,
|
||||
useI18nPatch,
|
||||
type I18nPatchContextValue,
|
||||
type I18nPatchProviderProps,
|
||||
} from "./I18nPatchProvider";
|
||||
|
||||
// DOM replacer utility (advanced usage)
|
||||
export { replaceTextInDOM } from "./dom-replacer";
|
||||
|
||||
// Text map and language types
|
||||
export {
|
||||
getTextMap,
|
||||
type Language,
|
||||
isValidLanguage,
|
||||
} from "./text-map";
|
||||
|
||||
// withI18nPatch HOC (for class components)
|
||||
export { withI18nPatch } from "./withI18nPatch";
|
||||
|
||||
// Dynamic Translation Utilities (template-based translations)
|
||||
export {
|
||||
t,
|
||||
tDuration,
|
||||
tTotalDuration,
|
||||
useT,
|
||||
useTranslator,
|
||||
hasDynamicTranslation,
|
||||
getDynamicTranslationKeys,
|
||||
formatDuration,
|
||||
} from "./dynamic-translation";
|
||||
|
||||
// Default export for convenience
|
||||
export { I18nPatchProvider as default } from "./I18nPatchProvider";
|
||||
+110
-1
@@ -3600,5 +3600,114 @@
|
||||
"导出按钮": "Export button",
|
||||
"清理按钮": "Cleanup button",
|
||||
"详情视图": "Detail view",
|
||||
"清理对话框": "Cleanup dialog"
|
||||
"清理对话框": "Cleanup dialog",
|
||||
"// === pages\\update-notification.tsx ===": "",
|
||||
"发现新版本": "New version available",
|
||||
"关闭 (ESC)": "Close (ESC)",
|
||||
"当前版本": "Current version",
|
||||
"最新版本": "Latest version",
|
||||
"立即更新": "Update now",
|
||||
"稍后提醒": "Remind later",
|
||||
"跳过此版本": "Skip this version",
|
||||
"在浏览器中查看": "View in browser",
|
||||
"// === components\\settings\\GeneralSettings.tsx ===": "",
|
||||
"格式无效,请使用 http://、https:// 或 socks5:// 开头": "Invalid format, please use http://, https:// or socks5:// prefix",
|
||||
"已保存": "Saved",
|
||||
"全局代理": "Global proxy",
|
||||
"凭证级代理优先于全局代理,留空表示直连": "Credential-level proxy takes priority over global proxy, leave empty for direct connection",
|
||||
"主题": "Theme",
|
||||
"语言": "Language",
|
||||
"音效": "Sound effects",
|
||||
"工具调用和打字时播放提示音": "Play sound effects during tool calls and typing",
|
||||
"启动行为": "Startup behavior",
|
||||
"开机自启动": "Auto-start on boot",
|
||||
"关闭时最小化到托盘": "Minimize to tray when closing",
|
||||
"初次设置向导": "Initial setup wizard",
|
||||
"重新运行初次安装向导,重新选择用户群体和安装插件": "Re-run initial installation wizard to re-select user group and install plugins",
|
||||
"重新引导": "Re-run wizard",
|
||||
"// === components\\settings\\ProxySettings.tsx ===": "",
|
||||
"设置已保存": "Settings saved",
|
||||
"所有模型可用": "All models available",
|
||||
"部分模型可用": "Some models available",
|
||||
"API 不可用": "API unavailable",
|
||||
"代理服务配置": "Proxy service configuration",
|
||||
"监听地址": "Listen address",
|
||||
"用于验证 API 请求的密钥": "Key used to verify API requests",
|
||||
"Claude Code 兼容性检测": "Claude Code compatibility detection",
|
||||
"检测项目:": "Detection items:",
|
||||
"检测结果:": "Detection results:",
|
||||
"// === pages\\FlowMonitorPage.tsx ===": "",
|
||||
"监控和分析 LLM API 请求/响应流量": "Monitor and analyze LLM API request/response traffic",
|
||||
"列表": "List",
|
||||
"统计": "Statistics",
|
||||
"调整窗口大小": "Adjust window size",
|
||||
"窗口": "Window",
|
||||
"退出全屏": "Exit fullscreen",
|
||||
"全屏模式": "Fullscreen mode",
|
||||
"返回窗口模式": "Return to window mode",
|
||||
"使用整个屏幕": "Use entire screen",
|
||||
"导出": "Export",
|
||||
"清理": "Cleanup",
|
||||
"清理日志数据": "Clean log data",
|
||||
"// === components\\skills ===": "",
|
||||
"管理 Skill 仓库源": "Manage Skill repository sources",
|
||||
"在 GitHub 上查看": "View on GitHub",
|
||||
"什么是 Skills": "What are Skills",
|
||||
"// === components\\mcp ===": "",
|
||||
"暂无 MCP 服务器": "No MCP servers",
|
||||
"添加 MCP 服务器": "Add MCP server",
|
||||
"编辑 MCP 服务器": "Edit MCP server",
|
||||
"什么是 MCP": "What is MCP",
|
||||
"// === components\\provider-pool\\api-key ===": "",
|
||||
"无法删除系统预设 Provider": "Cannot delete system preset Provider",
|
||||
"删除 Provider": "Delete Provider",
|
||||
"该 Provider 包含": "This Provider contains",
|
||||
"导入/导出 Provider 配置": "Import/Export Provider configuration",
|
||||
"导出当前 Provider 配置或从文件导入配置": "Export current Provider configuration or import from file",
|
||||
"包含 API Key 元数据": "Include API Key metadata",
|
||||
"或粘贴配置 JSON": "Or paste configuration JSON",
|
||||
"导入 Provider": "Import Provider",
|
||||
"跳过(已存在)": "Skip (already exists)",
|
||||
"// === components\\clients ===": "",
|
||||
"支持 Claude": "Supports Claude",
|
||||
"支持 OpenAI": "Supports OpenAI",
|
||||
"当前:": "Current:",
|
||||
"// === Additional missing translations ===": "",
|
||||
"(已禁用)": "(Disabled)",
|
||||
"复制 URL": "Copy URL",
|
||||
"搜索模型...": "Search models...",
|
||||
"复制模型 ID": "Copy model ID",
|
||||
"(无输出)": "(No output)",
|
||||
"在画布中打开": "Open in canvas",
|
||||
"收起详情": "Collapse details",
|
||||
"实际:": "Actual:",
|
||||
"检测到当前配置,可一键导入": "Current configuration detected, can be imported with one click",
|
||||
"模型:": "Model:",
|
||||
"总耗时:": "Total duration:",
|
||||
"耗时:": "Duration:",
|
||||
"输入 URL...": "Enter URL...",
|
||||
"输入 URL 开始浏览": "Enter URL to start browsing",
|
||||
"// === Dynamic Translation Templates ===": "",
|
||||
"// These templates use {placeholder} syntax for dynamic values": "",
|
||||
"// Usage: See dynamic-translation.ts for examples": "",
|
||||
"TEMPLATE:new_flow_id": "New Flow ID: {id}",
|
||||
"TEMPLATE:total_duration": "Total duration: {duration}",
|
||||
"TEMPLATE:duration": "Duration: {duration}",
|
||||
"TEMPLATE:model_name": "Model: {model}",
|
||||
"TEMPLATE:replay_status": "Replay will create new Flow and mark as {status}",
|
||||
"TEMPLATE:flow_count": "Will replay {count} Flow(s)",
|
||||
"TEMPLATE:config_mismatch": "Actual effective configuration differs from currently selected '{provider}'",
|
||||
"TEMPLATE:actual_value": "Actual: {value}",
|
||||
"TEMPLATE:current_value": "Current: {value}",
|
||||
"TEMPLATE:items_count": "Total {count} {item}(s)",
|
||||
"TEMPLATE:loading_with_item": "Loading {item}...",
|
||||
"TEMPLATE:error_with_message": "{action} failed: {error}",
|
||||
"TEMPLATE:success_with_message": "{action} successful: {message}",
|
||||
"TEMPLATE:flow_detail_title": "Flow #{id}",
|
||||
"TEMPLATE:request_duration": "Request duration: {duration}",
|
||||
"TEMPLATE:response_duration": "Response duration: {duration}",
|
||||
"TEMPLATE:provider_model_count": "{provider} has {count} model(s)",
|
||||
"TEMPLATE:credential_type": "{type} credential",
|
||||
"TEMPLATE:setting_updated": "{setting} updated",
|
||||
"TEMPLATE:setting_failed": "Failed to update {setting}: {error}"
|
||||
}
|
||||
+110
-1
@@ -3581,5 +3581,114 @@
|
||||
"导出按钮": "导出按钮",
|
||||
"清理按钮": "清理按钮",
|
||||
"详情视图": "详情视图",
|
||||
"清理对话框": "清理对话框"
|
||||
"清理对话框": "清理对话框",
|
||||
"// === pages\\update-notification.tsx ===": "",
|
||||
"发现新版本": "发现新版本",
|
||||
"关闭 (ESC)": "关闭 (ESC)",
|
||||
"当前版本": "当前版本",
|
||||
"最新版本": "最新版本",
|
||||
"立即更新": "立即更新",
|
||||
"稍后提醒": "稍后提醒",
|
||||
"跳过此版本": "跳过此版本",
|
||||
"在浏览器中查看": "在浏览器中查看",
|
||||
"// === components\\settings\\GeneralSettings.tsx ===": "",
|
||||
"格式无效,请使用 http://、https:// 或 socks5:// 开头": "格式无效,请使用 http://、https:// 或 socks5:// 开头",
|
||||
"已保存": "已保存",
|
||||
"全局代理": "全局代理",
|
||||
"凭证级代理优先于全局代理,留空表示直连": "凭证级代理优先于全局代理,留空表示直连",
|
||||
"主题": "主题",
|
||||
"语言": "语言",
|
||||
"音效": "音效",
|
||||
"工具调用和打字时播放提示音": "工具调用和打字时播放提示音",
|
||||
"启动行为": "启动行为",
|
||||
"开机自启动": "开机自启动",
|
||||
"关闭时最小化到托盘": "关闭时最小化到托盘",
|
||||
"初次设置向导": "初次设置向导",
|
||||
"重新运行初次安装向导,重新选择用户群体和安装插件": "重新运行初次安装向导,重新选择用户群体和安装插件",
|
||||
"重新引导": "重新引导",
|
||||
"// === components\\settings\\ProxySettings.tsx ===": "",
|
||||
"设置已保存": "设置已保存",
|
||||
"所有模型可用": "所有模型可用",
|
||||
"部分模型可用": "部分模型可用",
|
||||
"API 不可用": "API 不可用",
|
||||
"代理服务配置": "代理服务配置",
|
||||
"监听地址": "监听地址",
|
||||
"用于验证 API 请求的密钥": "用于验证 API 请求的密钥",
|
||||
"Claude Code 兼容性检测": "Claude Code 兼容性检测",
|
||||
"检测项目:": "检测项目:",
|
||||
"检测结果:": "检测结果:",
|
||||
"// === pages\\FlowMonitorPage.tsx ===": "",
|
||||
"监控和分析 LLM API 请求/响应流量": "监控和分析 LLM API 请求/响应流量",
|
||||
"列表": "列表",
|
||||
"统计": "统计",
|
||||
"调整窗口大小": "调整窗口大小",
|
||||
"窗口": "窗口",
|
||||
"退出全屏": "退出全屏",
|
||||
"全屏模式": "全屏模式",
|
||||
"返回窗口模式": "返回窗口模式",
|
||||
"使用整个屏幕": "使用整个屏幕",
|
||||
"导出": "导出",
|
||||
"清理": "清理",
|
||||
"清理日志数据": "清理日志数据",
|
||||
"// === components\\skills ===": "",
|
||||
"管理 Skill 仓库源": "管理 Skill 仓库源",
|
||||
"在 GitHub 上查看": "在 GitHub 上查看",
|
||||
"什么是 Skills": "什么是 Skills",
|
||||
"// === components\\mcp ===": "",
|
||||
"暂无 MCP 服务器": "暂无 MCP 服务器",
|
||||
"添加 MCP 服务器": "添加 MCP 服务器",
|
||||
"编辑 MCP 服务器": "编辑 MCP 服务器",
|
||||
"什么是 MCP": "什么是 MCP",
|
||||
"// === components\\provider-pool\\api-key ===": "",
|
||||
"无法删除系统预设 Provider": "无法删除系统预设 Provider",
|
||||
"删除 Provider": "删除 Provider",
|
||||
"该 Provider 包含": "该 Provider 包含",
|
||||
"导入/导出 Provider 配置": "导入/导出 Provider 配置",
|
||||
"导出当前 Provider 配置或从文件导入配置": "导出当前 Provider 配置或从文件导入配置",
|
||||
"包含 API Key 元数据": "包含 API Key 元数据",
|
||||
"或粘贴配置 JSON": "或粘贴配置 JSON",
|
||||
"导入 Provider": "导入 Provider",
|
||||
"跳过(已存在)": "跳过(已存在)",
|
||||
"// === components\\clients ===": "",
|
||||
"支持 Claude": "支持 Claude",
|
||||
"支持 OpenAI": "支持 OpenAI",
|
||||
"当前:": "当前:",
|
||||
"// === Additional missing translations ===": "",
|
||||
"(已禁用)": "(已禁用)",
|
||||
"复制 URL": "复制 URL",
|
||||
"搜索模型...": "搜索模型...",
|
||||
"复制模型 ID": "复制模型 ID",
|
||||
"(无输出)": "(无输出)",
|
||||
"在画布中打开": "在画布中打开",
|
||||
"收起详情": "收起详情",
|
||||
"实际:": "实际:",
|
||||
"检测到当前配置,可一键导入": "检测到当前配置,可一键导入",
|
||||
"模型:": "模型:",
|
||||
"总耗时:": "总耗时:",
|
||||
"耗时:": "耗时:",
|
||||
"输入 URL...": "输入 URL...",
|
||||
"输入 URL 开始浏览": "输入 URL 开始浏览",
|
||||
"// === Dynamic Translation Templates ===": "",
|
||||
"// 这些模板使用 {placeholder} 语法表示动态值": "",
|
||||
"// 使用方法: 参见 dynamic-translation.ts 中的示例": "",
|
||||
"TEMPLATE:new_flow_id": "新 Flow ID: {id}",
|
||||
"TEMPLATE:total_duration": "总耗时: {duration}",
|
||||
"TEMPLATE:duration": "耗时: {duration}",
|
||||
"TEMPLATE:model_name": "模型: {model}",
|
||||
"TEMPLATE:replay_status": "重放会创建新的 Flow 并标记为 {status}",
|
||||
"TEMPLATE:flow_count": "将重放 {count} 个 Flow",
|
||||
"TEMPLATE:config_mismatch": "实际生效的配置与当前选中的 '{provider}' 不一致",
|
||||
"TEMPLATE:actual_value": "实际: {value}",
|
||||
"TEMPLATE:current_value": "当前: {value}",
|
||||
"TEMPLATE:items_count": "共 {count} 个{item}",
|
||||
"TEMPLATE:loading_with_item": "正在加载{item}...",
|
||||
"TEMPLATE:error_with_message": "{action}失败: {error}",
|
||||
"TEMPLATE:success_with_message": "{action}成功: {message}",
|
||||
"TEMPLATE:flow_detail_title": "Flow #{id}",
|
||||
"TEMPLATE:request_duration": "请求耗时: {duration}",
|
||||
"TEMPLATE:response_duration": "响应耗时: {duration}",
|
||||
"TEMPLATE:provider_model_count": "{provider} 有 {count} 个模型",
|
||||
"TEMPLATE:credential_type": "{type} 凭证",
|
||||
"TEMPLATE:setting_updated": "{setting} 已更新",
|
||||
"TEMPLATE:setting_failed": "更新 {setting} 失败: {error}"
|
||||
}
|
||||
Reference in New Issue
Block a user