diff --git a/README.en.md b/README.en.md index 2050afcc..816e8cc2 100644 --- a/README.en.md +++ b/README.en.md @@ -25,21 +25,26 @@ Turns [Windsurf](https://windsurf.com) (formerly Codeium)'s AI models into **two ## What is it doing? -``` - ┌─────────────┐ /v1/chat/completions ┌────────────┐ - │ OpenAI SDK │ ──────────────────────→ │ │ - │ curl / Frontend │ ←────────────────────── │ │ - └─────────────┘ OpenAI JSON + SSE │ WindsurfAPI│ - │ Node.js │ ┌──────────────┐ ┌─────────────────┐ - ┌─────────────┐ /v1/messages │ (This Service) │ gRPC │ Language │ HTTPS │ Windsurf Cloud │ - │ Claude Code │ ──────────────────────→ │ │ ───→ │ Server (LS) │ ────→ │ server.self- │ - │ Cline │ ←────────────────────── │ │ ←─── │ (Windsurf │ ←─── │ serve.windsurf │ - │ Cursor │ Anthropic SSE │ │ │ binary) │ │ .com │ - └─────────────┘ └────────────┘ └──────────────┘ └─────────────────┘ - ↑ - Account Pool Round-Robin - Rate Limit Isolation - Failover +```mermaid +flowchart LR + subgraph Clients + A[OpenAI SDK
curl / Frontend] + B[Claude Code
Cline
Cursor] + end + + subgraph WindsurfAPI["WindsurfAPI (Node.js)"] + C[HTTP Service
Port 3003] + D[Account Pool
Round-Robin
Rate Limit
Failover] + end + + E["Language Server
(Windsurf binary)"] + F[Windsurf Cloud
server.self-serve.windsurf.com] + + A -->|"/v1/chat/completions"
OpenAI JSON + SSE| C + B -->|"/v1/messages"
Anthropic SSE| C + C <-->|gRPC| E + E <-->|HTTPS| F + D -.-> C ``` **What it does**: @@ -52,29 +57,26 @@ Turns [Windsurf](https://windsurf.com) (formerly Codeium)'s AI models into **two The model itself does **not** operate on files — file operations are executed locally by the IDE Agent client (Claude Code, Cline, etc.): -``` - You "Help me fix a bug" Claude Code WindsurfAPI Windsurf Cloud - │ │ │ │ - │────────────────────────────→ │ │ │ - │ │ POST /v1/messages │ │ - │ │ messages + tools + system │ │ - │ │ ─────────────────────────────→│ Package into Cascade request │ - │ │ │ ──────────────────────→ │ - │ │ │ │ - │ │ │ Model thinks → returns - │ │ │ tool_use(edit_file) - │ │ │ ←────────────────────── │ - │ │ ←── Anthropic SSE ────────────│ │ - │ │ content_block=tool_use │ │ - │ │ │ │ - │ │ Execute edit_file() locally │ │ - │ │ (Read/write local files) │ │ - │ │ │ │ - │ │ Send another turn with tool_result │ │ - │ │ ─────────────────────────────→│ ──────────────────────→ │ - │ │ ... (loop) ... - │ │ │ │ - │ ← Final answer │ │ │ +```mermaid +sequenceDiagram + actor U as You + participant CC as Claude Code + participant WA as WindsurfAPI + participant WC as Windsurf Cloud + + U->>CC: "Help me fix a bug" + CC->>WA: POST /v1/messages
messages + tools + system + WA->>WC: Package into Cascade request + WC-->>WA: Model thinks → returns
tool_use(edit_file) + WA-->>CC: Anthropic SSE
content_block=tool_use + CC->>CC: Execute edit_file() locally
(Read/write local files) + CC->>WA: Send tool_result + WA->>WC: Continue conversation... + loop Conversation Loop + WC-->>WA: Response + WA-->>CC: SSE stream + end + CC-->>U: Final answer ``` **Key Point**: WindsurfAPI is only responsible for **passing** `tool_use` / `tool_result`. The client CLI is what actually modifies the files. @@ -165,6 +167,10 @@ LS_PORT=42100 DASHBOARD_PASSWORD= EOF +# Note: Inline comments are supported in .env for unquoted values: +# PORT=3003 # Service port +# Quoted values preserve everything inside the quotes. + node src/index.js ``` @@ -181,16 +187,16 @@ Open `http://YOUR_IP:3003/dashboard` → Login to get token → Click **Sign in Go to [windsurf.com/show-auth-token](https://windsurf.com/show-auth-token) to copy your token: ```bash -curl -X POST http://localhost:3003/auth/login - -H "Content-Type: application/json" +curl -X POST http://localhost:3003/auth/login + -H "Content-Type: application/json" -d '{"token": "YOUR_TOKEN"}' ``` **Method 3: Batch** ```bash -curl -X POST http://localhost:3003/auth/login - -H "Content-Type: application/json" +curl -X POST http://localhost:3003/auth/login + -H "Content-Type: application/json" -d '{"accounts": [{"token": "t1"}, {"token": "t2"}]}' ``` @@ -218,9 +224,9 @@ claude # Use Claude Code as usual ```bash # Raw curl test -curl http://localhost:3003/v1/messages - -H "Authorization: Bearer YOUR_KEY" - -H "anthropic-version: 2023-06-01" +curl http://localhost:3003/v1/messages + -H "Authorization: Bearer YOUR_KEY" + -H "anthropic-version: 2023-06-01" -d '{"model":"claude-opus-4.6","max_tokens":100,"messages":[{"role":"user","content":"Hello"}]}' ``` @@ -250,12 +256,21 @@ In your client's settings for **Custom OpenAI Compatible**: | `PORT` | `3003` | Service port | | `API_KEY` | empty | API key required for requests. Leave empty to disable validation. | | `DATA_DIR` | project root | Directory for persisted JSON state and `logs/`. Docker deployments should usually use `/data`. | +| `CODEIUM_API_KEY` | empty | Direct API key from Windsurf (alternative to token-based auth). | +| `CODEIUM_AUTH_TOKEN` | empty | Token from [windsurf.com/show-auth-token](https://windsurf.com/show-auth-token). | +| `CODEIUM_EMAIL` | empty | Email for Windsurf account authentication. | +| `CODEIUM_PASSWORD` | empty | Password for Windsurf account authentication. | +| `CODEIUM_API_URL` | `https://server.self-serve.windsurf.com` | Windsurf cloud API endpoint. | | `DEFAULT_MODEL` | `claude-4.5-sonnet-thinking` | The model to use if `model` is not specified. | | `MAX_TOKENS` | `8192` | Default maximum number of response tokens. | | `LOG_LEVEL` | `info` | debug / info / warn / error | | `LS_BINARY_PATH` | `/opt/windsurf/language_server_linux_x64` | Path to the LS binary. | | `LS_PORT` | `42100` | LS gRPC port. | +| `LS_DATA_DIR` | `/opt/windsurf` | Per-proxy LS data directory root. | | `DASHBOARD_PASSWORD` | empty | Dashboard password. Leave empty for no password. | +| `CASCADE_REUSE_STRICT` | `0` | Set to `1` for strict conversation reuse mode (waits for same fingerprint). | +| `CASCADE_REUSE_STRICT_RETRY_MS` | `60000` | Retry delay in ms for strict reuse mode. | +| `CASCADE_REUSE_HASH_SYSTEM` | `0` | Set to `1` to include system messages in conversation reuse hash. | ## Dashboard Features @@ -306,6 +321,10 @@ deepseek-v3 / v3-2 / r1 · grok-3 / mini / mini-thinking / code-fast-1 · qwen-3 > **Free accounts** can only use `gpt-4o-mini` and `gemini-2.5-flash`. Others require Windsurf Pro. +### Language-Following for CJK Users + +The service automatically detects Chinese, Japanese, or Korean characters in your messages and injects a language-following hint to ensure the model responds in the same language. This fixes the issue where Claude Code's large English system prompt would override the communication language. + ## Architecture Highlights - **Zero npm dependencies** Everything uses `node:*` built-ins · Protobuf is handcrafted (`src/proto.js`) · Download and run. diff --git a/docs/dashboard-i18n.md b/docs/dashboard-i18n.md new file mode 100644 index 00000000..fc4879be --- /dev/null +++ b/docs/dashboard-i18n.md @@ -0,0 +1,252 @@ +# Dashboard Internationalization (i18n) Guide + +This document describes the internationalization system used in the WindsurfAPI dashboard. + +## Overview + +The dashboard supports multiple languages through JSON locale bundles. Currently implemented: +- English (`en`) +- Simplified Chinese (`zh-CN`) + +## Architecture + +### Files + +| File | Purpose | +|------|---------| +| `src/dashboard/i18n/en.json` | English translations | +| `src/dashboard/i18n/zh-CN.json` | Chinese translations | +| `src/dashboard/index.html` | Main dashboard (contains I18n helper) | +| `src/dashboard/check-i18n.js` | Regression protection script | + +### I18n Helper + +The `I18n` object in `index.html` provides: + +```javascript +// Load locale bundles +await I18n.init('en'); // or 'zh-CN' + +// Translate a key +I18n.t('nav.overview'); // "Dashboard" +I18n.t('card.activeAccounts.title'); // "Active Accounts" + +// Translate with placeholders +I18n.t('card.activeAccounts.subtitle', { total: 5, error: 1 }); +// Result: "5 total · 1 abnormal" +``` + +## Adding New Translations + +### 1. Static HTML Content + +Add `data-i18n` attributes to translatable elements: + +```html + +

Active Accounts

+ + +

Active Accounts

+``` + +Update both locale files: + +```json +// en.json +{ + "section": { + "accounts": { + "title": "Active Accounts" + } + } +} + +// zh-CN.json +{ + "section": { + "accounts": { + "title": "活跃账号" + } + } +} +``` + +### 2. Dynamic JavaScript Content + +Use `I18n.t()` in JavaScript: + +```javascript +// Before +element.textContent = `成功 ${count} 个`; + +// After +element.textContent = I18n.t('toast.successCount', { count }); +``` + +Add to locale files: + +```json +// en.json +{ "toast": { "successCount": "Success: {{count}}" } } + +// zh-CN.json +{ "toast": { "successCount": "成功 {{count}} 个" } } +``` + +### 3. Backend Error Codes + +Backend should return error codes, not raw messages: + +```javascript +// Before +res.json({ error: '邮箱或密码错误' }); + +// After +res.json({ error: 'ERR_INVALID_CREDENTIALS' }); +``` + +Add the error code to locale files: + +```json +// en.json +{ "error": { "ERR_INVALID_CREDENTIALS": "Invalid email or password" } } + +// zh-CN.json +{ "error": { "ERR_INVALID_CREDENTIALS": "邮箱或密码错误" } } +``` + +## Key Naming Conventions + +### Structure +``` +
.. +``` + +Examples: +- `nav.overview` - Navigation items +- `page.accounts.title` - Page titles +- `card.activeAccounts.title` - Card titles +- `button.save` - Button labels +- `toast.saved` - Toast messages +- `error.ERR_*` - Error codes + +### Common Sections + +| Section | Usage | +|---------|-------| +| `brand` | Brand name and subtitle | +| `nav` | Navigation items | +| `page` | Page titles and subtitles | +| `pageSubtitle` | Page descriptions | +| `section` | Panel titles and descriptions | +| `card` | Metric card titles | +| `button` | Button labels | +| `action` | Action labels | +| `field` | Form field labels and placeholders | +| `table` | Table headers | +| `toast` | Toast notification messages | +| `help` | Help text | +| `status` | Status messages | +| `confirm` | Confirmation dialog titles/descriptions | +| `modal` | Modal dialog content | +| `log` | Log level names | +| `batch` | Batch import messages | +| `error` | Error messages and codes | +| `footer` | Footer text | +| `experimental` | Experimental feature labels | + +## Placeholder Syntax + +Use `{{variableName}}` for dynamic values: + +```json +{ + "card": { + "activeAccounts": { + "subtitle": "{{total}} total · {{error}} abnormal" + } + } +} +``` + +Usage: +```javascript +I18n.t('card.activeAccounts.subtitle', { total: 5, error: 1 }); +// "5 total · 1 abnormal" +``` + +## Regression Protection + +Run the check script before committing: + +```bash +cd src/dashboard +node check-i18n.js +``` + +This checks for: +1. Hardcoded Chinese text in HTML/JS +2. Missing translation keys +3. Keys present in one locale but not the other +4. `data-i18n` attributes referencing non-existent keys + +### CI Integration + +Add to your CI pipeline: + +```yaml +# .github/workflows/i18n-check.yml +name: I18n Check +on: [push, pull_request] +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - run: node src/dashboard/check-i18n.js +``` + +## Language Toggle + +The dashboard persists language preference in `localStorage`: + +```javascript +// Toggle language +I18n.toggle(); + +// Get current locale +const current = I18n.currentLocale; // 'en' or 'zh-CN' +``` + +## Best Practices + +1. **Always add both locales**: When adding a new key, add it to both `en.json` and `zh-CN.json` + +2. **Use descriptive keys**: Prefer `card.activeAccounts.title` over `card.title1` + +3. **Keep placeholders semantic**: Use `{{email}}` not `{{e}}` + +4. **Error codes**: Use `ERR_*` prefix for backend error codes + +5. **Test both languages**: Verify your changes in both EN and CN modes + +6. **Run regression check**: Use `check-i18n.js` before committing + +## Troubleshooting + +### Key not translating +- Check that the key exists in both locale files +- Verify the key path is correct (e.g., `section.accounts.title` vs `accounts.title`) +- Check browser console for I18n errors + +### Locale not loading +- Verify the locale file is valid JSON +- Check browser Network tab for 404 errors on `/dashboard/i18n/*.json` +- Ensure `I18n.init()` completed before using `I18n.t()` + +### Mixed languages showing +- Some content may be cached; hard refresh (Ctrl+F5) the page +- Check that all dynamic content uses `I18n.t()` +- Verify no hardcoded strings remain in JavaScript diff --git a/src/dashboard/api.js b/src/dashboard/api.js index fe9994d7..928f38e2 100644 --- a/src/dashboard/api.js +++ b/src/dashboard/api.js @@ -52,8 +52,9 @@ function checkAuth(req) { async function processWindsurfLogin({ email, password, loginProxy, autoAdd }) { if (!email || !password) { - const err = new Error('email 和 password 为必填'); + const err = new Error('ERR_EMAIL_PASSWORD_REQUIRED'); err.statusCode = 400; + err.code = 'ERR_EMAIL_PASSWORD_REQUIRED'; throw err; } @@ -175,7 +176,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) { // ─── Proxy test — try an HTTP CONNECT through the given proxy ── if (subpath === '/test-proxy' && method === 'POST') { const { host, port, username, password, type = 'http' } = body || {}; - if (!host || !port) return json(res, 400, { ok: false, error: '缺少 host 或 port' }); + if (!host || !port) return json(res, 400, { ok: false, error: 'ERR_HOST_PORT_REQUIRED' }); const startTime = Date.now(); try { const result = await testProxy({ host, port: Number(port), username, password, type }); @@ -210,7 +211,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) { return json(res, 200, { ok: false, dirty: true, - error: '工作区有未提交的修改(SFTP 部署或手动改过代码)。确定要覆盖本地修改用远程最新版本吗?', + error: 'ERR_UNCOMMITTED_CHANGES', dirtyFiles: dirty.split('\n').slice(0, 20), }); } @@ -523,7 +524,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) { try { const { accounts, proxy: loginProxy, autoAdd } = body || {}; if (!Array.isArray(accounts) || !accounts.length) { - return json(res, 400, { error: 'accounts 为必填数组' }); + return json(res, 400, { error: 'ERR_ACCOUNTS_REQUIRED' }); } const results = []; @@ -563,9 +564,9 @@ export async function handleDashboardApi(method, subpath, body, req, res) { if (subpath === '/batch-import' && method === 'POST') { try { const { text, autoAdd = true } = body || {}; - if (!text || typeof text !== 'string') return json(res, 400, { error: '缺少 text 字段' }); + if (!text || typeof text !== 'string') return json(res, 400, { error: 'ERR_TEXT_REQUIRED' }); const lines = text.split('\n').map(l => l.trim()).filter(Boolean); - if (!lines.length) return json(res, 400, { error: '没有有效行' }); + if (!lines.length) return json(res, 400, { error: 'ERR_NO_VALID_LINES' }); const results = []; for (const line of lines) { @@ -579,7 +580,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) { email = parts[0]; password = parts[1]; } else { - results.push({ success: false, email: line.slice(0, 30), error: '格式错误 需要 [proxy] email password' }); + results.push({ success: false, email: line.slice(0, 30), error: 'ERR_FORMAT_INVALID' }); continue; } try { @@ -616,7 +617,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) { if (subpath === '/oauth-login' && method === 'POST') { try { const { idToken, refreshToken, email, provider, autoAdd } = body; - if (!idToken) return json(res, 400, { error: '缺少 idToken' }); + if (!idToken) return json(res, 400, { error: 'ERR_IDTOKEN_REQUIRED' }); const proxy = getProxyConfig().global; const { apiKey, name } = await reRegisterWithCodeium(idToken, proxy); @@ -731,7 +732,7 @@ async function gitStatus() { const PRIVATE_PROXY_HOST = /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.|localhost$|0\.0\.0\.0$)/i; async function testProxy({ host, port, username, password, type }) { - if (PRIVATE_PROXY_HOST.test(host)) throw new Error('代理地址不能指向内网/本机'); + if (PRIVATE_PROXY_HOST.test(host)) throw new Error('ERR_PROXY_PRIVATE_IP'); const { isSocks, createSocksTunnel } = await import('../socks.js'); const tls = await import('node:tls'); const targetHost = 'api.ipify.org'; @@ -755,11 +756,11 @@ async function testProxy({ host, port, username, password, type }) { timeout: 10000, }); req.on('connect', (res, sock) => { - if (res.statusCode !== 200) { sock.destroy(); return reject(new Error(`代理返回 HTTP ${res.statusCode}`)); } + if (res.statusCode !== 200) { sock.destroy(); return reject(new Error(`ERR_PROXY_HTTP_ERROR:${res.statusCode}`)); } resolve(sock); }); - req.on('error', (err) => reject(new Error(`连接失败: ${err.message}`))); - req.on('timeout', () => { req.destroy(); reject(new Error('超时(10s)')); }); + req.on('error', (err) => reject(new Error(`ERR_CONNECTION_FAILED:${err.message}`))); + req.on('timeout', () => { req.destroy(); reject(new Error('ERR_TIMEOUT')); }); req.end(); }); } @@ -777,10 +778,10 @@ async function testProxy({ host, port, username, password, type }) { const ip = match ? match[1].trim() : ''; tlsSock.destroy(); if (!ip || !/^\d+\.\d+\.\d+\.\d+$/.test(ip)) { - return reject(new Error('TLS 隧道建立但返回内容异常')); + return reject(new Error('ERR_TLS_TUNNEL_ERROR')); } resolve({ egressIp: ip, type }); }); - tlsSock.on('error', (err) => reject(new Error(`TLS 失败: ${err.message}`))); + tlsSock.on('error', (err) => reject(new Error(`ERR_TLS_FAILED:${err.message}`))); }); } diff --git a/src/dashboard/check-i18n.js b/src/dashboard/check-i18n.js new file mode 100644 index 00000000..09c05d4c --- /dev/null +++ b/src/dashboard/check-i18n.js @@ -0,0 +1,262 @@ +#!/usr/bin/env node +/** + * I18n Regression Protection Script + * + * This script checks for: + * 1. Hardcoded Chinese text in dashboard HTML/JS + * 2. Missing translation keys in locale files + * 3. Keys present in one locale but not the other + * + * Usage: node check-i18n.js + * Exit code: 0 if all checks pass, 1 if violations found + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const YELLOW = '\x1b[33m'; +const RESET = '\x1b[0m'; + +let exitCode = 0; +let violations = 0; + +function logError(msg) { + console.log(`${RED}✗${RESET} ${msg}`); + violations++; + exitCode = 1; +} + +function logWarn(msg) { + console.log(`${YELLOW}⚠${RESET} ${msg}`); +} + +function logOk(msg) { + console.log(`${GREEN}✓${RESET} ${msg}`); +} + +// Chinese character regex +const CHINESE_REGEX = /[\u4e00-\u9fff]/; + +// Patterns that are allowed to contain Chinese (whitelisted) +const WHITELIST_PATTERNS = [ + /data-i18n=/, // i18n attributes are ok + /\/\/.*/, // comments + /https?:\/\//, // URLs + /windsurf\.com/, // windsurf domain + /firebase/, // Firebase references +]; + +function isWhitelisted(line) { + return WHITELIST_PATTERNS.some(p => p.test(line)); +} + +function checkFileForChinese(filePath, content) { + const lines = content.split('\n'); + let found = false; + + lines.forEach((line, idx) => { + if (CHINESE_REGEX.test(line) && !isWhitelisted(line)) { + logError(`${filePath}:${idx + 1}: ${line.trim().slice(0, 80)}`); + found = true; + } + }); + + return found; +} + +function extractKeys(obj, prefix = '') { + const keys = []; + for (const [k, v] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${k}` : k; + if (typeof v === 'object' && v !== null && !Array.isArray(v)) { + keys.push(...extractKeys(v, fullKey)); + } else { + keys.push(fullKey); + } + } + return keys; +} + +function compareLocales(enKeys, zhKeys) { + const enSet = new Set(enKeys); + const zhSet = new Set(zhKeys); + + const onlyInEn = enKeys.filter(k => !zhSet.has(k)); + const onlyInZh = zhKeys.filter(k => !enSet.has(k)); + + return { onlyInEn, onlyInZh }; +} + +// Main checks +console.log('\n📋 I18n Regression Check\n'); + +// 1. Check HTML file for hardcoded Chinese +const htmlPath = path.join(__dirname, 'index.html'); +const htmlContent = fs.readFileSync(htmlPath, 'utf-8'); +console.log('Checking index.html for hardcoded Chinese...'); +const htmlHasChinese = checkFileForChinese('index.html', htmlContent); +if (!htmlHasChinese) { + logOk('No hardcoded Chinese found in HTML'); +} + +// 2. Check API files for Chinese error messages +const apiPath = path.join(__dirname, '../dashboard/api.js'); +if (fs.existsSync(apiPath)) { + const apiContent = fs.readFileSync(apiPath, 'utf-8'); + console.log('\nChecking api.js for Chinese error messages...'); + + // Look for Chinese in error messages (not in comments) + const lines = apiContent.split('\n'); + let foundApiChinese = false; + lines.forEach((line, idx) => { + // Skip comments + if (/^\s*\/\//.test(line)) return; + // Check for Chinese in error: or throw new Error( + if (/error.*:.*'[^']*[一-鿿]/.test(line) || /throw new Error\('[^']*[一-鿿]/.test(line)) { + if (!isWhitelisted(line)) { + logError(`api.js:${idx + 1}: ${line.trim().slice(0, 80)}`); + foundApiChinese = true; + } + } + }); + + if (!foundApiChinese) { + logOk('No Chinese error messages found in API'); + } +} + +// 3. Compare locale files +console.log('\nComparing locale files...'); +const enPath = path.join(__dirname, 'i18n/en.json'); +const zhPath = path.join(__dirname, 'i18n/zh-CN.json'); + +const enJson = JSON.parse(fs.readFileSync(enPath, 'utf-8')); +const zhJson = JSON.parse(fs.readFileSync(zhPath, 'utf-8')); + +const enKeys = extractKeys(enJson).sort(); +const zhKeys = extractKeys(zhJson).sort(); + +const { onlyInEn, onlyInZh } = compareLocales(enKeys, zhKeys); + +if (onlyInEn.length > 0) { + logError(`Keys in en.json but missing in zh-CN.json (${onlyInEn.length}):`); + onlyInEn.forEach(k => console.log(` - ${k}`)); +} + +if (onlyInZh.length > 0) { + logError(`Keys in zh-CN.json but missing in en.json (${onlyInZh.length}):`); + onlyInZh.forEach(k => console.log(` - ${k}`)); +} + +if (onlyInEn.length === 0 && onlyInZh.length === 0) { + logOk('Locale files are synchronized'); +} + +// 4. Check for data-i18n usage consistency +console.log('\nChecking data-i18n attribute usage...'); +const i18nRegex = /data-i18n="([^"]+)"/g; +const usedKeys = []; +let match; +while ((match = i18nRegex.exec(htmlContent)) !== null) { + usedKeys.push(match[1]); +} + +// Check if all used keys exist in locales +const missingInLocales = []; +for (const key of usedKeys) { + // Navigate nested key path + const parts = key.split('.'); + let enVal = enJson; + let zhVal = zhJson; + let exists = true; + + for (const part of parts) { + enVal = enVal?.[part]; + zhVal = zhVal?.[part]; + if (enVal === undefined || zhVal === undefined) { + exists = false; + break; + } + } + + if (!exists) { + missingInLocales.push(key); + } +} + +if (missingInLocales.length > 0) { + logError(`data-i18n keys missing in locales (${missingInLocales.length}):`); + missingInLocales.forEach(k => console.log(` - ${k}`)); +} else { + logOk('All data-i18n keys exist in locales'); +} + +// 5. Check for I18n.t() calls in JavaScript code +console.log('\nChecking I18n.t() calls in JavaScript code...'); +const i18nCallRegex = /I18n\.t\(['"`]([^'"`]+)['"`]/g; +const jsKeys = []; +while ((match = i18nCallRegex.exec(htmlContent)) !== null) { + jsKeys.push(match[1]); +} + +// Also check for I18n.t() with variables (e.g., I18n.t(key)) +const i18nVarRegex = /I18n\.t\(\s*([^)]+)\s*\)/g; +while ((match = i18nVarRegex.exec(htmlContent)) !== null) { + const keyExpr = match[1].trim(); + // Skip if it's a variable expression (not a string literal) + if (!/^[`'"']/.test(keyExpr) && !/^\${/.test(keyExpr) && keyExpr !== 'key') { + jsKeys.push(keyExpr); + } +} + +// Deduplicate +const uniqueJsKeys = [...new Set(jsKeys)]; + +// Check if all I18n.t() keys exist in locales +const missingJsKeys = []; +for (const key of uniqueJsKeys) { + // Skip template expressions and variables + if (key.includes('${') || key === 'key' || key.includes('?') || key.includes('vars')) continue; + + // Navigate nested key path + const parts = key.split('.'); + let enVal = enJson; + let zhVal = zhJson; + let exists = true; + + for (const part of parts) { + enVal = enVal?.[part]; + zhVal = zhVal?.[part]; + if (enVal === undefined || zhVal === undefined) { + exists = false; + break; + } + } + + if (!exists) { + missingJsKeys.push(key); + } +} + +if (missingJsKeys.length > 0) { + logError(`I18n.t() keys missing in locales (${missingJsKeys.length}):`); + missingJsKeys.forEach(k => console.log(` - ${k}`)); +} else { + logOk('All I18n.t() keys exist in locales'); +} + +// Summary +console.log(`\n${'='.repeat(50)}`); +if (violations === 0) { + console.log(`${GREEN}✓ All i18n checks passed!${RESET}`); +} else { + console.log(`${RED}✗ Found ${violations} violation(s)${RESET}`); +} +console.log(`${'='.repeat(50)}\n`); + +process.exit(exitCode); diff --git a/src/dashboard/i18n/en.json b/src/dashboard/i18n/en.json new file mode 100644 index 00000000..2f6a766a --- /dev/null +++ b/src/dashboard/i18n/en.json @@ -0,0 +1,841 @@ +{ + "_meta": { + "name": "English", + "code": "en" + }, + + "brand": { + "sub": "Control Panel" + }, + + "nav": { + "group": { + "overview": "Overview", + "account": "Accounts", + "system": "System", + "about": "About" + }, + "overview": "Dashboard", + "stats": "Statistics", + "windsurf-login": "Account Login", + "accounts": "Account Pool", + "bans": "Health Check", + "models": "Model Control", + "proxy": "Proxy", + "logs": "Logs", + "experimental": "Experimental", + "credits": "Credits" + }, + + "page": { + "overview": "Dashboard", + "stats": "Statistics", + "windsurf-login": "Account Login", + "accounts": "Account Pool", + "bans": "Health Check", + "models": "Model Control", + "proxy": "Proxy Config", + "logs": "Runtime Logs", + "experimental": "Experimental Features", + "credits": "Credits" + }, + + "pageTitle": { + "main": "WindsurfAPI bydwgx1337 Control Panel" + }, + + "pageSubtitle": { + "overview": "System status and key metrics overview", + "windsurf-login": "Login to Windsurf via Google / GitHub / Email to get API Key", + "accounts": "Manage account pool, probe subscription tiers and available models", + "models": "Configure model access policies, limit available model scope", + "proxy": "Configure global or per-account exit proxies; each dedicated proxy spawns an independent Language Server instance", + "logs": "Receive server logs in real-time via SSE streaming", + "stats": "Request volume, success rate, latency percentiles and account/model dimension stats", + "bans": "Track error accounts and abnormal status", + "experimental": "Unstable optimizations; disable anytime if issues occur", + "credits": "Thanks to contributors who submitted PRs / audited code / reported issues" + }, + + "button": { + "cancel": "Cancel", + "confirm": "Confirm", + "forceOverride": "Force Overwrite & Update", + "save": "Save", + "restart": "Restart", + "updateAndRestart": "Update and Restart" + }, + + "action": { + "checkUpdate": "Check Update", + "applyUpdate": "Update & Restart", + "restart": "Restart", + "reset": "Reset", + "save": "Save", + "clearPool": "Clear Pool", + "login": "Login", + "clear": "Clear", + "cancel": "Cancel", + "confirm": "Confirm", + "delete": "Delete", + "refresh": "Refresh", + "probe": "Probe", + "probeTitle": "Probe capabilities", + "enable": "Enable", + "disable": "Disable", + "copy": "Copy", + "copyTitle": "Click to copy", + "configure": "Configure", + "test": "Test", + "readClipboard": "Read Clipboard", + "batchImport": "Batch Import & Login", + "paste": "Paste", + "add": "Add", + "close": "Close", + "edit": "Edit", + "selectAll": "Select All", + "selectNone": "Select None", + "invert": "Invert", + "showPassword": "Show/Hide Password" + }, + + "status": { + "active": "Active", + "disabled": "Disabled", + "error": "Error", + "running": "Running", + "stopped": "Stopped", + "success": "Success", + "failed": "Failed", + "pending": "Pending", + "unknown": "Unknown", + "rateLimited": "Rate Limited", + "loading": "Loading...", + "saving": "Saving...", + "testing": "Testing...", + "updateAndRestartHint": "Update and restart the service", + "proxySuccess": "✓ Connected. Egress IP {{ip}} · {{latency}}ms", + "proxyFailed": "✗ Failed: {{error}} · {{latency}}ms", + "checkingUpdate": "Checking for updates...", + "updateAvailable": "Update available", + "currentVersion": "Current:", + "remoteVersion": "Remote:", + "upToDate": "✓ Up to date ({{commit}}) · {{message}}", + "pullingRestarting": "Pulling + restarting...", + "cancelled": "Cancelled", + "noUpdateNeeded": "Already up to date, no update needed", + "updateComplete": "Update complete {{before}} → {{after}}", + "restarting": "Service restarting in background, page will auto-refresh in ~8 seconds...", + "notGitRepo": "Not a git repository", + "notGitRepoHint": "Ensure .git directory is present for one-click updates", + "workingDirDirty": "Working directory has local changes", + "workingDirDirtyHint": "The following files have local changes:
{{files}}
Continue will overwrite with remote version.", + "notFetched": "Not fetched", + "fetchFailed": "Fetch failed", + "clickToEditTier": "Click to manually edit tier (use when probe fails)" + }, + + "tier": { + "pro": "PRO", + "free": "FREE", + "expired": "Expired", + "unknown": "Unknown", + "expiredLabel": "Expired" + }, + + "creditsBar": { + "daily": "D", + "weekly": "W", + "monthly": "M" + }, + + "section": { + "banStatus": { + "title": "Health Status" + }, + "lsStatus": { + "title": "Language Server", + "desc": "Language Server instance status" + }, + "oauthLogin": { + "title": "Quick Login (Recommended)", + "desc": "Login to Windsurf directly with your Google or GitHub account — no password needed" + }, + "emailLogin": { + "title": "Email & Password Login", + "desc": "For accounts registered with email+password only. Third-party login please use buttons above. Supports single login or batch import in \"[proxy] email password\" format." + }, + "loginProxy": { + "title": "Login Proxy (Optional)", + "desc": "Specify a proxy for this login session; leave empty to use global proxy. When proxy takes effect, subsequent chat requests will also egress through this proxy." + }, + "loginHistory": { + "title": "Login History", + "desc": "Local record of last 50 login operations" + }, + "addAccount": { + "title": "Add Account", + "desc": "Supports both API Key and Auth Token methods", + "hint": "Recommended: Use Token method. Visit {{link}} after logging in, copy the Token and paste below. Works with all login methods (Email/Google/GitHub)." + }, + "accountList": { + "title": "Account List", + "desc": "Click probe button to update capability info for a specific account" + }, + "modelPolicy": { + "title": "Access Policy", + "desc": "Choose \"Allow All\" for no restrictions; \"Allowlist\" only permits listed models; \"Blocklist\" blocks listed models" + }, + "modelList": { + "title": "Model List", + "titleAllow": "Allowed Models", + "titleBlock": "Blocked Models", + "descAllow": "Only selected models are allowed; unselected models return 403 error", + "descBlock": "Selected models will be blocked; unselected models work normally", + "search": "Search models...", + "provider": "Provider" + }, + "globalProxy": { + "title": "Global Proxy", + "desc": "Accounts without dedicated proxy will use this configuration" + }, + "accountProxy": { + "title": "Per-Account Proxy", + "desc": "Set dedicated proxy for specific accounts; each dedicated proxy spawns an independent LS instance" + }, + "identityInjection": { + "title": "Model Identity Injection", + "desc": "When enabled, injects a system prompt at the start of each request, overriding Cascade's built-in Windsurf identity; each provider's template below can be customized and takes effect immediately after saving." + }, + "cascadeReuse": { + "title": "Cascade Conversation Reuse", + "desc": "Reuse the same cascade_id across multi-turn conversations, only sending the latest user message to Windsurf. Let the server maintain context cache. Significantly reduces TTFB and upload volume on cache hits; automatically falls back to new session on cache miss or account/LS change. Requires client to retain full history and append in order (e.g., new-api, OpenWebUI).", + "enable": "Enable Cascade Conversation Reuse", + "hint": "Disabled by default. When enabled, takes effect immediately on current conversation pool." + }, + "identityPrompt": { + "title": "Model Identity Prompt", + "desc": "Inject model identity system prompt at the start of each request.", + "enable": "Enable Model Identity Injection", + "hint": "Enabled by default; templates below won't work when disabled.", + "templateHint": "Each provider's template is editable. {{model}} placeholder will be replaced with the requested model name." + }, + "systemPrompts": { + "title": "System Prompts", + "desc": "Tool injection, conversation mode and other built-in prompt templates. Changes take effect immediately; click \"Reset\" to restore defaults." + }, + "lsPool": { + "title": "Language Server Pool", + "desc": "{{count}} LS instances · one process per dedicated proxy", + "sectionTitle": "Language Server Pool", + "sectionDesc": "Dedicated Language Server instances per proxy", + "port": "Port", + "defaultInstance": "Default Instance", + "noProxy": "No proxy" + }, + "modelStats": { + "title": "Model Usage Statistics", + "desc": "Aggregated by model: requests, success count, error count, success rate, avg duration and p50/p95 latency percentiles" + }, + "accountStats": { + "title": "Account Dimension Statistics", + "desc": "Request volume and success rate per account ID (first 8 chars)" + }, + "accountHealth": { + "title": "Account Health Status" + }, + "requestChart": { + "title": "Request Volume Time Series" + } + }, + + "help": { + "token": "Recommended: Use Token method. Visit {{link}} after logging in, copy the Token and paste below. Works with all login methods (Email/Google/GitHub).", + "tokenGuide": "Recommended: Use Token method. Visit ", + "tokenGuideEnd": " after logging in, copy the Token and paste below. Works with all login methods (Email/Google/GitHub)." + }, + + "field": { + "email": { + "label": "Email", + "placeholder": "your-email@example.com" + }, + "password": { + "label": "Password", + "placeholder": "••••••••" + }, + "type": { + "label": "Type" + }, + "tier": { + "label": "Tier" + }, + "provider": { + "label": "Provider", + "all": "All Providers" + }, + "searchModel": { + "label": "Search Model", + "placeholder": "Enter model name to filter..." + }, + "proxyUser": { + "placeholder": "Proxy auth username" + }, + "proxyPass": { + "placeholder": "Proxy auth password" + }, + "searchProvider": { + "placeholder": "Search providers..." + }, + "host": { + "label": "Host", + "placeholder": "proxy.example.com" + }, + "port": { + "label": "Port", + "placeholder": "8080" + }, + "username": { + "label": "Username", + "placeholder": "Optional" + }, + "passwordProxy": { + "label": "Password", + "placeholder": "Optional" + }, + "key": { + "label": "Key / Token", + "placeholder": "Paste Auth Token (from windsurf.com/show-auth-token)" + }, + "label": { + "label": "Label", + "placeholder": "Optional" + }, + "batchInput": { + "label": "Batch Import", + "hint": "One account per line. Format: [proxy] email password. Proxy is optional, supports http/socks5. Proxy will be automatically bound to corresponding account." + }, + "proxyType": { + "label": "Type" + }, + "proxyHost": { + "label": "Host", + "placeholder": "Leave empty = use global" + }, + "providerFilter": { + "label": "Provider" + }, + "logLevel": { + "label": "Log Level" + }, + "identitySearch": { + "placeholder": "Search providers..." + }, + "search": { + "label": "Search", + "placeholder": "Search..." + }, + "autoScroll": "Auto-scroll", + "autoAdd": "Auto-add to pool after login" + }, + + "table": { + "header": { + "time": "Time", + "email": "Email", + "status": "Status", + "proxy": "Proxy", + "action": "Action", + "id": "ID", + "label": "Label", + "tier": "Tier", + "rpm": "RPM", + "credits": "Credits", + "models": "Available Models", + "error": "Error", + "lastUsed": "Last Used", + "key": "Key", + "accountId": "Account ID", + "account": "Account", + "requests": "Requests", + "success": "Success", + "errors": "Errors", + "successRate": "Success Rate", + "avg": "Avg", + "p50": "p50", + "p95": "p95", + "errorCount": "Error Count", + "model": "Model", + "note": "Note" + }, + "empty": { + "loginHistory": "No login records", + "accounts": "No accounts yet, please add one first", + "accountsProxy": "No accounts", + "modelStats": "No model statistics available", + "accountStats": "No account statistics available", + "banList": "No abnormal accounts", + "models": "No matching models", + "credits": "Loading...", + "accountNotFound": "Account not found", + "noValidAccounts": "No valid accounts available for routing", + "probeFailed": "Probe failed", + "noRequestData": "No request data", + "noAccountRequestData": "No account-level request data", + "noData": "No data" + } + }, + + "card": { + "activeAccounts": { + "title": "Active Accounts", + "subtitle": "{{total}} total · {{error}} abnormal" + }, + "totalRequests": { + "title": "Total Requests", + "subtitle": "{{rate}}% success rate" + }, + "uptime": { + "title": "Uptime", + "subtitle": "Started at {{time}}" + }, + "languageServer": { + "title": "Language Server", + "running": "Running", + "stopped": "Stopped", + "subtitle": "{{count}} instances · Port {{port}}" + }, + "responseCache": { + "title": "Response Cache", + "subtitle": "{{hits}} hit / {{misses}} miss · {{size}}/{{max}} entries" + }, + "banStats": { + "title": "Health Overview" + }, + "abnormalAccounts": { + "title": "Abnormal Accounts", + "subtitle": "Total {{count}} accounts" + }, + "disabled": { + "title": "Disabled", + "subtitle": "Auto-disabled due to errors" + }, + "rateLimit": { + "title": "Rate Limited", + "badge": "Rate Limited", + "subtitle": "Temporarily excluded from scheduling" + }, + "stats": { + "title": "Total Requests", + "subtitle": "Running {{time}}" + }, + "success": { + "title": "Success", + "subtitle": "{{rate}}% success rate" + }, + "errors": { + "title": "Errors", + "subtitle": "{{rate}}% failure rate" + }, + "p95Latency": { + "title": "Avg p95 Latency", + "subtitle": "{{count}} models" + }, + "hitRate": { + "title": "Hit Rate", + "subtitle": "{{hits}} hit / {{misses}} miss" + }, + "poolSize": { + "title": "Pool Size", + "subtitle": "Max {{max}} · TTL {{ttl}} min" + }, + "stores": { + "title": "Stores", + "subtitle": "Expired {{expired}} · Evicted {{evicted}}" + }, + "defaultInstance": "Default Instance", + "noProxy": "No proxy", + "restarts": "restarts", + "custom": "Customized" + }, + + "login": { + "title": "Control Panel Login", + "subtitle": "Please enter the admin password to continue", + "passwordPlaceholder": "Dashboard Password", + "button": "Login" + }, + + "range": { + "6h": "Last 6h", + "24h": "Last 24h", + "72h": "Last 72h" + }, + + "credits": { + "contributors": "Core Contributors", + "cta": { + "title": "Want to join this list?", + "desc": "Open an issue or pull request on GitHub — bug fixes, new models, UI improvements, or security findings all get recognised here.", + "issue": "Open Issue", + "pr": "Open PR" + } + }, + + "experimental": { + "enable": "Enable", + "disable": "Disable", + "cascadeReuse": { + "title": "Enable Cascade Conversation Reuse", + "desc": "Disabled by default. When enabled, takes effect immediately on current conversation pool.", + "enable": "Enable Cascade Conversation Reuse", + "hint": "Disabled by default. When enabled, takes effect immediately on current conversation pool." + }, + "identityPrompt": { + "title": "Enable Model Identity Injection", + "desc": "Enabled by default; templates below won't work when disabled." + }, + "templateHint": "Each provider's template is editable. {{model}} placeholder will be replaced with the requested model name (e.g., claude-opus-4.6).", + "clearPool": "Clear Conversation Pool", + "refresh": "Refresh", + "noMatch": "No matches", + "restoreDefault": "Restore Default", + "save": "Save", + "default": "Default", + "toolReinforcement": "Tool Call Format Guide", + "communicationWithTools": "Communication with Tools", + "communicationNoTools": "Communication without Tools", + "promptEmpty": "Template cannot be empty", + "missingPlaceholder": "Missing {model} placeholder", + "missingPlaceholderDesc": "After saving, the provider's model name won't be replaced in the prompt. Continue?", + "restored": "restored" + }, + + "confirm": { + "restartTitle": "Restart Language Server", + "restartDesc": "In-progress requests will fail. Continue?", + "restoreDefaultDesc": "restore to default?", + "clearPoolDesc": "All active Cascade sessions will be cleared. Continue?" + }, + + "modal": { + "blockedModels": { + "title": "Edit Available Models — {{email}}", + "hint": "Checked = available; Uncheck = disabled for this account", + "enabled": "Enabled", + "saved": "Saved: enabled {{enabled}} / disabled {{disabled}}", + "saving": "Saving...", + "total": "Total" + }, + "restartLS": { + "title": "Restart Language Server", + "desc": "In-progress requests will fail. Continue?" + }, + "deleteAccount": { + "title": "Delete Account", + "desc": "This action cannot be undone. Delete this account?" + }, + "resetStats": { + "title": "Reset Statistics", + "desc": "Clear all statistics data?" + }, + "overrideTier": { + "title": "Manually Set Account Tier", + "desc": "Current: {{current}}. Windsurf Pro trial sometimes fails auto-detection (issue #8 side effect), manually change to pro to unlock all Pro models.", + "options": { + "pro": "Pro (unlock all models)", + "free": "Free (gpt-4o-mini and gemini-2.5-flash only)", + "unknown": "Unknown (let system re-evaluate)" + } + }, + "applyUpdate": { + "title": "Update & Restart", + "desc": "Will execute git pull and restart PM2. Dashboard will be briefly unavailable during restart (about 5-10 seconds). Continue?" + }, + "dirtyWorktree": { + "title": "Local Changes in Working Directory", + "desc": "The following files have been modified but not committed:

{{files}}
Continuing will overwrite local changes with remote version. For non-git deployments (SFTP / zip), click force overwrite.", + "forceUpdate": "Force Overwrite & Update" + }, + "configureProxy": { + "title": "Configure Account Proxy", + "desc": "Set dedicated proxy for account {{label}}" + } + }, + + "oauth": { + "tryAnotherWay": "Try Another Way", + "google": "Google Login", + "github": "GitHub Login", + "copyAuthToken": "Copy Auth Token", + "pasteToken": "Paste Token in Account Management", + "status": { + "loading": "Logging in via {{provider}}...", + "success": "Login successful! {{email}} added to pool", + "codeium": "{{provider}} auth successful, registering with Codeium...", + "firebaseError": "Firebase SDK failed to load, please refresh and retry", + "loggingIn": "Logging in via {{label}}...", + "codeiumRegistering": "{{label}} auth success, registering Codeium...", + "loginSuccess": "Login successful {{email}} added to pool", + "loginFailed": "{{label}} login failed: {{error}}" + } + }, + + "update": { + "checking": "Checking for updates...", + "newVersion": "New version found", + "current": "Current", + "remote": "Remote", + "latest": "Already up to date", + "applying": "Pulling + restarting...", + "complete": "Update complete", + "restarting": "Service restarting in background, page will auto-refresh in ~8 seconds...", + "cancelled": "Cancelled", + "notGit": "Not a git deployment — one-click update unavailable", + "notGitDesc": "If you deployed via git clone, ensure .git directory is with WindsurfAPI source. For SFTP / zip uploads, manually replace files and pm2 restart.", + "upToDate": "Already up to date, no update needed" + }, + + "proxy": { + "test": { + "button": "Test Proxy", + "result": "Result" + }, + "none": "None (use global)", + "direct": "Direct", + "global": "Global", + "current": "Current", + "notConfigured": "No global proxy configured", + "placeholderSaved": "Saved (leave empty to keep)", + "placeholderPassword": "Proxy auth password", + "auth": "Auth" + }, + + "account": { + "addSuccess": "Account added", + "deleteSuccess": "Account deleted", + "enableSuccess": "Account enabled", + "disableSuccess": "Account disabled", + "resetErrors": "Errors reset", + "tierSet": "Tier set to {{tier}}", + "editModels": "Edit available models", + "probe": { + "doing": "Probing, takes about 10-30 seconds...", + "complete": "Probe complete: {{result}}", + "allDoing": "Probing all accounts, takes 1-3 minutes depending on count...", + "allComplete": "Probe complete: {{summary}}", + "probeAllComplete": "Probe complete: {{summary}}", + "probeComplete": "Probe complete: {{tier}}" + }, + "credits": { + "refreshTitle": "Refresh credits", + "refreshed": "Credits refreshed", + "refreshFailed": "Refresh failed", + "allRefreshed": "All account credits refreshed: {{ok}} success / {{fail}} failed", + "refreshing": "Refreshing all account credits...", + "refreshingAll": "Refreshing all account credits..." + }, + "ls": { + "restarting": "Language Server restarting..." + } + }, + + "batch": { + "pasteFirst": "Please paste batch accounts first", + "importing": "Importing...", + "importingDesc": "{{count}} lines, please wait", + "complete": "Batch import complete", + "importFailed": "Batch import failed", + "importComplete": "Batch import complete: success {{success}} / failed {{fail}}", + "success": "Success", + "fail": "Fail", + "formatError": "Line {{line}} format error" + }, + + "toast": { + "copied": "Copied", + "saved": "Saved", + "cleared": "Cleared", + "refresh": "Refresh", + "error": "Error", + "loading": "Loading...", + "enterHostPort": "Please enter host and port first", + "experimentalEnabled": "Experimental feature enabled", + "experimentalDisabled": "Experimental feature disabled", + "toggleFailed": "Toggle failed: {{error}}", + "modeUpdated": "Mode updated", + "noTierModels": "No models available for this account tier", + "loginSuccess": "{{label}} login successful", + "loginFailed": "{{label}} login failed: {{error}}", + "loginSuccessAdded": "Login successful, account added to pool", + "clipboardRead": "Clipboard read", + "clipboardEmpty": "Clipboard is empty", + "clipboardUnsupported": "Current environment doesn't support auto clipboard read, please paste manually", + "clipboardError": "Failed to read clipboard: {{error}}", + "apiKeyCopied": "API Key copied", + "copyFailed": "Copy failed, please select manually", + "copyFailedError": "Copy failed: {{error}}", + "enterEmailPassword": "Please enter email and password", + "restarting": "Language Server restarting...", + "proxyConfigured": "Account proxy configured", + "proxyCleared": "Account proxy cleared", + "globalProxySaved": "Global proxy saved", + "globalProxyCleared": "Global proxy cleared", + "enterProxyHost": "Please enter proxy host", + "statsReset": "Statistics reset", + "enterKeyOrToken": "Please enter Key or Token", + "addFailed": "Add failed", + "probe": { + "doing": "Probing, takes about 10-30 seconds...", + "probeComplete": "Probe complete: {{tier}}", + "allDoing": "Probing all accounts, takes 1-3 minutes depending on count...", + "probeAllComplete": "Probe complete: {{summary}}" + }, + "probeComplete": "Probe complete: {{tier}}", + "probeAllComplete": "Probe complete: {{summary}}", + "credits": { + "refreshed": "Credits refreshed", + "refreshFailed": "Failed to refresh credits", + "refreshing": "Refreshing credits...", + "allRefreshed": "All account credits refreshed: {{ok}} success / {{fail}} failed" + } + }, + + "log": { + "level": { + "all": "All Levels", + "debug": "Debug", + "info": "Info", + "warn": "Warn", + "error": "Error" + }, + "searchPlaceholder": "Search log content...", + "clearView": "Clear View", + "viaSSE": "via SSE real-time streaming" + }, + + "model": { + "mode": { + "all": "Allow All", + "allowlist": "Allowlist", + "blocklist": "Blocklist" + }, + "provider": { + "all": "All Providers", + "anthropic": "Anthropic", + "openai": "OpenAI", + "google": "Google", + "deepseek": "DeepSeek", + "xai": "xAI", + "alibaba": "Alibaba", + "moonshot": "Moonshot", + "windsurf": "Windsurf" + }, + "list": { + "current": "Current List ({{count}})", + "empty": "List is empty" + } + }, + + "error": { + "generic": "An error occurred", + "unknown": "Unknown error", + "updateFailed": "Update failed", + "apiError": "API error", + "loadFailed": "Failed to load", + "probeFailed": "Probe failed", + "accountNotFound": "Account not found", + "openFailed": "Failed to open", + "saveFailed": "Save failed", + "deleteFailed": "Delete failed", + "networkError": "Network error", + "authRequired": "Please login first", + "clipboardNotSupported": "Current environment doesn't support clipboard auto-read, please paste manually", + "clipboardEmpty": "Clipboard is empty", + "clipboardReadFailed": "Failed to read clipboard", + "firebaseLoadFailed": "Firebase SDK failed to load, please refresh and retry", + "ERR_EMAIL_PASSWORD_REQUIRED": "Email and password are required", + "ERR_HOST_PORT_REQUIRED": "Host and port are required", + "ERR_UNCOMMITTED_CHANGES": "Working directory has uncommitted changes. Force overwrite?", + "ERR_ACCOUNTS_REQUIRED": "Accounts array is required", + "ERR_TEXT_REQUIRED": "Text field is required", + "ERR_NO_VALID_LINES": "No valid lines found", + "ERR_FORMAT_INVALID": "Invalid format. Expected: [proxy] email password", + "ERR_IDTOKEN_REQUIRED": "ID token is required", + "ERR_PROXY_PRIVATE_IP": "Proxy cannot point to private/local address", + "ERR_PROXY_HTTP_ERROR": "Proxy returned HTTP error", + "ERR_CONNECTION_FAILED": "Connection failed", + "ERR_TIMEOUT": "Timeout (10s)", + "ERR_TLS_TUNNEL_ERROR": "TLS tunnel established but returned abnormal content", + "ERR_TLS_FAILED": "TLS failed", + "ERR_LOGIN_FAILED": "Login failed", + "ERR_EMAIL_NOT_FOUND": "Email not registered for password login. If registered via Google/GitHub OAuth, use OAuth buttons or visit windsurf.com/show-auth-token to copy Auth Token", + "ERR_INVALID_PASSWORD": "Invalid password", + "ERR_INVALID_CREDENTIALS": "Invalid email or password", + "ERR_NO_PASSWORD_SET": "No password set for this account. Use Google/GitHub OAuth or visit windsurf.com/show-auth-token to copy Auth Token", + "ERR_USER_DISABLED": "Account has been disabled", + "ERR_TOO_MANY_ATTEMPTS": "Too many attempts. Please try again later", + "ERR_INVALID_EMAIL": "Invalid email format", + "ERR_CODEIUM_REGISTER_FAILED": "Codeium registration failed", + "ERR_AUTH1_TOKEN_MISSING": "Auth1 response missing token", + "ERR_POSTAUTH_FAILED": "Windsurf PostAuth failed", + "ERR_TOKEN_FETCH_FAILED": "Failed to fetch one-time auth token", + "ERR_FIREBASE_TOKEN_MISSING": "Firebase response missing idToken" + }, + + "footer": { + "version": "Version", + "langToggle": "Switch language" + }, + + "placeholder": { + "noProxy": "No proxy" + }, + + "loginResult": { + "success": "✓ Login Successful", + "apiKeyGenerated": "API Key generated{{autoAdd}}", + "fail": "✗ Login Failed", + "batchComplete": "Batch Import Complete", + "batchDesc": "Total {{total}} accounts, {{success}} success, {{fail}} fail{{autoAdd}}", + "tryOther": "Try Another Way", + "oauthHint": "If Google/GitHub OAuth was used to register, password login won't work. Use the Google/GitHub buttons above, or visit {{link}} to copy Auth Token then manually add in Account Pool page.", + "fields": { + "email": "Email", + "name": "Name", + "apiKey": "API Key", + "accountId": "Account ID" + }, + "successTitle": "Login Success", + "addedToPool": "and added to account pool" + }, + + "timeRange": { + "minutesAgo": "{{count}}m ago", + "hoursAgo": "{{count}}h ago", + "daysAgo": "{{count}}d ago" + }, + + "time": { + "day": "d", + "hour": "h", + "minute": "m", + "second": "s" + }, + + "prompt": { + "editor": { + "reset": "Reset", + "search": "Search providers...", + "templateHint": "{{model}} placeholder will be replaced with the requested model name." + } + }, + + "ls": { + "defaultInstance": "Default Instance", + "noProxy": "No proxy", + "port": "Port", + "pid": "PID", + "restartCount": "Restarts" + } +} diff --git a/src/dashboard/i18n/zh-CN.json b/src/dashboard/i18n/zh-CN.json new file mode 100644 index 00000000..1c169524 --- /dev/null +++ b/src/dashboard/i18n/zh-CN.json @@ -0,0 +1,841 @@ +{ + "_meta": { + "name": "简体中文", + "code": "zh-CN" + }, + + "brand": { + "sub": "管理控制台" + }, + + "nav": { + "group": { + "overview": "概览", + "account": "账号", + "system": "系统", + "about": "关于" + }, + "overview": "仪表盘", + "stats": "统计分析", + "windsurf-login": "登录取号", + "accounts": "账号管理", + "bans": "异常监测", + "models": "模型控制", + "proxy": "代理配置", + "logs": "运行日志", + "experimental": "实验性功能", + "credits": "致谢" + }, + + "pageTitle": { + "main": "WindsurfAPI bydwgx1337 控制台" + }, + + "page": { + "overview": "仪表盘", + "stats": "统计分析", + "windsurf-login": "登录取号", + "accounts": "账号管理", + "bans": "异常监测", + "models": "模型控制", + "proxy": "代理配置", + "logs": "运行日志", + "experimental": "实验性功能", + "credits": "致谢" + }, + + "pageSubtitle": { + "overview": "系统运行状态与关键指标概览", + "windsurf-login": "支持 Google / GitHub / 邮箱密码 三种方式登录 Windsurf 获取 API Key", + "accounts": "维护账号池,探测各账号订阅层级与可用模型", + "models": "配置模型访问策略,限制可用模型范围", + "proxy": "全局或按账号配置出口代理,独立代理将启动独立的语言服务器实例", + "logs": "通过 SSE 实时流式接收服务端日志", + "stats": "请求量、成功率、延迟分位数与账号/模型维度统计", + "bans": "追踪错误账号和异常状态", + "experimental": "尚未稳定的优化项;如果发现异常可以随时关闭", + "credits": "感谢以下朋友为项目提交 PR / 审计代码 / 修复问题" + }, + + "button": { + "cancel": "取消", + "confirm": "确认", + "forceOverride": "强制覆盖并更新", + "save": "保存", + "restart": "重启", + "updateAndRestart": "更新并重启" + }, + + "action": { + "checkUpdate": "检查更新", + "applyUpdate": "一键更新并重启", + "restart": "重启", + "login": "登录", + "save": "保存", + "clear": "清除", + "cancel": "取消", + "confirm": "确认", + "delete": "删除", + "reset": "重置", + "clearPool": "清空对话池", + "refresh": "刷新", + "probe": "探测", + "probeTitle": "探测能力", + "enable": "启用", + "disable": "停用", + "copy": "复制", + "copyTitle": "点击复制", + "configure": "配置", + "test": "测试", + "readClipboard": "读取剪贴板", + "batchImport": "批量导入并登录", + "paste": "粘贴", + "add": "添加", + "close": "关闭", + "edit": "编辑", + "selectAll": "全选", + "selectNone": "全不选", + "invert": "反选", + "showPassword": "显示/隐藏密码" + }, + + "status": { + "active": "正常", + "disabled": "已禁用", + "error": "错误", + "running": "运行中", + "stopped": "已停止", + "success": "成功", + "failed": "失败", + "pending": "待处理", + "unknown": "未知", + "rateLimited": "限速中", + "loading": "加载中…", + "testing": "测试中…", + "saving": "保存中…", + "updateAndRestartHint": "更新并重启服务", + "proxySuccess": "✓ 连通成功 出口 IP {{ip}} · 耗时 {{latency}}ms", + "proxyFailed": "✗ 失败: {{error}} · {{latency}}ms", + "checkingUpdate": "正在检查更新…", + "updateAvailable": "发现新版本", + "currentVersion": "当前版本", + "remoteVersion": "远程版本", + "upToDate": "✓ 已是最新 ({{commit}}) · {{message}}", + "pullingRestarting": "正在拉取 + 重启…", + "cancelled": "已取消", + "noUpdateNeeded": "已是最新,无需更新", + "updateComplete": "更新完成 {{before}} → {{after}}", + "restarting": "服务正在后台重启 约 8 秒后自动刷新页面…", + "notGitRepo": "非 git 部署 — 一键更新不可用", + "notGitRepoHint": "如果你是通过 git clone 部署的,请确保 .git 目录随 WindsurfAPI 源码一起上传", + "workingDirDirty": "工作区有本地修改", + "workingDirDirtyHint": "以下文件已被修改但未提交:
{{files}}
继续将用远程版本覆盖这些修改。", + "notFetched": "尚未获取", + "fetchFailed": "获取失败", + "clickToEditTier": "点击手动修改层级(探测异常时用)" + }, + + "tier": { + "pro": "PRO", + "free": "FREE", + "expired": "已过期", + "unknown": "未知", + "expiredLabel": "已过期" + }, + + "creditsBar": { + "daily": "日", + "weekly": "周", + "monthly": "月" + }, + + "section": { + "banStatus": { + "title": "健康状态" + }, + "lsStatus": { + "title": "Language Server", + "desc": "语言服务器实例状态" + }, + "oauthLogin": { + "title": "快捷登录(推荐)", + "desc": "用你的 Google 或 GitHub 账号直接登录 Windsurf 无需密码" + }, + "emailLogin": { + "title": "邮箱密码登录", + "desc": "仅限邮箱+密码注册的账号 第三方登录请用上面的按钮;支持单个登录或按「邮箱 密码」格式批量导入" + }, + "loginProxy": { + "title": "登录代理(可选)", + "desc": "为本次登录指定代理;留空则使用全局代理设置。代理生效后账号的后续聊天请求也会经此代理出站" + }, + "loginHistory": { + "title": "登录历史", + "desc": "本地记录最近 50 条登录操作" + }, + "addAccount": { + "title": "添加账号", + "desc": "支持 API Key 或 Auth Token 两种方式", + "hint": "推荐用 Token 方式添加账号 点 {{link}} 登录后复制 Token 粘贴到下面就行 所有登录方式(邮箱/Google/GitHub)都能用" + }, + "accountList": { + "title": "账号列表", + "desc": "点击探测按钮可单独更新某个账号的能力信息" + }, + "modelPolicy": { + "title": "访问策略", + "desc": "选择「全部允许」不限制;「白名单」只允许列出的模型;「黑名单」屏蔽列出的模型" + }, + "modelList": { + "title": "模型清单", + "titleAllow": "允许的模型", + "titleBlock": "屏蔽的模型", + "descAllow": "只有选中的模型可以使用,未选中的模型将返回 403 错误", + "descBlock": "选中的模型将被屏蔽,未选中的模型可正常使用", + "search": "搜索模型...", + "provider": "供应商" + }, + "globalProxy": { + "title": "全局代理", + "desc": "未配置独立代理的账号将使用此配置" + }, + "accountProxy": { + "title": "账号独立代理", + "desc": "为特定账号设置专属代理;每个独立代理会启动独立 LS 实例" + }, + "cascadeReuse": { + "title": "Cascade 对话复用", + "desc": "多轮对话时复用同一个 cascade_id,只把最新一条 user 消息发给 Windsurf,让服务端维持上下文缓存。命中时可显著降低 TTFB 与上传体积;未命中或账号/LS 变更会自动回退到新会话。需要客户端保留完整历史并按顺序追加(如 new-api、OpenWebUI)。", + "enable": "启用 Cascade 对话复用", + "hint": "默认关闭。开启后对当前对话池立即生效。" + }, + "identityPrompt": { + "title": "模型身份提示词", + "desc": "在每个请求开头注入模型身份系统提示词。", + "enable": "启用模型身份注入", + "hint": "默认开启;关闭后下面的模板不起作用", + "templateHint": "每个厂商的模板可编辑,{{model}} 占位符会被替换成请求的模型名(例如 claude-opus-4.6)。" + }, + "identityInjection": { + "title": "模型身份注入", + "desc": "开启后在每个请求最前面注入一条系统提示词,覆盖 Cascade 自带的 Windsurf 身份;下面每个厂商的模板可自定义,保存后立即生效" + }, + "systemPrompts": { + "title": "系统提示词", + "desc": "工具注入、对话模式等内置提示词模板。修改后立即生效,点「重置」恢复默认。" + }, + "lsPool": { + "title": "语言服务器池", + "desc": "{{count}} 个 LS 实例 · 每个独立代理对应一个进程", + "sectionTitle": "语言服务器池", + "sectionDesc": "{{count}} 个 LS 实例 · 每个独立代理对应一个进程", + "port": "端口", + "defaultInstance": "默认实例", + "noProxy": "无代理" + }, + "modelStats": { + "title": "模型使用统计", + "desc": "按模型聚合:请求量、成功率、平均耗时与 p50 / p95 延迟分位数" + }, + "accountStats": { + "title": "账号维度统计", + "desc": "每个账号 ID 前 8 位的请求量与成功率" + }, + "accountHealth": { + "title": "账号健康状况" + }, + "requestChart": { + "title": "请求量时间序列" + } + }, + + "help": { + "token": "推荐用 Token 方式添加账号 点 {{link}} 登录后复制 Token 粘贴到下面就行 所有登录方式(邮箱/Google/GitHub)都能用", + "tokenGuide": "推荐用 Token 方式添加账号 点 ", + "tokenGuideEnd": " 登录后复制 Token 粘贴到下面就行 所有登录方式(邮箱/Google/GitHub)都能用" + }, + + "field": { + "email": { + "label": "邮箱", + "placeholder": "your-email@example.com" + }, + "password": { + "label": "密码", + "placeholder": "••••••••" + }, + "type": { + "label": "类型" + }, + "tier": { + "label": "层级" + }, + "host": { + "label": "主机", + "placeholder": "proxy.example.com" + }, + "port": { + "label": "端口", + "placeholder": "8080" + }, + "username": { + "label": "用户名", + "placeholder": "可选" + }, + "passwordProxy": { + "label": "密码", + "placeholder": "可选" + }, + "key": { + "label": "Key / Token", + "placeholder": "粘贴 Auth Token(从 windsurf.com/show-auth-token 获取)" + }, + "label": { + "label": "标签", + "placeholder": "可选" + }, + "batchInput": { + "label": "批量导入", + "hint": "每行一个账号。格式:[代理] 邮箱 密码。代理可选,支持 http/socks5。会自动绑定代理到对应账号。" + }, + "proxyType": { + "label": "类型" + }, + "proxyHost": { + "label": "主机", + "placeholder": "留空=使用全局" + }, + "provider": { + "label": "供应商", + "all": "全部供应商" + }, + "proxyUser": { + "placeholder": "代理认证用户名" + }, + "proxyPass": { + "placeholder": "代理认证密码" + }, + "searchProvider": { + "placeholder": "搜索提供商..." + }, + "search": { + "label": "搜索", + "placeholder": "搜索..." + }, + "searchModel": { + "label": "搜索模型", + "placeholder": "输入模型名称筛选..." + }, + "providerFilter": { + "label": "供应商" + }, + "logLevel": { + "label": "日志级别" + }, + "identitySearch": { + "placeholder": "搜索提供商..." + }, + "autoScroll": "自动滚动", + "autoAdd": "登录成功自动加入账号池" + }, + + "table": { + "header": { + "time": "时间", + "email": "邮箱", + "status": "状态", + "proxy": "代理", + "action": "操作", + "id": "ID", + "label": "标签", + "tier": "层级", + "rpm": "RPM", + "credits": "余额", + "models": "可用模型", + "error": "错误", + "lastUsed": "最后使用", + "key": "Key", + "accountId": "账号 ID", + "account": "账号", + "requests": "请求", + "success": "成功", + "errors": "错误", + "successRate": "成功率", + "avg": "平均", + "p50": "p50", + "p95": "p95", + "errorCount": "错误数", + "model": "模型", + "note": "说明" + }, + "empty": { + "loginHistory": "暂无登录记录", + "accounts": "暂无账号,请先添加", + "accountNotFound": "账号不存在", + "probeFailed": "探测失败", + "noRequestData": "暂无请求数据", + "noAccountRequestData": "暂无账号级请求数据", + "noData": "暂无数据", + "noValidAccounts": "暂无可用账号可用于路由", + "accountsProxy": "暂无账号", + "modelStats": "暂无模型统计数据", + "accountStats": "暂无账号统计数据", + "banList": "暂无异常账号", + "models": "没有符合的模型", + "credits": "加载中…" + } + }, + + "card": { + "activeAccounts": { + "title": "活跃账号", + "subtitle": "{{total}} 个总账号 · {{error}} 异常" + }, + "totalRequests": { + "title": "总请求数", + "subtitle": "成功率 {{rate}}%" + }, + "uptime": { + "title": "运行时间", + "subtitle": "启动于 {{time}}" + }, + "languageServer": { + "title": "Language Server", + "running": "运行中", + "stopped": "已停止", + "subtitle": "{{count}} 个实例 · 端口 {{port}}" + }, + "responseCache": { + "title": "响应缓存", + "subtitle": "{{hits}} 命中 / {{misses}} 未命中 · {{size}}/{{max}} 条" + }, + "banStats": { + "title": "健康概览" + }, + "abnormalAccounts": { + "title": "异常账号", + "subtitle": "共 {{count}} 个账号" + }, + "disabled": { + "title": "已停用", + "subtitle": "自动错误停用" + }, + "rateLimit": { + "title": "限流中", + "badge": "限流中", + "subtitle": "暂时不参与调度" + }, + "stats": { + "title": "总请求", + "subtitle": "运行 {{time}}" + }, + "success": { + "title": "成功", + "subtitle": "成功率 {{rate}}%" + }, + "errors": { + "title": "错误", + "subtitle": "失败率 {{rate}}%" + }, + "p95Latency": { + "title": "平均 p95 延迟", + "subtitle": "{{count}} 个模型" + }, + "hitRate": { + "title": "命中率", + "subtitle": "{{hits}} 命中 / {{misses}} 未命中" + }, + "poolSize": { + "title": "对话池", + "subtitle": "上限 {{max}} · TTL {{ttl}} 分钟" + }, + "stores": { + "title": "存储次数", + "subtitle": "过期 {{expired}} · 淘汰 {{evicted}}" + }, + "defaultInstance": "默认实例", + "noProxy": "无代理", + "restarts": "重启", + "custom": "已自定义" + }, + + "login": { + "title": "控制台登录", + "subtitle": "请输入管理密码以继续", + "passwordPlaceholder": "Dashboard 密码", + "button": "登 录" + }, + + "range": { + "6h": "近 6小时", + "24h": "近 24小时", + "72h": "近 72小时" + }, + + "credits": { + "contributors": "核心贡献者", + "cta": { + "title": "想加入这份名单?", + "desc": "欢迎到 GitHub 提 issue 或 pull request — 不管是修 bug、加模型、改 UI 还是发现安全问题,都会被记录在这里。", + "issue": "提 issue", + "pr": "提 PR" + } + }, + + "experimental": { + "enable": "启用", + "disable": "禁用", + "cascadeReuse": { + "title": "Cascade 对话复用", + "desc": "多轮对话时复用同一个 cascade_id,只把最新一条 user 消息发给 Windsurf,让服务端维持上下文缓存。命中时可显著降低 TTFB 与上传体积;未命中或账号/LS 变更会自动回退到新会话。需要客户端保留完整历史并按顺序追加(如 new-api、OpenWebUI)。", + "enable": "启用 Cascade 对话复用", + "hint": "默认关闭。开启后对当前对话池立即生效。" + }, + "identityPrompt": { + "title": "启用模型身份注入", + "desc": "默认开启;关闭后下面的模板不起作用。" + }, + "templateHint": "每个厂商的模板可编辑,{{model}} 占位符会被替换成请求的模型名(例如 claude-opus-4.6)。", + "clearPool": "清空对话池", + "refresh": "刷新", + "noMatch": "无匹配", + "restoreDefault": "恢复默认", + "save": "保存", + "default": "默认", + "toolReinforcement": "工具调用格式指引", + "communicationWithTools": "有工具时的通信指令", + "communicationNoTools": "无工具时的通信指令", + "promptEmpty": "模板不能为空", + "missingPlaceholder": "缺少 {model} 占位符", + "missingPlaceholderDesc": "保存后该厂商的模型名将无法被替换进提示词。确定继续?", + "restored": "已恢复" + }, + + "confirm": { + "restartTitle": "重启 Language Server", + "restartDesc": "进行中的请求将会失败,确定要继续吗?", + "restoreDefaultDesc": "恢复到默认?", + "clearPoolDesc": "当前所有进行中的 Cascade 会话都将被清除,确定继续吗?" + }, + + "modal": { + "blockedModels": { + "title": "编辑可用模型 — {{email}}", + "hint": "勾选 = 可用;取消 = 对该账号禁用", + "enabled": "已启用", + "saved": "已保存:启用 {{enabled}} / 禁用 {{disabled}}", + "saving": "保存中...", + "total": "总计" + }, + "restartLS": { + "title": "重启 Language Server", + "desc": "进行中的请求将会失败,确定要继续吗?" + }, + "deleteAccount": { + "title": "删除账号", + "desc": "此操作不可撤销,确定要删除该账号吗?" + }, + "resetStats": { + "title": "重置统计", + "desc": "清空所有统计数据?" + }, + "overrideTier": { + "title": "手动设置账号层级", + "desc": "当前:{{current}}。Windsurf Pro 试用有时自动探测失败(issue #8 的连锁影响),手动改成 pro 即可解锁所有 Pro 模型。", + "options": { + "pro": "Pro(解锁所有模型)", + "free": "Free(仅 gpt-4o-mini 和 gemini-2.5-flash)", + "unknown": "Unknown(让系统重新判定)" + } + }, + "applyUpdate": { + "title": "一键更新并重启", + "desc": "将执行 git pull 并重启 PM2。重启期间 dashboard 会短暂不可用(约 5-10 秒)。确定继续?" + }, + "dirtyWorktree": { + "title": "工作区有本地修改", + "desc": "以下文件已被修改但未提交:

{{files}}
继续将用远程版本覆盖这些修改。非 git 部署(SFTP / 压缩包)请点强制覆盖。", + "forceUpdate": "强制覆盖并更新" + }, + "configureProxy": { + "title": "配置账号代理", + "desc": "为账号 {{label}} 设置独立代理" + } + }, + + "oauth": { + "tryAnotherWay": "换个方式试试", + "google": "Google 登录", + "github": "GitHub 登录", + "copyAuthToken": "复制 Auth Token", + "pasteToken": "去账号管理粘贴 Token", + "status": { + "loading": "正在通过 {{provider}} 登录...", + "success": "登录成功 {{email}} 已加入账号池", + "codeium": "{{provider}} 认证成功 正在注册 Codeium...", + "firebaseError": "Firebase SDK 加载失败 请刷新页面重试", + "loggingIn": "正在通过 {{label}} 登录...", + "codeiumRegistering": "{{label}} 认证成功 正在注册 Codeium...", + "loginSuccess": "登录成功 {{email}} 已加入账号池", + "loginFailed": "{{label}} 登录失败: {{error}}" + } + }, + + "update": { + "checking": "正在检查更新...", + "newVersion": "发现新版本", + "current": "当前", + "remote": "远程", + "latest": "已是最新", + "applying": "正在拉取 + 重启...", + "complete": "更新完成", + "restarting": "服务正在后台重启 约 8 秒后自动刷新页面...", + "cancelled": "已取消", + "notGit": "此实例不是 git 部署 一键更新不可用", + "notGitDesc": "如果你用 git clone 部署的 需要确保 .git 目录和 WindsurfAPI 源代码目录在一起。SFTP / 压缩包上传的部署请手动替换文件后 pm2 重启", + "upToDate": "已是最新,无需更新" + }, + + "proxy": { + "test": { + "button": "测试代理", + "result": "结果" + }, + "none": "无(使用全局)", + "direct": "直连", + "global": "全局", + "current": "当前", + "notConfigured": "未配置全局代理", + "placeholderSaved": "已保存(留空保持原值)", + "placeholderPassword": "代理认证密码", + "auth": "认证" + }, + + "account": { + "addSuccess": "账号已添加", + "deleteSuccess": "账号已删除", + "enableSuccess": "账号已启用", + "disableSuccess": "账号已停用", + "resetErrors": "错误已重置", + "tierSet": "层级已设为 {{tier}}", + "editModels": "编辑可用模型", + "probe": { + "doing": "探测中,约需 10-30 秒...", + "complete": "探测完成:{{result}}", + "allDoing": "全部探测中,依账号数量约需 1-3 分钟...", + "allComplete": "探测完成:{{summary}}", + "probeAllComplete": "探测完成:{{summary}}", + "probeComplete": "探测完成:{{tier}}" + }, + "credits": { + "refreshTitle": "刷新余额", + "refreshed": "余额已刷新", + "refreshFailed": "刷新失败", + "allRefreshed": "余额刷新完成:{{ok}} 成功 / {{fail}} 失败", + "refreshing": "正在刷新所有账号余额...", + "refreshingAll": "正在刷新所有账号余额..." + }, + "ls": { + "restarting": "Language Server 重启中..." + } + }, + + "batch": { + "pasteFirst": "请先粘贴批量账号", + "importing": "导入中...", + "importingDesc": "{{count}} 行,请稍候", + "complete": "批量导入完成", + "importFailed": "批量导入失败", + "importComplete": "批量导入完成:成功 {{success}} / 失败 {{fail}}", + "success": "成功", + "fail": "失败", + "formatError": "第 {{line}} 行格式错误" + }, + + "toast": { + "copied": "已复制", + "saved": "已保存", + "cleared": "已清空", + "refresh": "已刷新", + "error": "错误", + "loading": "加载中...", + "enterHostPort": "请先填主机和端口", + "experimentalEnabled": "已启用实验性功能", + "experimentalDisabled": "已关闭实验性功能", + "toggleFailed": "切换失败:{{error}}", + "modeUpdated": "模式已更新", + "noTierModels": "该账号层级无可用模型", + "loginSuccess": "{{label}} 登录成功", + "loginFailed": "{{label}} 登录失败:{{error}}", + "loginSuccessAdded": "登录成功,账号已加入账号池", + "clipboardRead": "已读取剪贴板", + "clipboardEmpty": "剪贴板为空", + "clipboardUnsupported": "当前环境不支持自动读取剪贴板,请手动粘贴", + "clipboardError": "读取剪贴板失败:{{error}}", + "apiKeyCopied": "API Key 已复制", + "copyFailed": "复制失败,请手动选择", + "copyFailedError": "复制失败:{{error}}", + "enterEmailPassword": "请输入邮箱和密码", + "restarting": "Language Server 重启中...", + "proxyConfigured": "账号代理已配置", + "proxyCleared": "账号代理已清除", + "globalProxySaved": "全局代理已保存", + "globalProxyCleared": "全局代理已清除", + "enterProxyHost": "请输入代理主机", + "statsReset": "统计已重置", + "enterKeyOrToken": "请输入 Key 或 Token", + "addFailed": "添加失败", + "probe": { + "doing": "探测中,约需 10-30 秒...", + "probeComplete": "探测完成:{{tier}}", + "allDoing": "探测所有账号中,约需 1-3 分钟(视数量而定)...", + "probeAllComplete": "探测完成:{{summary}}" + }, + "probeComplete": "探测完成:{{tier}}", + "probeAllComplete": "探测完成:{{summary}}", + "credits": { + "refreshed": "余额已刷新", + "refreshFailed": "刷新余额失败", + "refreshing": "正在刷新余额...", + "allRefreshed": "余额刷新完成:{{ok}} 成功 / {{fail}} 失败" + } + }, + + "log": { + "level": { + "all": "所有级别", + "debug": "Debug", + "info": "Info", + "warn": "Warn", + "error": "Error" + }, + "searchPlaceholder": "搜索日志内容...", + "clearView": "清空视图", + "viaSSE": "通过 SSE 实时流式接收" + }, + + "model": { + "mode": { + "all": "全部允许", + "allowlist": "白名单", + "blocklist": "黑名单" + }, + "provider": { + "all": "全部供应商", + "anthropic": "Anthropic", + "openai": "OpenAI", + "google": "Google", + "deepseek": "DeepSeek", + "xai": "xAI", + "alibaba": "Alibaba", + "moonshot": "Moonshot", + "windsurf": "Windsurf" + }, + "list": { + "current": "当前清单 ({{count}})", + "empty": "清单为空" + } + }, + + "error": { + "generic": "发生错误", + "unknown": "未知错误", + "updateFailed": "更新失败", + "apiError": "API 错误", + "loadFailed": "加载失败", + "probeFailed": "探测失败", + "accountNotFound": "账号未找到", + "openFailed": "打开失败", + "saveFailed": "保存失败", + "deleteFailed": "删除失败", + "networkError": "网络错误", + "authRequired": "请先登录", + "clipboardNotSupported": "当前环境不支持自动读取剪贴板,请手动粘贴", + "clipboardEmpty": "剪贴板为空", + "clipboardReadFailed": "读取剪贴板失败", + "firebaseLoadFailed": "Firebase SDK 加载失败 请刷新页面重试", + "ERR_EMAIL_PASSWORD_REQUIRED": "email 和 password 为必填", + "ERR_HOST_PORT_REQUIRED": "缺少 host 或 port", + "ERR_UNCOMMITTED_CHANGES": "工作区有未提交的修改(SFTP 部署或手动改过代码)。确定要覆盖本地修改用远程最新版本吗?", + "ERR_ACCOUNTS_REQUIRED": "accounts 为必填数组", + "ERR_TEXT_REQUIRED": "缺少 text 字段", + "ERR_NO_VALID_LINES": "没有有效行", + "ERR_FORMAT_INVALID": "格式错误 需要 [proxy] email password", + "ERR_IDTOKEN_REQUIRED": "缺少 idToken", + "ERR_PROXY_PRIVATE_IP": "代理地址不能指向内网/本机", + "ERR_PROXY_HTTP_ERROR": "代理返回 HTTP 错误", + "ERR_CONNECTION_FAILED": "连接失败", + "ERR_TIMEOUT": "超时(10s)", + "ERR_TLS_TUNNEL_ERROR": "TLS 隧道建立但返回内容异常", + "ERR_TLS_FAILED": "TLS 失败", + "ERR_LOGIN_FAILED": "登录失败", + "ERR_EMAIL_NOT_FOUND": "该邮箱未注册邮箱密码登录方式。若通过 Google/GitHub OAuth 注册,请使用 OAuth 按钮或访问 windsurf.com/show-auth-token 复制 Auth Token", + "ERR_INVALID_PASSWORD": "密码错误", + "ERR_INVALID_CREDENTIALS": "邮箱或密码错误", + "ERR_NO_PASSWORD_SET": "该账号未设置密码登录方式。使用 Google/GitHub OAuth 或访问 windsurf.com/show-auth-token 复制 Auth Token", + "ERR_USER_DISABLED": "账号已被停用", + "ERR_TOO_MANY_ATTEMPTS": "尝试太多次 请稍后再试", + "ERR_INVALID_EMAIL": "邮箱格式错误", + "ERR_CODEIUM_REGISTER_FAILED": "Codeium 注册失败", + "ERR_AUTH1_TOKEN_MISSING": "Auth1 回应缺少 token", + "ERR_POSTAUTH_FAILED": "Windsurf PostAuth 失败", + "ERR_TOKEN_FETCH_FAILED": "获取一次性 Auth Token 失败", + "ERR_FIREBASE_TOKEN_MISSING": "Firebase 回应缺少 idToken" + }, + + "footer": { + "version": "版本", + "langToggle": "切换语言" + }, + + "placeholder": { + "noProxy": "无代理" + }, + + "loginResult": { + "success": "✓ 登录成功", + "apiKeyGenerated": "API Key 已生成{{autoAdd}}", + "fail": "✗ 登录失败", + "batchComplete": "批量导入完成", + "batchDesc": "共 {{total}} 个账号,成功 {{success}} 个,失败 {{fail}} 个{{autoAdd}}", + "tryOther": "换个方式试试", + "oauthHint": "若你用 Google/GitHub 注册的 Windsurf 账号 此处密码登录不适用 请用页面顶部的 Google / GitHub 登录按钮 或访问 {{link}} 复制 Auth Token 后在「账号管理」页手动添加", + "fields": { + "email": "邮箱", + "name": "姓名", + "apiKey": "API Key", + "accountId": "账号 ID" + }, + "successTitle": "登录成功", + "addedToPool": "并加入账号池" + }, + + "timeRange": { + "minutesAgo": "{{count}}分钟前", + "hoursAgo": "{{count}}小时前", + "daysAgo": "{{count}}天前" + }, + + "time": { + "day": "天", + "hour": "时", + "minute": "分", + "second": "秒" + }, + + "prompt": { + "editor": { + "reset": "重置", + "search": "搜索提供商...", + "templateHint": "{{model}} 占位符会被替换成请求的模型名。" + } + }, + + "ls": { + "defaultInstance": "默认实例", + "noProxy": "无代理", + "port": "端口", + "pid": "PID", + "restartCount": "重启次数" + } +} diff --git a/src/dashboard/index.html b/src/dashboard/index.html index a4fe3fc0..5b339136 100644 --- a/src/dashboard/index.html +++ b/src/dashboard/index.html @@ -3,7 +3,7 @@ -WindsurfAPI bydwgx1337 控制台 +WindsurfAPI bydwgx1337 控制台