diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da7920402..d45c95c54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,6 +112,12 @@ bun run typecheck # 如果更改影响运行逻辑,执行端到端验证或 bun run test ``` +### i18n 变更(翻译质量/抽查) + +如果 PR 涉及 i18n 文案(尤其是 `settings` / `dashboard` / `myUsage`),请遵循: +- 规则说明:`docs/i18n-translation-quality.md` +- PR Checklist:`docs/i18n-pr-checklist.md` + CI 会在 PR 上运行 `Docker Build Test`(见 `.github/CI_CD_SETUP.md`)。如需验证容器构建,可本地执行: ```bash diff --git a/biome.json b/biome.json index 87362d2ac..107110c0f 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/docs/i18n-pr-checklist.md b/docs/i18n-pr-checklist.md new file mode 100644 index 000000000..084fc5f94 --- /dev/null +++ b/docs/i18n-pr-checklist.md @@ -0,0 +1,36 @@ +# i18n PR checklist + +> 中文对照版: [i18n-pr-checklist.zh-CN.md](i18n-pr-checklist.zh-CN.md) + +This checklist is for changes that affect i18n messages, especially `settings`, `dashboard`, and `myUsage`. + +## Required (automation) + +- [ ] Run placeholder audit (scoped): + - `bun run i18n:audit-placeholders` +- [ ] If the PR is meant to eliminate placeholders, ensure fail mode is clean: + - `bun run i18n:audit-placeholders:fail` +- [ ] Run messages no-emoji audit (fail mode): + - `bun run i18n:audit-messages-no-emoji:fail` +- [ ] Run unit tests relevant to i18n/settings split: + - `bunx vitest run tests/unit/i18n/settings-split-guards.test.ts` + - `bunx vitest run tests/unit/i18n/settings-index-modules-load.test.ts` + +## Required (review evidence) + +- [ ] Include in PR description: + - the audit output diff (before/after) or a short summary (counts by locale + key modules) + - any allowlist changes with reasons (keep allowlist minimal and auditable) + +## Required (manual spotcheck) + +For at least `ja` and `zh-TW`: +- [ ] Settings pages (key areas): provider list, provider form, request filters, notifications +- [ ] Dashboard key widgets +- [ ] My Usage page + +Attach screenshots (or provide a local path) for the key pages above. + +## Rules to follow + +See `docs/i18n-translation-quality.md` for R1/R2/R3 rules and allowlist conventions. diff --git a/docs/i18n-pr-checklist.zh-CN.md b/docs/i18n-pr-checklist.zh-CN.md new file mode 100644 index 000000000..40b6188e3 --- /dev/null +++ b/docs/i18n-pr-checklist.zh-CN.md @@ -0,0 +1,36 @@ +# i18n PR 检查清单 + +> English: [i18n-pr-checklist.md](i18n-pr-checklist.md) + +此清单适用于会影响 i18n messages 的变更,尤其是 `settings`、`dashboard`、`myUsage`。 + +## 必做(自动化) + +- [ ] 运行 placeholder 审计(scoped): + - `bun run i18n:audit-placeholders` +- [ ] 若该 PR 目标是清零 placeholders,确保 fail 模式无命中: + - `bun run i18n:audit-placeholders:fail` +- [ ] 运行 messages no-emoji 审计(fail 模式): + - `bun run i18n:audit-messages-no-emoji:fail` +- [ ] 运行与 i18n/settings split 相关的单元测试: + - `bunx vitest run tests/unit/i18n/settings-split-guards.test.ts` + - `bunx vitest run tests/unit/i18n/settings-index-modules-load.test.ts` + +## 必做(Review 证据) + +- [ ] 在 PR 描述中包含: + - 审计输出 diff(before/after)或简短摘要(按 locale 统计 + 关键模块) + - 如有 allowlist 变更,说明原因(保持 allowlist 最小且可审计) + +## 必做(人工抽查) + +至少覆盖 `ja` 与 `zh-TW`: +- [ ] Settings 页面(关键区域):provider list、provider form、request filters、notifications +- [ ] Dashboard 关键组件 +- [ ] My Usage 页面 + +为上述关键页面附上截图(或提供本地路径)。 + +## 需要遵守的规则 + +参见 `docs/i18n-translation-quality.md` 获取 R1/R2/R3 规则与 allowlist 约定。 diff --git a/docs/i18n-pr-evidence-2026-01-11.md b/docs/i18n-pr-evidence-2026-01-11.md new file mode 100644 index 000000000..cafa696cb --- /dev/null +++ b/docs/i18n-pr-evidence-2026-01-11.md @@ -0,0 +1,68 @@ +# i18n no-emoji / docs zh evidence (2026-01-11) + +This document is intended to be copy-pasted into a PR description. + +## Summary + +- Emoji cleanup (messages JSON): + - Before: 20 strings contained emoji (top files: `provider-chain.json`, `settings/data.json` across locales) + - After: 0 hits from `rg -n --pcre2 "\\p{Extended_Pictographic}" messages` +- Placeholder audit (zh-CN equality placeholders): OK +- Quality gates: `lint` / `typecheck` / `test` / `build` all pass + +## Emoji cleanup details (messages JSON) + +Key locations that were cleaned (keys unchanged, only values updated): +- `messages//provider-chain.json` + - `provider-chain.timeline.circuitTriggered` + - `provider-chain.timeline.systemErrorNote` +- `messages//settings/data.json` + - `settings.data.import.warningMerge` + - `settings.data.import.warningOverwrite` + +Commands: +- Before/after scan: + - `rg -n --pcre2 "\\p{Extended_Pictographic}" messages` +- Optional structured listing (prints masked preview, no emoji characters): + - `node scripts/audit-messages-emoji.js --format=tsv` + +## Placeholder audit (R1) + +- `bun run i18n:audit-placeholders:fail` +- Expected output: + - `OK: no zh-CN placeholder candidates found in split settings.` + +## No-emoji gate (R4) + +- Script (codepoints only, no emoji printing): + - `bun run i18n:audit-messages-no-emoji:fail` +- Regression test (part of `bun run test`): + - `tests/unit/i18n/audit-messages-no-emoji-script.test.ts` + +## Full regression commands + +- `bun run lint` +- `bun run typecheck` +- `bun run test` +- `bun run build` + +## Related commits (local) + +- `564ab845` chore: add messages emoji audit script (I18NE-010) +- `aaa9fc7d` fix: remove emoji from messages warnings (I18NE-040) +- `80d20686` test: add messages no-emoji audit gate (I18NE-050) +- `2ee38f59` docs: add zh-CN i18n docs (I18NE-020) +- `44eeb5e9` docs: add messages no-emoji rule (I18NE-060) +- `92ebaf0e` chore: run full regression checks (I18NE-070) + +## Manual spotcheck (ja / zh-TW) + +Due to environment limitations (no GUI/browser automation in this run), screenshots are not attached here. + +Recommended steps: +1. Start dev server: `bun run dev` (port 13500) +2. Open pages for both `ja` and `zh-TW` locales: + - Settings: `/settings` (providers list/form, request filters, notifications) + - Dashboard: `/dashboard` (key widgets) + - My Usage: `/my-usage` +3. Attach screenshots to the PR (or provide local file paths) and label each with locale + route. diff --git a/docs/i18n-settings-split.md b/docs/i18n-settings-split.md new file mode 100644 index 000000000..9a42e0d34 --- /dev/null +++ b/docs/i18n-settings-split.md @@ -0,0 +1,37 @@ +# i18n settings split + +> 中文对照版: [i18n-settings-split.zh-CN.md](i18n-settings-split.zh-CN.md) + +This repository splits `messages//settings.json` into smaller JSON chunks under `messages//settings/`. + +## Layout +- `messages//settings/*.json`: settings top-level object parts +- `messages//settings/strings.json`: top-level string keys that belong directly under `settings` +- `messages//settings/providers/*.json`: `settings.providers` object parts +- `messages//settings/providers/strings.json`: provider-level string keys +- `messages//settings/providers/form/*.json`: `settings.providers.form` object parts +- `messages//settings/providers/form/strings.json`: provider form string keys + +Runtime composition happens in `messages//settings/index.ts` and is imported by `messages//index.ts`. + +## Verification +- Translation quality rules and audit commands: + - `docs/i18n-translation-quality.md` + +- Sync keys across locales (canonical: zh-CN): + - `node scripts/sync-settings-keys.js` + +- Unit tests: + - `bun run test` + +- Scoped coverage for split-related modules: + - `bunx vitest run --coverage --coverage.include=scripts/sync-settings-keys.js --coverage.include=messages/**/settings/index.ts` + +- Typecheck: + - `bun run typecheck` + +- Lint: + - `bun run lint` + +- Production build: + - `bun run build` diff --git a/docs/i18n-settings-split.zh-CN.md b/docs/i18n-settings-split.zh-CN.md new file mode 100644 index 000000000..f83f0ce55 --- /dev/null +++ b/docs/i18n-settings-split.zh-CN.md @@ -0,0 +1,39 @@ +# i18n settings split(拆分说明) + +> English: [i18n-settings-split.md](i18n-settings-split.md) + +本仓库将 `messages//settings.json` 拆分为更小的 JSON 文件,存放在 `messages//settings/` 目录下。 + +## 目录结构(Layout) + +- `messages//settings/*.json`: settings 顶层对象的各个子模块 +- `messages//settings/strings.json`: 直接属于 `settings` 顶层的字符串 key +- `messages//settings/providers/*.json`: `settings.providers` 对象拆分后的子模块 +- `messages//settings/providers/strings.json`: provider 级别的字符串 key +- `messages//settings/providers/form/*.json`: `settings.providers.form` 对象拆分后的子模块 +- `messages//settings/providers/form/strings.json`: provider form 的字符串 key + +运行时拼装发生在 `messages//settings/index.ts`,并由 `messages//index.ts` 引入。 + +## 验证(Verification) + +- 翻译质量规则与审计命令: + - `docs/i18n-translation-quality.md` + +- 跨 locale 同步 key(canonical: zh-CN): + - `node scripts/sync-settings-keys.js` + +- 单元测试: + - `bun run test` + +- 针对 split 相关模块的 scoped coverage: + - `bunx vitest run --coverage --coverage.include=scripts/sync-settings-keys.js --coverage.include=messages/**/settings/index.ts` + +- Typecheck: + - `bun run typecheck` + +- Lint: + - `bun run lint` + +- Production build: + - `bun run build` diff --git a/docs/i18n-translation-quality.md b/docs/i18n-translation-quality.md new file mode 100644 index 000000000..f4e817056 --- /dev/null +++ b/docs/i18n-translation-quality.md @@ -0,0 +1,64 @@ +# i18n translation quality rules (R1/R2/R3) + +> 中文对照版: [i18n-translation-quality.zh-CN.md](i18n-translation-quality.zh-CN.md) + +This document defines the **scope** and **executable rules** for i18n translation quality in this repo. +Downstream scripts and review checklists should follow this document as the source of truth. + +## Scope + +Must-translate scope (at least): +- `settings` (split settings messages under `messages//settings/`) +- `dashboard` (`messages//dashboard.json`) +- `myUsage` (`messages//myUsage.json`) + +Locales: `zh-CN` is canonical. Other supported locales: `en`, `ja`, `ru`, `zh-TW`. + +## Rule R1: No zh-CN placeholders in non-canonical locales + +For any non-canonical locale: +- If a leaf string **equals** the `zh-CN` leaf string at the same key path, and the `zh-CN` value contains Han characters, it is treated as a **placeholder candidate**. +- Placeholder candidates should be **fixed** (translated), or **explicitly allowlisted** with a documented reason. + +Executable check: +- `bun run i18n:audit-placeholders` +- To fail the command on any findings: add `--fail`. + - `bun run i18n:audit-placeholders:fail` + +Allowlist (auditable, minimal): +- `scripts/audit-settings-placeholders.allowlist.json` +- Supported filters: `key`, `keyPrefix`, `keyRegex`, `valueRegex`, plus `glossary` terms. + +## Rule R2: Placeholders/tokens must be preserved + +When updating translations, **do not change**: +- keys / JSON structure +- placeholder tokens (e.g. `{name}`, `{count}`, `{resetTime}`) +- URL / command snippets unless intentionally translated and verified safe + +Recommended verification: +- unit tests under `tests/unit/i18n/` +- spot-check affected UI pages for the locale (see the PR checklist) + +## Rule R3: Glossary and consistent terminology + +Maintain a short glossary for terms that should be consistent across locales (brand, model names, product terms). + +Initial glossary (expand as needed, but keep it minimal and reviewed): +- Provider / Model / API / HTTP/2 +- Claude / OpenAI / Codex (names should not be translated) + +## Rule R4: No emoji in messages JSON + +`messages/**/*.json` must not contain emoji characters. + +Executable check: +- `bun run i18n:audit-messages-no-emoji:fail` + +Notes: +- The audit output prints file path + key path + Unicode codepoints (without printing emoji characters). + +## Notes + +- Prefer fixing translations over expanding allowlists. +- Every allowlist entry must have a clear reason in the allowlist file (and ideally referenced in the PR description). diff --git a/docs/i18n-translation-quality.zh-CN.md b/docs/i18n-translation-quality.zh-CN.md new file mode 100644 index 000000000..dc8759e37 --- /dev/null +++ b/docs/i18n-translation-quality.zh-CN.md @@ -0,0 +1,64 @@ +# i18n 翻译质量规则(R1/R2/R3) + +> English: [i18n-translation-quality.md](i18n-translation-quality.md) + +本文档定义本仓库 i18n 翻译质量的 **scope** 与 **可执行规则**。 +下游脚本与 review checklist 应以本文档为真相源。 + +## Scope + +必须翻译的范围(至少包含): +- `settings`(拆分后的 settings messages,位于 `messages//settings/`) +- `dashboard`(`messages//dashboard.json`) +- `myUsage`(`messages//myUsage.json`) + +Locales:`zh-CN` 为 canonical。其他支持的 locales:`en`、`ja`、`ru`、`zh-TW`。 + +## Rule R1:非 canonical locale 禁止出现 zh-CN placeholder + +对于任意非 canonical locale: +- 若某个 leaf string 在相同 key path 下 **等于** `zh-CN` 的 leaf string,且 `zh-CN` 的值包含汉字,则视为 **placeholder candidate**。 +- placeholder candidates 应被 **修复**(翻译),或以明确理由 **加入 allowlist**。 + +可执行检查: +- `bun run i18n:audit-placeholders` +- 如需在任意命中时让命令失败:添加 `--fail`。 + - `bun run i18n:audit-placeholders:fail` + +Allowlist(可审计、保持最小): +- `scripts/audit-settings-placeholders.allowlist.json` +- 支持的过滤器:`key`、`keyPrefix`、`keyRegex`、`valueRegex`,以及 `glossary` terms。 + +## Rule R2:必须保留 placeholders/tokens + +更新翻译时,**不要改动**: +- keys / JSON structure +- placeholder tokens(例如 `{name}`、`{count}`、`{resetTime}`) +- URL / command snippets(除非明确要翻译且已验证安全) + +建议的验证方式: +- `tests/unit/i18n/` 下的单元测试 +- 对受影响 locale 的 UI 页面做 spot-check(见 PR checklist) + +## Rule R3:Glossary 与术语一致性 + +维护一份简短 glossary,用于跨 locale 保持一致的术语(品牌、模型名、产品术语等)。 + +初始 glossary(按需扩展,但保持最小并经过 review): +- Provider / Model / API / HTTP/2 +- Claude / OpenAI / Codex(名称不翻译) + +## Rule R4:messages JSON 禁止 Emoji + +`messages/**/*.json` 中不允许出现 Emoji 字符。 + +可执行检查: +- `bun run i18n:audit-messages-no-emoji:fail` + +说明: +- 审计输出包含文件路径 + key path + Unicode codepoints(不会直接打印 Emoji 字符本身)。 + +## Notes + +- 优先修复翻译,而不是扩展 allowlists。 +- 每条 allowlist 记录都必须在 allowlist 文件中写清原因(最好在 PR 描述中也有引用)。 diff --git a/messages/en/index.ts b/messages/en/index.ts index d5bad5de2..70f0e4ca7 100644 --- a/messages/en/index.ts +++ b/messages/en/index.ts @@ -11,7 +11,7 @@ import notifications from "./notifications.json"; import providerChain from "./provider-chain.json"; import providers from "./providers.json"; import quota from "./quota.json"; -import settings from "./settings.json"; +import settings from "./settings"; import ui from "./ui.json"; import usage from "./usage.json"; import users from "./users.json"; diff --git a/messages/en/provider-chain.json b/messages/en/provider-chain.json index 43e2b9a8d..65549cb00 100644 --- a/messages/en/provider-chain.json +++ b/messages/en/provider-chain.json @@ -86,7 +86,7 @@ "remaining": "{count} attempts remaining", "status": "Status", "alreadyBroken": "Circuit already broken", - "circuitTriggered": "⚠️ Circuit breaker triggered", + "circuitTriggered": "Warning: Circuit breaker triggered", "errorDetails": "Error Details", "systemError": "Network/System Error", "systemErrorFailed": "Network/System Error (Attempt {attempt})", @@ -97,7 +97,7 @@ "errorMeaning": "Meaning: {meaning}", "meaning": "Meaning", "notCountedInCircuit": "This error is not counted in provider circuit breaker", - "systemErrorNote": "ℹ️ This error is not counted in provider circuit breaker", + "systemErrorNote": "Note: This error is not counted in provider circuit breaker", "reselection": "Reselecting Provider", "reselect": "Reselecting Provider", "excluded": "Excluded: {providers}", diff --git a/messages/en/settings.json b/messages/en/settings.json deleted file mode 100644 index 55c879dbd..000000000 --- a/messages/en/settings.json +++ /dev/null @@ -1,2176 +0,0 @@ -{ - "clientVersions": { - "description": "Manage client version requirements to ensure users use latest stable version. VSCode plugin and CLI are managed separately.", - "empty": { - "description": "No active users using recognizable clients in past 7 days", - "title": "No client data available" - }, - "features": { - "activeWindow": "Active Window: ", - "activeWindowDesc": "Only counts users with requests in the past 7 days", - "autoDetect": "System automatically detects the latest stable version (GA version) for each client type", - "blockOldVersion": "Users with old versions will receive HTTP 400 error and cannot continue using the service", - "errorMessage": "Error message includes current version and required upgrade version", - "gaRule": "GA Rule: ", - "gaRuleDesc": "A version is considered GA when used by more than 1 user", - "recommendation": "Recommendation: ", - "recommendationDesc": "Monitor the version distribution below and confirm new version stability before enabling.", - "title": "Feature Description", - "whatHappens": "What happens when enabled:" - }, - "section": { - "distribution": { - "description": "Shows client version info for active users in past 7 days. Each client type independently tracks GA versions.", - "title": "Client Version Distribution" - }, - "settings": { - "description": "When enabled, system automatically detects client version and blocks old version users.", - "title": "Update Reminder Settings" - } - }, - "table": { - "currentGA": "Current GA Version: ", - "internalType": "Internal Type: ", - "lastActive": "Last Active", - "latest": "Latest", - "needsUpgrade": "Needs Upgrade", - "noUsers": "No user data available", - "status": "Status", - "unknown": "Unknown", - "user": "User", - "usersCount": "{count} users", - "version": "Current Version" - }, - "title": "Client Update Reminder", - "toggle": { - "description": "When enabled, system will block requests from old version clients", - "disableSuccess": "Client version check disabled", - "enable": "Enable Update Reminder", - "enableSuccess": "Client version check enabled", - "toggleFailed": "Update failed" - } - }, - "common": { - "cancel": "Cancel", - "completed": "Completed", - "confirm": "Confirm", - "copied": "Key copied to clipboard", - "copy": "Copy", - "copyFailed": "Copy failed", - "create": "Create", - "creating": "Creating...", - "delete": "Delete", - "disabled": "Disabled", - "edit": "Edit", - "empty": "No matching results found", - "enabled": "Enabled", - "error": "Unknown error", - "failed": "Failed", - "loading": "Loading...", - "none": "None (No users using this version)", - "refresh": "Refresh", - "reset": "Reset", - "save": "Save", - "saving": "Saving...", - "submit": "Submit", - "success": "Success", - "test": "Test", - "testing": "Testing...", - "unlimited": "Unlimited", - "unlimited_desc": "Unlimited", - "update": "Update", - "updating": "Updating..." - }, - "config": { - "autoCleanup": "Auto Log Cleanup", - "autoCleanupDesc": "Automatically clean up historical log data on schedule to free up database storage space.", - "description": "Manage system basic parameters that affect site display and statistics behavior.", - "section": { - "siteParams": { - "title": "Site Parameters", - "description": "Configure site title, currency display unit, and dashboard statistics display policy." - }, - "autoCleanup": { - "title": "Auto Log Cleanup", - "description": "Automatically clean up historical log data on schedule to free up database storage space." - } - }, - "form": { - "allowGlobalView": "Allow Global Usage View", - "allowGlobalViewDesc": "When disabled, regular users can only view their own key usage statistics in the dashboard.", - "verboseProviderError": "Verbose Provider Error", - "verboseProviderErrorDesc": "When enabled, return detailed error messages when all providers are unavailable (including provider count, rate limit reasons, etc.); when disabled, only return a simple error code.", - "enableHttp2": "Enable HTTP/2", - "enableHttp2Desc": "When enabled, proxy requests will prefer HTTP/2 protocol. Automatically falls back to HTTP/1.1 on failure.", - "interceptAnthropicWarmupRequests": "Intercept Warmup Requests (Anthropic)", - "interceptAnthropicWarmupRequestsDesc": "When enabled, Claude Code warmup probe requests will be answered by CCH directly to avoid upstream provider calls; the request is logged for audit but is not billed, not rate-limited, and excluded from statistics.", - "enableThinkingSignatureRectifier": "Enable Thinking Signature Rectifier", - "enableThinkingSignatureRectifierDesc": "When Anthropic providers return thinking signature incompatibility or invalid request errors, automatically removes incompatible thinking blocks and retries once against the same provider (enabled by default).", - "enableResponseFixer": "Enable Response Fixer", - "enableResponseFixerDesc": "Automatically repairs common upstream response issues (encoding, SSE, truncated JSON). Enabled by default.", - "responseFixerFixEncoding": "Fix encoding issues", - "responseFixerFixEncodingDesc": "Removes BOM/null bytes and normalizes invalid UTF-8.", - "responseFixerFixSseFormat": "Fix SSE format", - "responseFixerFixSseFormatDesc": "Adds missing data: prefix, normalizes line endings, and fixes common field formatting.", - "responseFixerFixTruncatedJson": "Fix truncated JSON", - "responseFixerFixTruncatedJsonDesc": "Closes unclosed braces/quotes, removes trailing commas, and fills missing values with null when needed.", - "cleanupSchedule": "Cleanup Schedule", - "cleanupScheduleDesc": "Select the execution schedule for automatic cleanup", - "configUpdated": "System settings updated. The page will refresh to apply currency display changes.", - "currencyDisplay": "Currency Display Unit", - "currencyDisplayPlaceholder": "Select currency unit", - "currencyDisplayDesc": "After modification, all pages and API interfaces will use the corresponding currency symbol (symbol only, no exchange rate conversion).", - "keepDays": "Retention Days", - "keepDaysDesc": "Clean up logs older than this number of days", - "saveFailed": "Save failed", - "saveSuccess": "Saved successfully", - "saveError": "Save failed", - "saveSettings": "Save Settings", - "siteTitle": "Site Title", - "siteTitlePlaceholder": "e.g. Claude Code Hub", - "siteTitleRequired": "Site title cannot be empty", - "siteTitleDesc": "Used to set browser tab title and system default display name.", - "enableAutoCleanup": "Enable Auto Cleanup", - "enableAutoCleanupDesc": "Automatically clean up historical log data on schedule", - "cleanupRetentionDays": "Retention Days", - "cleanupRetentionDaysRequired": "Retention Days *", - "cleanupRetentionDaysPlaceholder": "30", - "cleanupRetentionDaysDesc": "Logs older than this number of days will be automatically cleaned (range: 1-365 days)", - "cleanupScheduleLabel": "Execution Time (Cron)", - "cleanupScheduleRequired": "Execution Time (Cron) *", - "cleanupSchedulePlaceholder": "0 2 * * *", - "cleanupScheduleCronDesc": "Cron expression, default: 0 2 * * * (2 AM daily)", - "cleanupScheduleCronExample": "Example: 0 3 * * 0 (3 AM every Sunday)", - "cleanupBatchSize": "Batch Size", - "cleanupBatchSizeRequired": "Batch Size *", - "cleanupBatchSizePlaceholder": "10000", - "cleanupBatchSizeDesc": "Number of records to delete per batch (range: 1000-100000, recommended 10000)", - "saveConfig": "Save Configuration", - "autoCleanupSaved": "Auto cleanup configuration saved", - "currencies": { - "USD": "$ US Dollar (USD)", - "CNY": "¥ Chinese Yuan (CNY)", - "EUR": "€ Euro (EUR)", - "JPY": "¥ Japanese Yen (JPY)", - "GBP": "£ British Pound (GBP)", - "HKD": "HK$ Hong Kong Dollar (HKD)", - "TWD": "NT$ New Taiwan Dollar (TWD)", - "KRW": "₩ South Korean Won (KRW)", - "SGD": "S$ Singapore Dollar (SGD)" - }, - "billingModelSource": "Billing Model Source", - "billingModelSourcePlaceholder": "Select billing model source", - "billingModelSourceDesc": "Configure which model to use for billing when model redirection occurs. 'Before Redirection' uses the original model requested by the user, 'After Redirection' uses the actual model called.", - "billingModelSourceOptions": { - "original": "Before Redirection (Original Model)", - "redirected": "After Redirection (Actual Model)" - } - }, - "siteSettings": "Site Parameters", - "siteSettingsDesc": "Configure site title, currency display unit, and dashboard statistics display policy.", - "title": "Basic Configuration" - }, - "data": { - "cleanup": { - "rangeLabel": "Cleanup Range", - "range": { - "7days": "Logs older than 1 week (7 days)", - "30days": "Logs older than 1 month (30 days)", - "90days": "Logs older than 3 months (90 days)", - "180days": "Logs older than 6 months (180 days)" - }, - "rangeDescription": { - "7days": "1 week ago", - "30days": "1 month ago", - "90days": "3 months ago", - "180days": "6 months ago", - "default": "{days} days ago" - }, - "willClean": "Will clean all log records from {range}", - "button": "Clean Logs", - "confirmTitle": "Confirm Log Cleanup", - "confirmWarning": "This operation will permanently delete all log records from {range} and cannot be recovered.", - "previewLoading": "Counting...", - "previewCount": "Will delete {count} log records", - "previewError": "Unable to get preview", - "statisticsRetained": "✓ Statistics data will be retained (for trend analysis)", - "logsDeleted": "✗ Log details will be deleted (request/response content, error info, etc.)", - "backupRecommendation": "Recommendation: Export database backup before cleanup in case recovery is needed.", - "cancel": "Cancel", - "confirm": "Confirm Cleanup", - "cleaning": "Cleaning...", - "successMessage": "Successfully cleaned {count} log records ({batches} batches, took {duration}s)", - "failed": "Cleanup failed", - "error": "Failed to clean logs", - "descriptionWarning": "Clean up historical log data to free up database storage. Note: Statistics data will be retained, but log details will be permanently deleted." - }, - "description": "Manage database backup and recovery with full data import/export and log cleanup.", - "export": { - "button": "Export Database", - "exporting": "Exporting...", - "successMessage": "Database exported successfully!", - "failed": "Export failed", - "error": "Failed to export database", - "descriptionFull": "Export complete database backup file (.dump format) for data migration or recovery. Backup uses PostgreSQL custom format, auto-compressed and compatible with different database versions." - }, - "guide": { - "title": "Usage Instructions and Precautions", - "items": { - "cleanup": { - "title": "Log Cleanup", - "description": "Physically delete historical log data (irreversible). Statistics table will be retained. Recommend exporting database backup before cleanup." - }, - "format": { - "title": "Backup Format", - "description": "Uses PostgreSQL custom format (.dump), auto-compressed and compatible with different database versions." - }, - "overwrite": { - "title": "Overwrite Mode", - "description": "Deletes all existing data before importing, ensuring database matches backup exactly. Best for complete recovery." - }, - "merge": { - "title": "Merge Mode", - "description": "Retains existing data and attempts to insert backup data. Primary key conflicts may cause import failure." - }, - "safety": { - "title": "Security Recommendation", - "description": "Before importing, recommend exporting current database as backup to avoid data loss." - }, - "environment": { - "title": "Environment Requirements", - "description": "This feature requires Docker Compose deployment. Local development may not support it." - } - } - }, - "import": { - "selectFileLabel": "Select Backup File", - "fileSelected": "Selected: {name} ({size} MB)", - "fileError": "Please select .dump format backup file", - "noFileSelected": "Please select backup file first", - "cleanFirstLabel": "Clear existing data (overwrite mode)", - "cleanFirstDescription": "Delete all existing data before importing to ensure database matches backup exactly. If unchecked, will attempt to merge data but may fail due to primary key conflicts.", - "button": "Import Database", - "importing": "Importing...", - "progressTitle": "Import Progress", - "confirmTitle": "Confirm Database Import", - "confirmOverwrite": "You selected 'Overwrite Mode', which will delete all existing data before importing backup.", - "confirmMerge": "You selected 'Merge Mode', which will attempt to import backup while keeping existing data.", - "warningOverwrite": "⚠️ Warning: This action is irreversible, all current data will be permanently deleted!", - "warningMerge": "⚠️ Note: Import may fail if primary key conflicts exist.", - "backupFile": "Backup file:", - "backupRecommendation": "Recommend exporting current database as backup before proceeding.", - "cancel": "Cancel", - "confirm": "Confirm Import", - "successMessage": "Data import completed!", - "successCleanModeDesc": "All data has been successfully restored. Refresh your browser if the page displays incorrectly.", - "successMergeModeDesc": "Data has been successfully imported and merged. Refresh your browser if the page displays incorrectly.", - "successWithWarnings": "Data import completed (with warnings)", - "successWithWarningsDesc": "Data has been successfully imported, but some existing objects were skipped. Refresh your browser or restart the application if the page displays incorrectly.", - "failedMessage": "Data import failed", - "error": "Failed to import database", - "streamError": "Cannot read response stream", - "streamInterrupted": "Data stream unexpectedly interrupted", - "streamInterruptedDesc": "Import progress did not complete normally. Please check the logs and verify data integrity. Re-import if needed.", - "parseError": "Failed to parse response data", - "errorUnknown": "Unknown error", - "descriptionFull": "Restore database from backup file. Supports PostgreSQL custom format (.dump) backup files." - }, - "status": { - "loading": "Loading...", - "error": "Failed to get database status", - "retry": "Retry", - "connected": "Database connected", - "unavailable": "Database unavailable", - "tables": "{count} tables" - }, - "title": "Data Management", - "section": { - "status": { - "title": "Database Status", - "description": "View current database connection status and basic information." - }, - "cleanup": { - "title": "Log Cleanup", - "description": "Clean up historical log data to free up database storage. Note: Statistics data will be retained, but log details will be permanently deleted." - }, - "export": { - "title": "Data Export", - "description": "Export complete database backup file (.dump format) for data migration or recovery." - }, - "import": { - "title": "Data Import", - "description": "Restore database from backup file. Supports PostgreSQL custom format (.dump) backup files." - } - } - }, - "errors": { - "saveSuccess": "Save succeeded", - "saveFailed": "Save failed", - "saveFailed_error": "Failed to save settings", - "addSuccess": "Add succeeded", - "addFailed": "Failed to add provider", - "editSuccess": "Update succeeded", - "editFailed": "Failed to update provider", - "deleteSuccess": "Delete succeeded", - "deleteFailed": "Failed to delete provider", - "syncSuccess": "Sync succeeded", - "syncFailed": "Sync failed", - "testFailed": "Test failed", - "testFailedRetry": "Test failed, please retry", - "loadFailed": "Failed to load notification settings", - "unknownError": "An exception occurred during the operation" - }, - "logs": { - "description": "Dynamically adjust system log level to control logging verbosity in real-time.", - "subtitle": "Log Level Control", - "subtitleDesc": "Changes take effect immediately without restart. Useful for troubleshooting in production.", - "section": { - "title": "Log Level Control", - "description": "Changes take effect immediately without service restart." - }, - "levels": { - "fatal": { - "label": "Fatal", - "description": "Fatal errors only" - }, - "error": { - "label": "Error", - "description": "Error messages" - }, - "warn": { - "label": "Warn", - "description": "Warnings + Errors" - }, - "info": { - "label": "Info", - "description": "Key business events + Warnings + Errors (Recommended for Production)" - }, - "debug": { - "label": "Debug", - "description": "Debug info + All levels (Recommended for Development)" - }, - "trace": { - "label": "Trace", - "description": "Extremely detailed tracing + All levels" - } - }, - "form": { - "currentLevel": "Current Log Level", - "selectLevel": "Select Log Level", - "save": "Save Settings", - "saving": "Saving...", - "success": "Log level set to: {level}", - "failed": "Failed to set", - "failedError": "Failed to set log level", - "fetchFailed": "Failed to fetch log level", - "effectiveImmediately": "Log level changes take effect immediately without service restart.", - "levelGuideTitle": "Log Level Guide", - "levelGuideFatal": "Fatal/Error: Only errors shown, minimal logging, suitable for high-load production", - "levelGuideWarn": "Warn: Includes warnings (rate limiting, circuit breaker opening, etc.) + Errors", - "levelGuideInfo": "Info (Recommended for Production): Shows key business events (provider selection, Session reuse, price sync) + Warnings + Errors", - "levelGuideDebug": "Debug (Recommended for Development): Includes detailed debug info, suitable for troubleshooting", - "levelGuideTrace": "Trace: Extremely detailed trace information, includes all details", - "changeNotice": "Current level is {current}, will switch to {selected} after saving" - }, - "title": "Log Management" - }, - "nav": { - "apiDocs": "API Docs", - "clientVersions": "Updates", - "config": "Config", - "data": "Data", - "errorRules": "Errors", - "feedback": "Feedback", - "docs": "Documentation", - "logs": "Logs", - "notifications": "Notifications", - "prices": "Pricing", - "providers": "Providers", - "sensitiveWords": "Filters", - "requestFilters": "Requests" - }, - "notifications": { - "title": "Push Notifications", - "description": "Configure Webhook push notifications", - "global": { - "title": "Notification Master Switch", - "description": "Enable or disable all push notification features", - "enable": "Enable Push Notifications", - "legacyModeTitle": "Legacy Mode", - "legacyModeDescription": "You are using legacy single-URL notifications. Create a push target to switch to multi-target mode." - }, - "targets": { - "title": "Push Targets", - "description": "Manage push targets. Supports WeCom, Feishu, DingTalk, Telegram and custom Webhook.", - "add": "Add Target", - "update": "Save Target", - "edit": "Edit", - "delete": "Delete", - "deleteConfirmTitle": "Delete Push Target", - "deleteConfirm": "Are you sure you want to delete this target? Related bindings will also be removed.", - "enable": "Enable Target", - "statusEnabled": "Enabled", - "statusDisabled": "Disabled", - "lastTestAt": "Last Test", - "lastTestNever": "Never tested", - "lastTestSuccess": "Test OK", - "lastTestFailed": "Test Failed", - "test": "Test", - "testSelectType": "Select test type", - "emptyHint": "No push targets yet. Click \"Add Target\" to create one.", - "created": "Target created", - "updated": "Target updated", - "deleted": "Target deleted", - "bindingsSaved": "Bindings saved" - }, - "targetDialog": { - "createTitle": "Add Push Target", - "editTitle": "Edit Push Target", - "name": "Target Name", - "namePlaceholder": "e.g. Ops Group", - "type": "Platform Type", - "selectType": "Select platform type", - "enable": "Enable", - "webhookUrl": "Webhook URL", - "webhookUrlPlaceholder": "https://example.com/webhook", - "telegramBotToken": "Telegram Bot Token", - "telegramBotTokenPlaceholder": "e.g. 123456:ABCDEF...", - "telegramChatId": "Telegram Chat ID", - "telegramChatIdPlaceholder": "e.g. -1001234567890", - "dingtalkSecret": "DingTalk Secret", - "dingtalkSecretPlaceholder": "Optional, used for signing", - "customHeaders": "Custom Headers (JSON)", - "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", - "errors": { - "headersInvalidJson": "Headers must be valid JSON", - "headersMustBeObject": "Headers must be a JSON object", - "headersValueMustBeString": "Header values must be strings" - }, - "types": { - "wechat": "WeCom", - "feishu": "Feishu", - "dingtalk": "DingTalk", - "telegram": "Telegram", - "custom": "Custom Webhook" - }, - "proxy": { - "title": "Proxy", - "toggle": "Toggle proxy settings", - "url": "Proxy URL", - "urlPlaceholder": "http://127.0.0.1:7890", - "fallbackToDirect": "Fallback to direct on proxy failure" - } - }, - "bindings": { - "title": "Bindings", - "noTargets": "No push targets available.", - "bindTarget": "Bind target", - "enable": "Enable", - "enableType": "Enable this notification", - "advanced": "Advanced", - "scheduleCron": "Cron", - "scheduleCronPlaceholder": "e.g. 0 9 * * *", - "scheduleTimezone": "Timezone", - "templateOverride": "Template Override", - "editTemplateOverride": "Edit Override", - "templateOverrideTitle": "Edit Template Override", - "boundCount": "Bound: {count}", - "enabledCount": "Enabled: {count}" - }, - "templateEditor": { - "title": "Template (JSON)", - "placeholder": "Enter JSON template...", - "jsonInvalid": "Invalid JSON", - "placeholders": "Placeholders", - "insert": "Insert" - }, - "circuitBreaker": { - "title": "Circuit Breaker Alert", - "description": "Send alert immediately when provider is fully circuit broken", - "enable": "Enable Circuit Breaker Alert", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "test": "Test Connection" - }, - "dailyLeaderboard": { - "title": "Daily User Consumption Leaderboard", - "description": "Send daily scheduled user consumption Top N leaderboard", - "enable": "Enable Daily Leaderboard", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "time": "Send Time", - "timePlaceholder": "09:00", - "timeError": "Time format error, should be HH:mm", - "topN": "Show Top N", - "test": "Test Connection" - }, - "costAlert": { - "title": "Cost Alert", - "description": "Trigger alert when user/provider consumption exceeds quota threshold", - "enable": "Enable Cost Alert", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "webhookTypeWeCom": "WeCom", - "webhookTypeFeishu": "Feishu", - "webhookTypeUnknown": "Unknown platform. Please use WeCom or Feishu webhook URL", - "threshold": "Alert Threshold", - "thresholdLabel": "Alert Threshold: {percent}%", - "thresholdHelp": "Alert when consumption reaches {percent}% of quota", - "interval": "Check Interval (minutes)", - "test": "Test Connection" - }, - "form": { - "save": "Save Settings", - "saving": "Saving...", - "loading": "Loading...", - "success": "Notification settings saved and tasks rescheduled", - "saveFailed": "Save failed", - "saveError": "Failed to save settings", - "loadError": "Failed to load notification settings", - "webhookRequired": "Please fill in Webhook URL first", - "testSuccess": "Test message sent", - "testFailed": "Test failed", - "testFailedRetry": "Test failed, please retry", - "testError": "Test connection failed", - "testNoResult": "Test succeeded but no result returned" - } - }, - "prices": { - "title": "Pricing", - "description": "Manage platform basic configuration and model pricing", - "section": { - "title": "Model Pricing", - "description": "Manage AI model pricing configuration" - }, - "searchPlaceholder": "Search model name...", - "filters": { - "all": "All", - "local": "Local", - "anthropic": "Anthropic", - "openai": "OpenAI", - "vertex": "Vertex" - }, - "badges": { - "local": "Local" - }, - "capabilities": { - "assistantPrefill": "Assistant prefill", - "computerUse": "Computer use", - "functionCalling": "Function calling", - "pdfInput": "PDF input", - "promptCaching": "Prompt caching", - "reasoning": "Reasoning", - "responseSchema": "Response schema", - "toolChoice": "Tool choice", - "vision": "Vision", - "statusSupported": "Supported", - "statusUnsupported": "Not supported", - "tooltip": "{label}: {status}" - }, - "sync": { - "button": "Sync Cloud Price Table", - "syncing": "Syncing...", - "checking": "Checking conflicts...", - "successWithChanges": "Price table updated: {added} added, {updated} updated, {unchanged} unchanged", - "successNoChanges": "Price table is up to date, no updates needed", - "failed": "Sync failed", - "failedError": "Sync failed: {error}", - "failedNoResult": "Price table updated but no result returned", - "noModels": "No model prices found", - "partialFailure": "Partial update succeeded, but {failed} models failed", - "failedModels": "Failed models: {models}", - "skippedConflicts": "Skipped {count} manual models" - }, - "conflict": { - "title": "Select Items to Overwrite", - "description": "The following models have manual prices. Check the ones to overwrite with LiteLLM prices, unchecked ones will be kept unchanged", - "searchPlaceholder": "Search models...", - "table": { - "modelName": "Model", - "manualPrice": "Manual Price", - "litellmPrice": "LiteLLM Price", - "action": "Action" - }, - "viewDiff": "View Diff", - "diffTitle": "Price Difference", - "diff": { - "field": "Field", - "manual": "Manual", - "litellm": "LiteLLM", - "inputPrice": "Input Price", - "outputPrice": "Output Price", - "imagePrice": "Image Price", - "provider": "Provider", - "mode": "Type" - }, - "pagination": { - "showing": "Showing {from}-{to} of {total}" - }, - "selectedCount": "Selected {count}/{total} models", - "noMatch": "No matching models found", - "noConflicts": "No conflicts", - "applyOverwrite": "Apply Overwrite", - "applying": "Applying..." - }, - "table": { - "modelName": "Model Name", - "provider": "Provider", - "capabilities": "Capabilities", - "price": "Price", - "inputPrice": "Input Price ($/M)", - "outputPrice": "Output Price ($/M)", - "priceInput": "In", - "priceOutput": "Out", - "pricePerRequest": "Req", - "cacheReadPrice": "Cache Read ($/M)", - "cacheCreationPrice": "Cache Create ($/M)", - "cache5m": "5m", - "cache1h": "1h+", - "copyModelId": "Copy model ID", - "updatedAt": "Updated At", - "actions": "Actions", - "typeChat": "Chat", - "typeImage": "Image", - "typeCompletion": "Completion", - "typeUnknown": "Unknown", - "loading": "Loading...", - "noMatch": "No matching models found", - "noDataTitle": "No price data available", - "noDataHint": "System has built-in price table. Use buttons above to sync or update." - }, - "pagination": { - "showing": "Showing {from}-{to} of {total}", - "previous": "Previous", - "next": "Next", - "perPageLabel": "Per page", - "perPage": "{size} per page" - }, - "stats": { - "totalModels": "{count} models total", - "searchResults": "{count} search results", - "lastUpdated": "Last updated: {time}" - }, - "dialog": { - "title": "Update Model Price Table", - "description": "Select and upload JSON or TOML file containing model pricing data", - "selectFile": "Click to select JSON/TOML file or drag and drop here", - "fileSizeLimit": "File size cannot exceed 10MB", - "fileSizeLimitSmall": "File size not exceeding 10MB", - "invalidFileType": "Please select a JSON or TOML file", - "fileTooLarge": "File size exceeds 10MB limit", - "upload": "Upload and Update", - "uploading": "Uploading...", - "updatePriceTable": "Update Price Table", - "updating": "Updating...", - "selectJson": "Select File", - "updateSuccess": "Price table updated successfully, {count} models updated", - "updateFailed": "Update failed", - "systemHasBuiltIn": "System has built-in price table", - "manualDownload": "You can also manually download", - "latestPriceTable": "cloud price table", - "andUploadViaButton": ", and upload via button above", - "cloudModelCountLoading": "Loading cloud model count...", - "cloudModelCountFailed": "Failed to load cloud model count", - "supportedModels": "Currently supports {count} models", - "results": { - "title": "Update Results", - "total": "Total: {total} models", - "success": "Success: {success}", - "failed": "Failed: {failed}", - "skipped": "Skipped: {skipped}", - "more": " (+{count})", - "details": "Details", - "viewDetails": "View detailed logs" - } - }, - "addModel": "Add Model", - "editModel": "Edit Model", - "deleteModel": "Delete Model", - "addModelDescription": "Manually add a new model price configuration", - "editModelDescription": "Edit the model price configuration", - "deleteConfirm": "Are you sure you want to delete model {name}? This action cannot be undone.", - "form": { - "modelName": "Model ID", - "modelNamePlaceholder": "e.g., gpt-5.2-codex", - "modelNameRequired": "Model ID is required", - "displayName": "Display Name (Optional)", - "displayNamePlaceholder": "e.g., GPT-5.2 Codex", - "type": "Type", - "provider": "Provider", - "providerPlaceholder": "e.g., openai", - "requestPrice": "Per-call Price ($/request)", - "inputPrice": "Input Price ($/M tokens)", - "outputPrice": "Output Price ($/M tokens)", - "outputPriceImage": "Output Price ($/image)", - "cacheReadPrice": "Cache Read Price ($/M tokens)", - "cacheCreationPrice5m": "Cache Creation Price (5m, $/M tokens)", - "cacheCreationPrice1h": "Cache Creation Price (1h+, $/M tokens)" - }, - "drawer": { - "prefillLabel": "Search existing models to prefill", - "prefillEmpty": "No matching models found", - "prefillFailed": "Search failed", - "promptCachingHint": "Enable if the model supports prompt caching", - "cachePricingTitle": "Cache Pricing" - }, - "actions": { - "edit": "Edit", - "more": "More actions", - "delete": "Delete" - }, - "toast": { - "createSuccess": "Model added", - "updateSuccess": "Model updated", - "deleteSuccess": "Model deleted", - "saveFailed": "Failed to save", - "deleteFailed": "Failed to delete" - } - }, - "providers": { - "add": "Add Provider", - "addFailed": "Failed to add provider", - "addProvider": "Add Provider", - "addSuccess": "Provider added successfully", - "autoSort": { - "button": "Auto Sort Priority", - "dialogTitle": "Auto Sort Provider Priority", - "dialogDescription": "Automatically assign priority based on cost multiplier (lower cost = higher priority)", - "changeCount": "{count} providers will be updated", - "noChanges": "No changes needed (already sorted)", - "costMultiplierHeader": "Cost Multiplier", - "priorityHeader": "Priority", - "providersHeader": "Providers", - "changesTitle": "Change Details", - "providerHeader": "Provider", - "priorityChangeHeader": "Priority Change", - "confirm": "Apply Changes", - "success": "Updated priority for {count} providers", - "error": "Failed to update priorities" - }, - "types": { - "claude": { - "label": "Claude", - "description": "Anthropic Official API" - }, - "claudeAuth": { - "label": "Claude Auth", - "description": "Claude Relay Service" - }, - "codex": { - "label": "Codex", - "description": "Codex CLI API" - }, - "gemini": { - "label": "Gemini", - "description": "Google Gemini API" - }, - "geminiCli": { - "label": "Gemini CLI", - "description": "Gemini CLI API" - }, - "openaiCompatible": { - "label": "OpenAI Compatible", - "description": "OpenAI Compatible API" - } - }, - "list": { - "priority": "Priority", - "weight": "Weight", - "costMultiplier": "Cost Multiplier", - "todayUsageLabel": "Today's Usage", - "todayUsageCount": "{count} times", - "circuitBroken": "Circuit Broken", - "officialWebsite": "Official", - "viewFullKey": "View Complete API Key", - "viewFullKeyDesc": "Please keep it safe and don't share it with others", - "keyLoading": "Loading...", - "confirmDeleteTitle": "Confirm Delete Provider?", - "confirmDeleteMessage": "Are you sure you want to delete provider \"{name}\"? This action cannot be undone.", - "deleteButton": "Delete", - "cancelButton": "Cancel", - "deleteSuccess": "Deleted successfully", - "deleteSuccessDesc": "Provider \"{name}\" has been deleted", - "deleteFailed": "Delete failed", - "deleteError": "An error occurred during operation", - "unknownError": "Unknown error", - "getKeyFailed": "Failed to get key", - "keyCopied": "Key copied to clipboard", - "copyFailed": "Copy failed", - "clipboardUnavailable": "Clipboard access is blocked in this environment. Select and copy the key manually.", - "resetCircuitSuccess": "Circuit breaker reset", - "resetCircuitSuccessDesc": "Provider \"{name}\" circuit breaker status cleared", - "resetCircuitFailed": "Failed to reset circuit breaker", - "resetUsageTitle": "Reset total usage", - "resetUsageSuccess": "Total usage reset", - "resetUsageSuccessDesc": "Provider \"{name}\" total usage has been reset", - "resetUsageFailed": "Failed to reset total usage", - "toggleSuccess": "Provider {status}", - "toggleSuccessDesc": "Provider \"{name}\" status updated", - "toggleFailed": "Toggle failed", - "statusEnabled": "enabled", - "statusDisabled": "disabled" - }, - "inlineEdit": { - "save": "Save", - "cancel": "Cancel", - "saveSuccess": "Saved successfully", - "saveFailed": "Save failed", - "priorityLabel": "Priority", - "weightLabel": "Weight", - "costMultiplierLabel": "Cost Multiplier", - "priorityInvalid": "Please enter an integer >= 0", - "weightInvalid": "Please enter an integer between 1 and 100", - "costMultiplierInvalid": "Please enter a non-negative number" - }, - "schedulingDialog": { - "title": "Provider Scheduling Rules", - "description": "Understand how the system intelligently selects upstream providers for high availability and cost optimization", - "triggerButton": "Rules", - "step": "Step", - "before": "Before:", - "after": "After:", - "decision": "Decision:" - }, - "circuitBroken": "Circuit Broken", - "clone": "Clone Provider", - "cloneFailed": "Copy failed", - "confirmDelete": "Are you sure you want to delete this provider?", - "confirmDeleteDesc": "Are you sure you want to delete provider \"{name}\"? This action cannot be undone.", - "confirmDeleteProvider": "Confirm Delete Provider?", - "confirmDeleteProviderDesc": "Are you sure you want to delete provider \"{name}\"? This action is irreversible.", - "createProvider": "Add Provider", - "delete": "Delete Provider", - "deleteFailed": "Failed to delete provider", - "deleteSuccess": "Deleted successfully", - "description": "Configure API service providers and maintain availability status.", - "disabledStatus": "disabled", - "displayCount": "Showing {filtered} / {total} providers", - "edit": "Edit Provider", - "editFailed": "Failed to update provider", - "editProvider": "Edit Provider", - "enabledStatus": "enabled", - "form": { - "apiTest": { - "fillUrlFirst": "Please fill in provider URL first", - "invalidUrl": "Provider URL is invalid (http/https only)", - "fillKeyFirst": "Please fill in API key first", - "testFailed": "Test failed", - "testFailedRetry": "Test failed, please retry", - "noResult": "Test succeeded but no result returned", - "testSuccess": "Model test succeeded", - "testApi": "Provider Model Test", - "testing": "Testing...", - "apiFormat": "Provider type", - "selectApiFormat": "Select provider type to test", - "apiFormatDesc": "Defaults to the routing configuration unless manually changed", - "formatAnthropicMessages": "Claude (Anthropic Messages API)", - "formatOpenAIChat": "OpenAI Compatible", - "formatOpenAIResponses": "Codex (Response API)", - "testModel": "Test model", - "testModelDesc": "Leave empty to use the default model or type one manually", - "requestConfig": "Request Configuration", - "presetConfig": "Preset", - "customConfig": "Custom", - "selectPreset": "Select preset template", - "presetDesc": "Preset templates contain authentic CLI request patterns for relay service verification", - "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", - "customPayloadDesc": "Enter custom JSON payload to override default request body", - "successContains": "Success Keyword", - "successContainsPlaceholder": "pong", - "successContainsDesc": "Response must contain this keyword to be considered successful", - "model": "Model", - "responseModel": "Response model", - "responseTime": "Response time", - "usage": "Token usage", - "response": "Response preview", - "error": "Error message", - "unknown": "Unknown", - "viewDetails": "View Details", - "copySuccess": "Copied to clipboard", - "copyFailed": "Failed to copy", - "copyResult": "Copy Result", - "close": "Close", - "success": "Success", - "failed": "Failed", - "streamInfo": "Stream response info", - "chunksReceived": "Chunks received", - "streamFormat": "Stream format", - "streamResponse": "Stream response", - "chunksCount": "Received {count} chunks ({format})", - "truncatedPreview": "Showing first {length} characters, copy to see full content", - "truncatedBrief": "Showing first {length} characters, click \"View Details\" for more", - "timeout": { - "label": "Timeout (seconds)", - "desc": "Max wait time for test request (5-120 sec)", - "geminiHint": ", Gemini Thinking models recommend 60+ sec" - }, - "geminiAuthFallback": { - "warning": "Header auth failed, using URL parameter auth", - "desc": "Actual proxy forwarding only uses header auth, may cause request failures" - }, - "copyFormat": { - "testResult": "Test result", - "message": "Message", - "errorDetails": "Error details" - }, - "disclaimer": { - "title": "Notice", - "resultReference": "[IMPORTANT] Results may vary by provider and are for reference only", - "realRequest": "This test sends a real request to the provider and may consume a small quota", - "confirmConfig": "Please verify provider URL, API key, and model configuration" - }, - "resultCard": { - "status": { - "green": "Available", - "yellow": "Degraded", - "red": "Unavailable" - }, - "dialogTitle": "Provider Test Details", - "validation": { - "title": "Three-tier Validation Details", - "http": { - "title": "Tier 1: HTTP Status", - "statusCode": "Status Code", - "passed": "2xx/3xx Success", - "failed": "4xx/5xx Failed" - }, - "latency": { - "title": "Tier 2: Latency Threshold", - "actual": "Actual Latency", - "passed": "Within threshold", - "failed": "Exceeded threshold" - }, - "content": { - "title": "Tier 3: Content Validation", - "target": "Target", - "passed": "Contains target string", - "failed": "Target not found" - }, - "passed": "Passed", - "failed": "Failed", - "timeout": "Timeout" - }, - "labels": { - "http": "HTTP", - "latency": "Latency", - "content": "Content", - "model": "Model", - "firstByte": "First Byte", - "totalLatency": "Total Latency", - "error": "Error", - "responsePreview": "Response Preview" - }, - "timing": { - "title": "Timing Info", - "totalLatency": "Total Latency", - "firstByte": "First Byte", - "testedAt": "Tested At" - }, - "tokenUsage": { - "title": "Token Usage", - "input": "Input", - "output": "Output", - "cacheCreation": "Cache Creation", - "cacheRead": "Cache Read" - }, - "streamInfo": { - "title": "Stream Response Info", - "isStreaming": "Streaming", - "chunksCount": "Chunks Count", - "yes": "Yes", - "no": "No" - }, - "rawResponse": { - "title": "Raw Response Body", - "hint": "This shows the raw response content. You can check if the keyword exists in the response here." - }, - "errorDetails": { - "title": "Error Details", - "type": "Error Type" - }, - "copyText": { - "status": "Status", - "message": "Message", - "latency": "Latency", - "httpStatus": "HTTP Status", - "model": "Model", - "usage": "Usage", - "inputOutput": "Input {input} / Output {output} tokens", - "response": "Response", - "error": "Error", - "testedAt": "Tested At", - "validationDetails": "Validation Details", - "httpCheck": "HTTP Check", - "latencyCheck": "Latency Check", - "contentCheck": "Content Check" - }, - "judgment": "Judgment" - } - }, - "proxyTest": { - "fillUrlFirst": "Please fill in provider URL first", - "testFailed": "Test failed", - "testFailedRetry": "Test failed, please retry", - "noResult": "Test succeeded but no result returned", - "connectionSuccess": "Connection successful", - "connectionFailed": "Connection failed", - "viaProxy": "(via proxy)", - "viaDirect": "(direct)", - "responseTime": "Response time:", - "statusCode": "Status code:", - "connectionMethod": "Connection method:", - "proxy": "Proxy", - "direct": "Direct", - "errorType": "Error type:", - "testing": "Testing...", - "testConnection": "Test Connection", - "timeoutError": "Connection timeout (5s). Please check:\n1. Is proxy server accessible\n2. Are proxy address and port correct\n3. Are proxy credentials correct", - "proxyError": "Proxy error:", - "networkError": "Network error:" - }, - "urlPreview": { - "title": "URL Concatenation Preview", - "invalidUrl": "Invalid URL format", - "invalidUrlDesc": "Please enter a valid HTTP/HTTPS address", - "duplicatePath": "Duplicate path detected", - "copy": "Copy", - "copySuccess": "Copied {name} to clipboard", - "copyFailed": "Copy failed" - }, - "modelSelect": { - "allowAllModels": "Allow all {type} models", - "selectedCount": "Selected {count} models", - "searchPlaceholder": "Search model name...", - "loading": "Loading...", - "notFound": "Model not found", - "selectAll": "Select All ({count})", - "clear": "Clear", - "manualAdd": "Manually Add Model", - "manualPlaceholder": "Enter model name (e.g. gpt-5-turbo)", - "manualDesc": "Support adding any model name (not limited to price table)", - "claude": "Claude", - "openai": "OpenAI", - "gemini": "Gemini", - "sourceUpstream": "Upstream", - "sourceUpstreamDesc": "Model list from upstream provider API", - "sourceFallback": "Local", - "sourceFallbackDesc": "Using local price list (upstream unavailable or unsupported)", - "refresh": "Refresh model list" - }, - "modelRedirect": { - "currentRules": "Current Rules ({count})", - "addNewRule": "Add New Rule", - "sourceModel": "User Requested Model", - "targetModel": "Actual Forwarded Model", - "sourcePlaceholder": "e.g. claude-sonnet-4-5-20250929", - "targetPlaceholder": "e.g. glm-4.6", - "add": "Add", - "sourceEmpty": "Source model name cannot be empty", - "targetEmpty": "Target model name cannot be empty", - "alreadyExists": "Model \"{model}\" already has a redirect rule", - "description": "Redirect Claude Code client requested models (e.g. claude-sonnet-4.5) to upstream provider supported models (e.g. glm-4.6, gemini-pro). For cost optimization or third-party AI integration.", - "emptyState": "No redirect rules yet. After adding rules, the system will automatically rewrite model names in requests." - }, - "addRedirect": "Add Redirect", - "allowAllModels": "✓ Allow All Models (Recommended)", - "apiAddress": "API Address", - "apiAddressPlaceholder": "e.g. https://open.bigmodel.cn/api/anthropic", - "apiAddressRequired": "API Address *", - "apiKey": "API Key", - "apiKeyCurrent": "Current key:", - "apiKeyLeaveEmpty": "(Leave empty to keep unchanged)", - "apiKeyLeaveEmptyDesc": "Leave empty to keep existing key", - "apiKeyOptional": "Leave empty to keep existing key", - "apiKeyPlaceholder": "Enter API key", - "apiKeyRequired": "API Key *", - "baseUrl": "Base URL", - "baseUrlPlaceholder": "e.g. https://open.bigmodel.cn/api/anthropic", - "baseUrlRequired": "Please fill in provider URL first", - "circuitBreakerConfig": "Circuit Breaker Configuration", - "circuitBreakerConfigSummary": "{failureThreshold} failures / {openDuration} min circuit break / {successThreshold} successes to recover / {maxRetryAttempts} attempts per provider", - "circuitBreakerDesc": "Auto circuit break on consecutive failures to avoid overall service quality impact", - "clearSearch": "Clear search", - "codexInstructions": "Codex Instructions Policy", - "codexInstructionsAuto": "Auto (Recommended)", - "codexInstructionsDesc": "(determines scheduling policy)", - "codexInstructionsForce": "Force Official", - "codexInstructionsKeep": "Keep Original", - "codexStrategyAutoDesc": "Pass through client instructions, auto retry with official prompt on 400 error", - "codexStrategyAutoLabel": "Auto (Recommended)", - "codexStrategyConfig": "Codex Instructions Strategy", - "codexStrategyConfigAuto": "Auto (Recommended)", - "codexStrategyConfigForce": "Force Official", - "codexStrategyConfigKeep": "Keep Original", - "codexStrategyDesc": "Control how to handle Codex request instructions field, affects upstream gateway compatibility", - "codexStrategyForceDesc": "Always use official Codex CLI instructions (~4000+ chars)", - "codexStrategyForceLabel": "Force Official", - "codexStrategyHint": "Hint: Some strict Codex gateways (e.g. 88code, foxcode) require official instructions. Choose \"Auto\" or \"Force Official\" strategy", - "mcpPassthroughConfig": "MCP Passthrough Configuration", - "mcpPassthroughConfigMinimax": "Minimax", - "mcpPassthroughConfigGlm": "GLM", - "mcpPassthroughConfigCustom": "Custom (Reserved)", - "mcpPassthroughConfigNone": "Disabled", - "mcpPassthroughDesc": "When enabled, pass through MCP tool calls to specified AI provider (e.g. minimax for image recognition, web search)", - "mcpPassthroughSelect": "Passthrough Type", - "mcpPassthroughNoneLabel": "Disabled", - "mcpPassthroughNoneDesc": "Do not enable MCP passthrough (default)", - "mcpPassthroughMinimaxLabel": "Minimax", - "mcpPassthroughMinimaxDesc": "Pass through to minimax MCP service (supports image recognition, web search, etc.)", - "mcpPassthroughGlmLabel": "GLM", - "mcpPassthroughGlmDesc": "Pass through to GLM MCP service (supports image analysis, video analysis, etc.)", - "mcpPassthroughCustomLabel": "Custom", - "mcpPassthroughCustomDesc": "Pass through to custom MCP service (reserved, not implemented yet)", - "mcpPassthroughHint": "Hint: MCP passthrough allows Claude Code client to use tool capabilities provided by third-party AI providers (e.g. image recognition, web search)", - "codexStrategyKeepDesc": "Always pass through client instructions, no auto retry (for lenient gateways)", - "codexStrategyKeepLabel": "Keep Original", - "codexStrategySelect": "Strategy Selection", - "collapseAll": "Collapse All Advanced Configuration", - "confirmAdd": "Confirm Add", - "confirmAddPending": "Adding...", - "confirmUpdate": "Confirm Update", - "confirmUpdatePending": "Updating...", - "costMultiplier": "Cost Multiplier", - "costMultiplierDesc": "Cost calculation multiplier. Official=1.0, 20% cheaper=0.8, 20% more expensive=1.2 (up to 4 decimal places)", - "costMultiplierLabel": "Cost Multiplier", - "costMultiplierPlaceholder": "1.0", - "deleteButton": "Delete", - "enabled": "Enabled", - "expandAll": "Expand All Advanced Configuration", - "failureThreshold": "Failure Threshold (times)", - "failureThresholdDesc": "How many consecutive failures trigger circuit break", - "failureThresholdPlaceholder": "5", - "filterAllProviders": "All Providers", - "filterByType": "Filter by Provider Type", - "filterProvider": "Filter by Provider Type", - "group": "Group", - "groupPlaceholder": "e.g. premium, economy", - "joinClaudePool": "Join Claude Scheduling Pool", - "joinClaudePoolDesc": "When enabled, this provider will participate in load balancing with Claude type providers", - "joinClaudePoolHelp": "Only available when model redirect config contains mappings to claude-* models. When enabled, this provider will also participate in scheduling when users request claude-* models.", - "leaveEmpty": "Leave empty for unlimited", - "limit0Means": "0 means unlimited", - "limit5hLabel": "5-Hour Spending Limit (USD)", - "limitAmount5h": "5-Hour Spending Limit (USD)", - "limitAmount5hDesc": "e.g. Provider B has $10 limit, $9.8 consumed", - "limitAmountMonthly": "Monthly Spending Limit (USD)", - "limitAmountWeekly": "Weekly Spending Limit (USD)", - "limitConcurrent": "Concurrent Session Limit", - "limitConcurrentDesc": "e.g. Provider C has limit of 2, currently 2 active sessions", - "limitConcurrentLabel": "Concurrent Session Limit", - "limitMonthlyLabel": "Monthly Spending Limit (USD)", - "limitPlaceholder0": "0 means unlimited", - "limitPlaceholderUnlimited": "Leave empty for unlimited", - "limitWeeklyLabel": "Weekly Spending Limit (USD)", - "modelRedirects": "Model Redirects", - "modelRedirectsAddNew": "Add New Rule", - "modelRedirectsCurrentRules": "Current Rules ({count})", - "modelRedirectsDesc": "Redirect Claude Code client model requests (e.g. claude-sonnet-4.5) to upstream provider supported models (e.g. glm-4.6, gemini-pro). For cost optimization or third-party AI integration.", - "modelRedirectsEmpty": "No redirect rules yet. System will auto-rewrite model names after adding rules.", - "modelRedirectsExists": "Model \"{model}\" already has a redirect rule", - "modelRedirectsLabel": "Model Redirects Configuration", - "modelRedirectsOptional": "(Optional)", - "modelRedirectsSourceModel": "User Requested Model", - "modelRedirectsSourcePlaceholder": "e.g. claude-sonnet-4-5-20250929", - "modelRedirectsSourceRequired": "Source model name cannot be empty", - "modelRedirectsTargetModel": "Actual Forwarded Model", - "modelRedirectsTargetPlaceholder": "e.g. glm-4.6", - "modelRedirectsTargetRequired": "Target model name cannot be empty", - "modelWhitelist": "Model Whitelist", - "modelWhitelistAllowAll": "Allow all {type} models", - "modelWhitelistAllowAllClause": "Allow all Claude models", - "modelWhitelistAllowAllOpenAI": "Allow all OpenAI models", - "modelWhitelistClear": "Clear", - "modelWhitelistDesc": "Limit models this provider can handle. By default, provider can handle all models of its type.", - "modelWhitelistLabel": "Allowed Models", - "modelWhitelistLoading": "Loading...", - "modelWhitelistManualAdd": "Manually Add Model", - "modelWhitelistManualDesc": "Support adding any model name (not limited to price table)", - "modelWhitelistManualPlaceholder": "Enter model name (e.g. gpt-5-turbo)", - "modelWhitelistNotFound": "Model not found", - "modelWhitelistSearchPlaceholder": "Search model name...", - "modelWhitelistSelectAll": "Select All ({count})", - "modelWhitelistSelected": "Selected {count} models", - "modelWhitelistSelectedOnly": "Only allow selected {count} models. Requests for other models won't be routed to this provider.", - "name": { - "label": "Provider Name *", - "placeholder": "e.g. Zhipu" - }, - "namePlaceholder": "Enter provider name", - "openDuration": "Circuit Break Duration (minutes)", - "openDurationDesc": "How long before auto entering half-open state", - "openDurationPlaceholder": "30", - "priority": "Priority", - "priorityDesc": "Lower number = higher priority (0 is highest). System only selects from highest priority providers. Recommendation: Main=0, Backup=1, Emergency=2", - "priorityLabel": "Priority", - "priorityPlaceholder": "0", - "providerGroupDesc": "Provider group tag. Only users with matching providerGroup can use this provider. Example: Set to \"premium\" to allow only providerGroup=\"premium\" users", - "providerGroupLabel": "Provider Group", - "providerGroupPlaceholder": "e.g. premium, economy", - "providerName": "Provider Name", - "providerNamePlaceholder": "e.g. Zhipu", - "providerNameRequired": "Provider Name *", - "providerType": "Provider Type", - "providerTypeDesc": "Select the API format type for the provider.", - "providerTypeDisabledNote": "Note: Gemini CLI and OpenAI Compatible types are under development", - "proxy": "Proxy", - "proxyAddressFormats": "Supported formats:", - "proxyAddressLabel": "Proxy Address", - "proxyAddressOptional": "(Optional)", - "proxyAddressPlaceholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080", - "proxyConfig": "Proxy Configuration", - "proxyConfigDesc": "Configure proxy server to improve provider connectivity (supports HTTP, HTTPS, SOCKS4, SOCKS5)", - "proxyConfigNone": "Not configured", - "proxyConfigSummary": "Proxy configured", - "proxyConfigSummaryFallback": " (fallback enabled)", - "proxyConfigured": "Proxy configured", - "proxyFallback": "Proxy Fallback", - "proxyFallbackDesc": "When enabled, auto try direct connection on proxy failure", - "proxyFallbackLabel": "Fallback to direct on proxy failure", - "proxyNotConfigured": "Not configured", - "proxyTestButton": "Test Connection", - "proxyTestDesc": "Test provider URL access via configured proxy (uses HEAD request, no quota consumption)", - "proxyTestFailed": "Connection Failed", - "proxyTestFillUrl": "Please fill in provider URL first", - "proxyTestLabel": "Connection Test", - "proxyTestNetworkError": "Network error: {error}", - "proxyTestProxyError": "Proxy error: {error}", - "proxyTestResponseTime": "Response time: {time}", - "proxyTestResultConnectionMethod": "Connection method: {via}", - "proxyTestResultConnectionMethodDirect": "Direct", - "proxyTestResultConnectionMethodProxy": "Proxy", - "proxyTestResultErrorType": "Error type: {type}", - "proxyTestResultFailed": "Connection failed", - "proxyTestResultMessage": "{message}", - "proxyTestResultResponseTime": "Response time: {time}ms", - "proxyTestResultStatusCode": "Status code: {code}", - "proxyTestResultSuccess": "Connection successful {via}", - "proxyTestStatusCode": "| Status code: {code}", - "proxyTestSuccess": "Connection Successful", - "proxyTestTesting": "Testing...", - "proxyTestTimeout": "Connection timeout (5s). Please check:\n1. Is proxy server accessible\n2. Are proxy address and port correct\n3. Are proxy credentials correct", - "proxyTestViaDirect": "(direct)", - "proxyTestViaProxy": "(via proxy)", - "proxyUrl": "Proxy Address", - "proxyUrlPlaceholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080", - "rateLimitConfig": "Rate Limit Configuration", - "rateLimitConfigNone": "Unlimited", - "rateLimitConfigSummary": "5h: ${fiveHour}, Weekly: ${weekly}, Monthly: ${monthly}, Concurrent: {concurrent}", - "remark": "Remark", - "remarkPlaceholder": "Optional: Add notes...", - "removeRedirect": "Remove Redirect", - "routingConfig": "Routing Configuration", - "routingConfigNone": "Not configured", - "routingConfigSummary": "{models} model whitelist, {redirects} redirects", - "scheduleParams": "Scheduling Parameters", - "searchClear": "Clear search", - "searchPlaceholder": "Search provider name, URL, remark...", - "selectProviderType": "Select provider type", - "sort": "Sort Providers", - "sortByCost": "By Cost", - "sortByCreated": "By Created (New-Old)", - "sortByName": "By Name (A-Z)", - "sortByPriority": "By Priority (High-Low)", - "sortByWeight": "By Weight (High-Low)", - "sourceModel": "Source Model Name", - "sourceModelPlaceholder": "e.g. claude-sonnet-4-5-20250929", - "sourceModelRequired": "Source model name cannot be empty", - "successThreshold": "Recovery Threshold (times)", - "successThresholdDesc": "How many successes in half-open state to fully recover", - "successThresholdPlaceholder": "2", - "targetModel": "Target Model Name", - "targetModelPlaceholder": "e.g. glm-4.6", - "targetModelRequired": "Target model name cannot be empty", - "testProxy": "Test Connection", - "testProxyFailed": "Failed to test proxy connection", - "testProxyFailedError": "Connection test failed:", - "testProxySuccess": "Proxy connection successful", - "validUrlRequired": "Please enter a valid API address", - "websiteUrl": { - "label": "Provider Website", - "placeholder": "https://example.com", - "desc": "Provider official website for quick access" - }, - "websiteUrlDesc": "Provider website URL for quick access", - "websiteUrlInvalid": "Please enter a valid provider website URL", - "websiteUrlPlaceholder": "https://example.com", - "weight": "Weight", - "weightDesc": "Weighted random probability. Within same priority, higher weight = higher selection probability. E.g. weights 1:2:3 = probabilities 16%:33%:50%", - "weightLabel": "Weight", - "weightPlaceholder": "1", - "title": { - "create": "Add Provider", - "edit": "Edit Provider" - }, - "dialogDescription": "Configure provider details and advanced settings.", - "url": { - "label": "API Address *", - "placeholder": "e.g. https://open.bigmodel.cn/api/anthropic" - }, - "key": { - "label": "API Key", - "leaveEmpty": "(Leave empty to keep unchanged)", - "placeholder": "Enter API Key", - "leaveEmptyDesc": "Leave empty to keep existing key", - "currentKey": "Current key: {key}" - }, - "buttons": { - "expandAll": "Expand All Advanced Settings", - "collapseAll": "Collapse All Advanced Settings", - "submit": "Confirm Add", - "submitting": "Adding...", - "update": "Confirm Update", - "updating": "Updating...", - "delete": "Delete" - }, - "common": { - "core": "Core" - }, - "sections": { - "routing": { - "title": "Routing", - "summary": { - "models": "{count} whitelisted models", - "redirects": "{count} redirects", - "none": "Not configured" - }, - "providerType": { - "label": "Provider Type", - "desc": "(determines scheduling policy)", - "placeholder": "Select provider type" - }, - "providerTypeDesc": "Choose the API format type of the provider.", - "providerTypeDisabledNote": "Note: OpenAI Compatible is under development and currently unavailable", - "modelRedirects": { - "label": "Model Redirects", - "optional": "(optional)" - }, - "joinClaudePool": { - "label": "Join Claude Routing Pool", - "desc": "When enabled, this provider will participate in load balancing with Claude-type providers", - "help": "Available only when there is a redirect mapping to claude-* models. When users request claude-* models, this provider also joins scheduling." - }, - "preserveClientIp": { - "label": "Forward client IP", - "desc": "Pass x-forwarded-for / x-real-ip to upstream providers (may expose real client IP)", - "help": "Keep off by default for privacy. Enable only when upstream must see the end-user IP." - }, - "modelWhitelist": { - "title": "Model Allowlist", - "desc": "Restrict which models this provider can serve. By default, a provider can serve all models of its type.", - "label": "Allowed Models", - "optional": "(optional)", - "allowAll": "✓ Allow all models (recommended)", - "selectedOnly": "Only the selected {count} models are allowed. Other models will not be routed to this provider.", - "moreModels": "+{count} more" - }, - "scheduleParams": { - "title": "Scheduling", - "priority": { - "label": "Priority", - "placeholder": "0", - "desc": "Lower value = higher priority (0 is highest). The system only chooses from the highest priority tier. Suggested: primary=0, standby=1, emergency=2" - }, - "weight": { - "label": "Weight", - "placeholder": "1", - "desc": "Weighted random. Within the same priority, higher weight increases selection probability. Example 1:2:3 ≈ 16%:33%:50%" - }, - "costMultiplier": { - "label": "Cost Multiplier", - "placeholder": "1.0", - "desc": "Cost multiplier. Official provider = 1.0, 20% cheaper = 0.8, 20% more expensive = 1.2 (up to 4 decimals)" - }, - "group": { - "label": "Provider Group", - "placeholder": "e.g. premium, economy", - "desc": "Group tag. Only users whose providerGroup matches can use this provider. Example: set to \"premium\" to serve users with providerGroup=\"premium\" only" - } - }, - "cacheTtl": { - "label": "Cache TTL Override", - "options": { - "inherit": "No override (follow client)", - "5m": "5 minutes", - "1h": "1 hour" - }, - "desc": "Force prompt cache TTL; only affects requests with cache_control." - }, - "context1m": { - "label": "1M Context Window", - "options": { - "inherit": "Inherit (follow client request)", - "forceEnable": "Force Enable (for supported models)", - "disabled": "Disabled" - }, - "desc": "Configure 1M context window support. Only affects Sonnet models (claude-sonnet-4-5, claude-sonnet-4). Tiered pricing applies when enabled." - }, - "codexOverrides": { - "reasoningEffort": { - "label": "Reasoning Effort Override", - "help": "Controls how much reasoning effort the model uses before answering. \"inherit\" follows the client request; other values force override reasoning.effort. Note: \"none\" is only supported on GPT-5.1 models; \"xhigh\" is only supported on GPT-5.1-Codex-Max. Using an unsupported value will cause an upstream error.", - "options": { - "inherit": "No override (follow client)", - "minimal": "minimal", - "low": "low", - "medium": "medium (default)", - "high": "high", - "xhigh": "xhigh (GPT-5.1-Codex-Max only)", - "none": "none (GPT-5.1 only)" - } - }, - "reasoningSummary": { - "label": "Reasoning Summary Override", - "help": "Controls whether the Responses API returns reasoning summaries. \"auto\" returns a condensed summary, \"detailed\" returns a more comprehensive one. \"inherit\" follows the client request.", - "options": { - "inherit": "No override (follow client)", - "auto": "auto", - "detailed": "detailed" - } - }, - "textVerbosity": { - "label": "Text Verbosity Override", - "help": "Controls how verbose the model output is. \"low\" is concise, \"high\" is verbose. \"inherit\" follows the client request.", - "options": { - "inherit": "No override (follow client)", - "low": "low", - "medium": "medium (default)", - "high": "high" - } - }, - "parallelToolCalls": { - "label": "Parallel Tool Calls Override", - "help": "Controls whether parallel tool calls are allowed. \"inherit\" follows the client request. Disabling may reduce tool-call concurrency.", - "options": { - "inherit": "No override (follow client)", - "true": "Force enable", - "false": "Force disable" - } - } - } - }, - "rateLimit": { - "title": "Rate Limit", - "summary": { - "fiveHour": "5h: ${amount}", - "daily": "Day: ${amount} (reset ${resetTime})", - "weekly": "Week: ${amount}", - "monthly": "Month: ${amount}", - "total": "Total: ${amount}", - "concurrent": "Concurrent: {count}", - "none": "Unlimited" - }, - "limit5h": { - "label": "5h Spend Limit (USD)", - "placeholder": "Leave empty for unlimited" - }, - "limitDaily": { - "label": "Daily Spend Limit (USD)", - "placeholder": "Leave empty for unlimited" - }, - "dailyResetMode": { - "label": "Daily Reset Mode", - "options": { - "fixed": "Fixed Time Reset", - "rolling": "Rolling Window (24h)" - }, - "desc": { - "fixed": "Reset quota at a fixed time each day", - "rolling": "Reset 24 hours after first API call" - } - }, - "dailyResetTime": { - "label": "Daily Reset Time (HH:mm)" - }, - "limitWeekly": { - "label": "Weekly Spend Limit (USD)", - "placeholder": "Leave empty for unlimited" - }, - "limitMonthly": { - "label": "Monthly Spend Limit (USD)", - "placeholder": "Leave empty for unlimited" - }, - "limitTotal": { - "label": "Total Spend Limit (USD)", - "placeholder": "Leave empty for unlimited" - }, - "limitConcurrent": { - "label": "Concurrent Sessions Limit", - "placeholder": "0 means unlimited" - } - }, - "circuitBreaker": { - "title": "Circuit Breaker", - "summary": "{failureThreshold} failures / {openDuration} min break / {successThreshold} successes to recover / {maxRetryAttempts} attempts per provider", - "desc": "Automatically break on consecutive failures to protect overall service quality", - "failureThreshold": { - "label": "Failure Threshold", - "placeholder": "5", - "desc": "Number of consecutive failures to trigger break" - }, - "openDuration": { - "label": "Break Duration (minutes)", - "placeholder": "30", - "desc": "Time before switching to half-open" - }, - "successThreshold": { - "label": "Recovery Threshold", - "placeholder": "2", - "desc": "Number of successes in half-open to fully recover" - }, - "maxRetryAttempts": { - "label": "Max Attempts Per Provider", - "placeholder": "2", - "desc": "Total tries (including the first call) before switching providers. Leave empty to use the system default." - } - }, - "proxy": { - "title": "Proxy", - "summary": { - "configured": "Proxy configured", - "fallback": " (fallback enabled)", - "none": "Not configured" - }, - "desc": "Configure a proxy to improve connectivity (HTTP, HTTPS, SOCKS4, SOCKS5 supported)", - "url": { - "label": "Proxy URL", - "optional": "(optional)", - "placeholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080", - "formats": "Supported formats:" - }, - "fallback": { - "label": "Fallback to direct on proxy failure", - "desc": "When enabled, will try direct connection if proxy fails" - }, - "test": { - "label": "Connectivity Test", - "desc": "Test accessing provider URL via proxy (HEAD request, no credits consumed)" - } - }, - "timeout": { - "title": "Timeout Configuration", - "summary": "First byte: {streaming}s | Stream interval: {idle}s | Non-streaming: {nonStreaming}s", - "desc": "Configure request timeout duration, 0 means disable timeout", - "streamingFirstByte": { - "label": "Streaming First Byte Timeout (seconds)", - "placeholder": "30", - "desc": "Streaming request first byte timeout, range 1-120 seconds, default 30 seconds", - "core": "true" - }, - "streamingIdle": { - "label": "Streaming Idle Timeout (seconds)", - "placeholder": "60", - "desc": "Streaming request idle timeout, range 60-600 seconds, enter 0 to disable (prevent mid-stream stalling)", - "core": "true" - }, - "nonStreamingTotal": { - "label": "Non-streaming Total Timeout (seconds)", - "placeholder": "600", - "desc": "Non-streaming request total timeout, range 60-1200 seconds, default 600 seconds (10 minutes)", - "core": "true" - }, - "disableHint": "Set to 0 to disable the timeout (for canary rollback scenarios only, not recommended)" - }, - "apiTest": { - "title": "Provider Model Test", - "summary": "Verify provider & model connectivity", - "desc": "Validate whether the selected provider type and model respond correctly. Defaults to the routing configuration unless overridden.", - "testLabel": "Provider Model Test" - }, - "codexStrategy": { - "title": "Codex Instructions Policy", - "summary": { - "auto": "Auto (recommended)", - "force": "Force official", - "keep": "Pass-through" - }, - "desc": "Control how to handle the instructions field in Codex requests; affects gateway compatibility", - "select": { - "label": "Strategy", - "placeholder": "Select a strategy", - "auto": { - "label": "Auto (recommended)", - "desc": "Pass through client instructions; on 400 error, retry with official prompt" - }, - "force": { - "label": "Force official", - "desc": "Always use official Codex CLI instructions (~4000+ chars)" - }, - "keep": { - "label": "Pass-through", - "desc": "Always pass through client instructions, no auto retry (for permissive gateways)" - } - }, - "hint": "Hint: Some strict Codex gateways (e.g. 88code, foxcode) require official instructions. Choose \"Auto\" or \"Force official\"." - }, - "mcpPassthrough": { - "title": "MCP Passthrough Configuration", - "summary": { - "none": "Disabled", - "minimax": "Minimax", - "glm": "GLM", - "custom": "Custom (Reserved)" - }, - "desc": "When enabled, pass through MCP tool calls to specified AI provider (e.g. minimax for image recognition, web search)", - "select": { - "label": "Passthrough Type", - "none": { - "label": "Disabled", - "desc": "Do not enable MCP passthrough (default)" - }, - "minimax": { - "label": "Minimax", - "desc": "Pass through to minimax MCP service (supports image recognition, web search, etc.)" - }, - "glm": { - "label": "GLM", - "desc": "Pass through to GLM MCP service (supports image analysis, video analysis, etc.)" - }, - "custom": { - "label": "Custom", - "desc": "Pass through to custom MCP service (reserved, not implemented yet)" - }, - "placeholder": "Select passthrough type" - }, - "hint": "Hint: MCP passthrough allows Claude Code client to use tool capabilities provided by third-party AI providers (e.g. image recognition, web search)", - "urlLabel": "MCP Passthrough URL", - "urlPlaceholder": "https://api.minimaxi.com", - "urlDesc": "MCP service base URL. Leave empty to auto-extract from provider URL", - "urlAuto": "Auto-extracted: {url}" - } - }, - "providerTypes": { - "claude": "Claude (Anthropic Messages API)", - "claudeAuth": "Claude (Anthropic Auth Token)", - "codex": "Codex (Response API)", - "gemini": "Gemini (Google Gemini API)", - "geminiCli": "Gemini CLI", - "geminiCliDisabled": " - in development", - "openaiCompatible": "OpenAI Compatible", - "openaiCompatibleDisabled": " - in development" - }, - "deleteDialog": { - "title": "Delete Provider", - "description": "Are you sure you want to delete provider \"{name}\"? This action cannot be undone.", - "cancel": "Cancel", - "confirm": "Confirm Delete" - }, - "failureThresholdConfirmDialog": { - "title": "Confirm Special Configuration", - "descriptionDisabledPrefix": "You are setting the circuit breaker failure threshold to ", - "descriptionDisabledValue": "0", - "descriptionDisabledMiddle": ", which means ", - "descriptionDisabledAction": "disabling the circuit breaker", - "descriptionDisabledSuffix": ". The provider will not be circuit-broken due to consecutive failures.", - "descriptionHighValuePrefix": "You are setting the circuit breaker failure threshold to ", - "descriptionHighValueSuffix": ", which is a high value and may cause the provider to be circuit-broken only after many failures.", - "confirmQuestion": "Are you sure you want to save this configuration?", - "cancel": "Cancel", - "confirm": "Confirm Save" - }, - "errors": { - "invalidUrl": "Please enter a valid API address", - "invalidWebsiteUrl": "Please enter a valid provider website URL", - "groupTagTooLong": "Provider group tags are too long (max {max} chars total)", - "addFailed": "Failed to add provider", - "updateFailed": "Failed to update provider", - "deleteFailed": "Failed to delete provider" - }, - "success": { - "created": "Provider added successfully", - "createdDesc": "Provider \"{name}\" has been added" - } - }, - "guide": { - "after": "After:", - "before": "Before:", - "bestPracticesConcurrent": "• Concurrent Control: Set session concurrency by provider API limits", - "bestPracticesCost": "• Cost Multiplier: Official=1.0, Self-hosted can be 0.8-1.2", - "bestPracticesLimit": "• Limit Settings: Set 5h, 7d, 30d limits based on budget", - "bestPracticesPriority": "• Priority Settings: Core providers=0, Backup=1-3", - "bestPracticesTitle": "Best Practices", - "bestPracticesWeight": "• Weight Config: Set weight by capacity (higher capacity = higher weight)", - "circuitBreaker": "Circuit Breaker Check", - "circuitBreakerOpen": "A filtered, remaining: B, C, D", - "circuitBreakerRecovery": "A automatically recovers to half-open after 60 seconds", - "circuitBreakerRecovery5h": "Auto recovery after 5-hour sliding window", - "costOptimize": "2️⃣ Cost Optimization: Within same priority, lower cost multiplier has higher probability", - "costSort": "Cost-based Sorting Fallback", - "costSortExample": "All providers: A (default), B (premium), C (premium), D (economy)", - "costSortProb": "Lower cost C has higher selection probability", - "costSortResult": "After sorting: C (0.8x), A (1.0x)", - "decision": "Decision:", - "group": "User Group Filtering", - "groupDesc": "If user has provider group specified, system prioritizes selection from that group", - "groupDowngrade": "Log warning and select from global provider pool", - "groupExample": "User configured providerGroup = 'premium'", - "groupFallback": "If no available providers in user group, fallback to all providers", - "groupFiltered": "Select only from A and C, B and D filtered", - "groupUnavailable": "All providers in user group 'vip' are disabled or over limit", - "health": "Health Filtering (Circuit Breaker + Rate Limit)", - "healthCheck": "Check if B is enabled and healthy", - "healthCheckAmountLimit": "Check if spending exceeds limits (5h, 7d, 30d)", - "healthCheckAmountLimitExample": "Provider B has $10 limit (5h), $9.8 consumed", - "healthCheckCircuit": "Provider A failed 5 times, circuit breaker: open", - "healthCheckConcurrent": "Check if current active session count exceeds limit", - "healthCheckConcurrentExample": "Provider C limit 2, currently 2 active sessions", - "healthFilter": "3️⃣ Health Filtering: Auto skip circuit-broken or over-limit providers", - "healthFiltered": "B filtered (near limit), remaining: C, D", - "healthFiltered2": "C filtered (full), remaining: D", - "history": "Check Request History", - "historyDesc": "Query providers used by this API Key in last 10 seconds", - "priority": "Priority Layering", - "priorityExample": "4 enabled providers with different priorities", - "priorityFirst": "1️⃣ Priority First: Select only from highest priority (lowest number) providers", - "priorityResult": "Filtered to highest priority (0) providers: A, C", - "priorityStep": "System first filters by priority, selecting only from highest priority providers", - "randomResult": "Finally selected C randomly", - "randomSelect": "Weighted Random", - "reset": "Manual Circuit Breaker Reset", - "resetSuccess": "Circuit breaker reset", - "scenario1Desc": "System first filters by priority, selecting only from highest priority providers", - "scenario1Step1": "Initial State", - "scenario1Step1After": "Filtered to highest priority (0) providers: A, C", - "scenario1Step1Before": "Provider A (priority 0), B (priority 1), C (priority 0), D (priority 2)", - "scenario1Step1Decision": "Select only from A and C, B and D filtered out", - "scenario1Step1Desc": "4 enabled providers with different priorities", - "scenario1Step2": "Cost Sorting", - "scenario1Step2After": "After sorting: C (0.8x), A (1.0x)", - "scenario1Step2Before": "A (cost 1.0x), C (cost 0.8x)", - "scenario1Step2Decision": "Lower cost C has higher selection probability", - "scenario1Step2Desc": "Within same priority, sort by cost multiplier low to high", - "scenario1Step3": "Weighted Random", - "scenario1Step3After": "C has 75% probability, A has 25%", - "scenario1Step3Before": "C (weight 3), A (weight 1)", - "scenario1Step3Decision": "Finally randomly selected C", - "scenario1Step3Desc": "Use weight for random selection, higher weight = higher probability", - "scenario1Title": "Priority Layering", - "scenario2Desc": "If user has provider group specified, system prioritizes selection from that group", - "scenario2Step1": "Check User Group", - "scenario2Step1After": "Filtered to 'premium' group: B, C", - "scenario2Step1Before": "All providers: A (default), B (premium), C (premium), D (economy)", - "scenario2Step1Decision": "Select only from B and C", - "scenario2Step1Desc": "User configured providerGroup = 'premium'", - "scenario2Step2": "Group Fallback", - "scenario2Step2After": "Fallback to all enabled providers: A, B, C, D", - "scenario2Step2Before": "All providers in user group 'vip' disabled or over limit", - "scenario2Step2Decision": "Log warning and select from global provider pool", - "scenario2Step2Desc": "If no available providers in user group, fallback to all providers", - "scenario2Title": "User Group Filtering", - "scenario3Desc": "System auto filters circuit-broken or over-limit providers", - "scenario3Step1": "Circuit Breaker Check", - "scenario3Step1After": "A filtered, remaining: B, C, D", - "scenario3Step1Before": "Provider A failed 5 times, circuit breaker: open", - "scenario3Step1Decision": "A auto recovers to half-open after 60s", - "scenario3Step1Desc": "Circuit breaker opens after 5 consecutive failures, unavailable for 60s", - "scenario3Step2": "Amount Rate Limit", - "scenario3Step2After": "B filtered (near limit), remaining: C, D", - "scenario3Step2Before": "Provider B 5h limit $10, consumed $9.8", - "scenario3Step2Decision": "Auto recovery after 5h sliding window", - "scenario3Step2Desc": "Check if spending exceeds limits (5h, 7d, 30d)", - "scenario3Step3": "Concurrent Session Limit", - "scenario3Step3After": "C filtered (full), remaining: D", - "scenario3Step3Before": "Provider C concurrent limit 2, currently 2 active sessions", - "scenario3Step3Decision": "Auto release after session expiry (5 min)", - "scenario3Step3Desc": "Check if active session count exceeds configured concurrent limit", - "scenario3Title": "Health Filtering (Circuit Breaker + Rate Limit)", - "scenario4Desc": "Consecutive chats prioritize using same provider, leveraging Claude context cache", - "scenario4Step1": "Check Request History", - "scenario4Step1After": "Check if B is enabled and healthy", - "scenario4Step1Before": "Last request used provider B", - "scenario4Step1Decision": "B available, reuse directly, skip random selection", - "scenario4Step1Desc": "Query providers used by this API Key in last 10 seconds", - "scenario4Step2": "Reuse Invalidation", - "scenario4Step2After": "Enter normal selection flow", - "scenario4Step2Before": "Last used provider B disabled or circuit-broken", - "scenario4Step2Decision": "Select from other available providers", - "scenario4Step2Desc": "If last used provider unavailable, reselect", - "scenario4Title": "Session Reuse Mechanism", - "scenariosTitle": "Interactive Scenario Demos", - "session": "Session Reuse Mechanism", - "sessionDesc": "If the last used provider is unavailable, reselect", - "sessionExample": "Last request used provider B", - "sessionExpired": "Session automatically released after expiration (5 minutes)", - "sessionFallback": "Select from other available providers", - "sessionLastUsed": "B is available, reuse directly, skip random selection", - "sessionReuse": "4️⃣ Session Reuse: Consecutive chats reuse same provider, saving context costs", - "sessionUnavailable": "Last used provider B is disabled or circuit-broken", - "step": "Step", - "title": "Core Principles", - "weight": "Weighted random selection based on weight", - "weightCalc": "C has 75% selection probability, A has 25%", - "weightExample": "C (weight 3), A (weight 1)" - }, - "keyLoading": "Loading...", - "noProviders": "No providers configured", - "noProvidersDesc": "Add your first API provider", - "notFound": "No matching providers found", - "official": "Official", - "resetCircuit": "Circuit breaker reset", - "resetCircuitDesc": "Provider \"{name}\" circuit breaker status cleared", - "resetCircuitFailed": "Failed to reset circuit breaker", - "scheduling": "Scheduling Strategy Details", - "schedulingDesc": "Understand how provider selection works with priority layering, session reuse, load balancing and failover", - "searchNoResults": "No matching providers found", - "searchResults": "Found {count} matching providers", - "section": { - "description": "Configure upstream provider rate limiting and concurrent session limits. Leave empty for unlimited.", - "leaderboard": "Leaderboard", - "title": "Provider Management" - }, - "filter": { - "status": { - "all": "Any status", - "active": "Active", - "inactive": "Inactive" - }, - "groups": { - "label": "Groups:", - "all": "All", - "default": "default" - }, - "circuitBroken": "Circuit Broken" - }, - "subtitle": "Provider Management", - "subtitleDesc": "Configure upstream provider rate limiting and concurrent session limits. Leave empty for unlimited.", - "title": "Provider Management", - "todayUsage": "Today's Usage", - "todayUsageCount": "{count} times", - "toggleFailed": "Toggle failed", - "toggleSuccess": "Provider {status}", - "toggleSuccessDesc": "Provider \"{name}\" status updated", - "updateFailed": "Failed to update provider", - "viewKey": "View Complete API Key", - "viewKeyDesc": "Please keep it safe and don't share it with others", - "sort": { - "byName": "By Name (A-Z)", - "byPriority": "By Priority (High-Low)", - "byWeight": "By Weight (High-Low)", - "byActualPriority": "By Actual Selection Priority", - "byCreatedAt": "By Created Time (New-Old)", - "placeholder": "Sort Providers" - }, - "search": { - "placeholder": "Search provider name, URL, notes...", - "clear": "Clear search", - "found": "Found {count} matching provider(s)", - "notFound": "No matching providers found", - "showing": "Showing {filtered} / {total} providers" - } - }, - "sensitiveWords": { - "add": "Add Sensitive Word", - "addFailed": "Failed to create sensitive word", - "addSuccess": "Sensitive word created successfully", - "cacheStats": "Cache stats: Contains({containsCount}) Exact({exactCount}) Regex({regexCount})", - "confirmDelete": "Are you sure you want to delete the sensitive word \"{word}\"?", - "delete": "Delete Sensitive Word", - "deleteFailed": "Delete failed", - "deleteSuccess": "Sensitive word deleted successfully", - "description": "Configure sensitive word filtering rules to block requests with sensitive content.", - "dialog": { - "addDescription": "Configure sensitive word filtering rules. Matched requests will not be forwarded upstream.", - "addTitle": "Add Sensitive Word", - "creating": "Creating...", - "descriptionLabel": "Description", - "descriptionPlaceholder": "Optional: Add description...", - "editDescription": "Modify sensitive word configuration. Changes will automatically refresh the cache.", - "editTitle": "Edit Sensitive Word", - "matchTypeContains": "Contains Match - Block if text contains this word", - "matchTypeExact": "Exact Match - Block only if exact match", - "matchTypeLabel": "Match Type *", - "matchTypeRegex": "Regular Expression - Support complex pattern matching", - "saving": "Saving...", - "wordLabel": "Sensitive Word *", - "wordPlaceholder": "Enter sensitive word...", - "wordRequired": "Please enter a sensitive word" - }, - "disable": "Sensitive word disabled", - "edit": "Edit Sensitive Word", - "editFailed": "Failed to update sensitive word", - "editSuccess": "Sensitive word updated successfully", - "emptyState": "No sensitive words yet. Click 'Add Sensitive Word' in the top right to start configuration.", - "enable": "Sensitive word enabled", - "refreshCache": "Refresh Cache", - "refreshCacheFailed": "Failed to refresh cache", - "refreshCacheSuccess": "Cache refreshed successfully, loaded {count} sensitive words", - "section": { - "description": "Requests blocked by sensitive words will not be forwarded upstream and will not be charged. Supports contains matching, exact matching, and regex patterns.", - "title": "Sensitive Words List" - }, - "table": { - "actions": "Actions", - "createdAt": "Created At", - "description": "Description", - "matchType": "Match Type", - "matchTypeContains": "Contains Match", - "matchTypeExact": "Exact Match", - "matchTypeRegex": "Regular Expression", - "status": "Status", - "word": "Sensitive Word" - }, - "title": "Sensitive Words Management", - "toggleFailed": "Toggle failed", - "toggleFailedError": "Toggle failed:" - }, - "requestFilters": { - "nav": "Request Filters", - "title": "Request Filters", - "description": "Configure header removal/override and body replacement rules to sanitize requests before forwarding upstream.", - "add": "Add Filter", - "addSuccess": "Filter created", - "addFailed": "Failed to create filter", - "edit": "Edit Filter", - "editSuccess": "Filter updated", - "editFailed": "Failed to update filter", - "delete": "Delete Filter", - "deleteSuccess": "Filter deleted", - "deleteFailed": "Delete failed", - "enable": "Enabled", - "disable": "Disabled", - "confirmDelete": "Delete filter \"{name}\"?", - "empty": "No filters yet. Click Add Filter to configure.", - "refreshCache": "Refresh Cache", - "refreshSuccess": "Cache refreshed, loaded {count} filters", - "refreshFailed": "Refresh failed", - "dialog": { - "createTitle": "Add Filter", - "editTitle": "Edit Filter", - "name": "Name", - "scope": "Scope", - "action": "Action", - "target": "Target field/path", - "replacement": "Replacement (optional)", - "description": "Description (optional)", - "priority": "Priority", - "matchType": "Match Type", - "matchTypeContains": "Contains", - "matchTypeExact": "Exact", - "matchTypeRegex": "Regex", - "jsonPathPlaceholder": "e.g. messages.0.content or data.items[0].token", - "targetPlaceholder": "Header name or text/path", - "replacementPlaceholder": "String or JSON, leave blank to clear", - "save": "Save", - "saving": "Saving...", - "validation": { - "fieldRequired": "Name and target are required" - }, - "bindingType": "Apply To", - "bindingGlobal": "All Providers (Global)", - "bindingProviders": "Specific Providers", - "bindingGroups": "Provider Groups", - "selectProviders": "Select providers...", - "selectGroups": "Select groups...", - "searchProviders": "Search providers...", - "searchGroups": "Search groups...", - "noProvidersFound": "No providers found", - "noGroupsFound": "No groups found", - "providersSelected": "{count} provider(s) selected", - "groupsSelected": "{count} group(s) selected", - "loading": "Loading...", - "clear": "Clear", - "selectAll": "Select All" - }, - "table": { - "name": "Name", - "scope": "Scope", - "action": "Action", - "target": "Target", - "replacement": "Replacement", - "priority": "Priority", - "apply": "Apply", - "status": "Status", - "createdAt": "Created At", - "actions": "Actions" - }, - "scopeLabel": { - "header": "Header", - "body": "Body" - }, - "actionLabel": { - "remove": "Remove Header", - "set": "Set Header", - "json_path": "JSON Path Replace", - "text_replace": "Text Replace" - }, - "applyToAll": "Applied to all requests", - "providers": "Providers", - "groups": "Groups" - }, - "errorRules": { - "nav": "Error Rules", - "title": "Error Rules Management", - "description": "Manage client error rules that should not trigger automatic retries. When configured, errors matching these rules will be returned directly to users without retrying or counting toward provider circuit breaker thresholds.", - "section": { - "title": "Error Rules List" - }, - "tester": { - "title": "Error Rule Tester", - "description": "Input an error message to check if it matches configured rules and see the final response.", - "inputLabel": "Test Error Message", - "inputPlaceholder": "Enter an error message to test...", - "testButton": "Run Test", - "testing": "Testing...", - "matched": "Matched an error rule", - "notMatched": "No rule matched", - "finalResponse": "Override response to return", - "ruleInfo": "Matched rule", - "noRule": "No rule matched", - "category": "Category", - "pattern": "Pattern", - "matchType": "Match type", - "overrideStatusCode": "Override status code", - "testFailed": "Test failed, please try again", - "messageRequired": "Please enter an error message to test", - "warnings": "Configuration Warnings", - "statusCodeOnlyOverride": "Only status code will be overridden, response body will use upstream error" - }, - "add": "Add Error Rule", - "addSuccess": "Error rule created successfully", - "addFailed": "Failed to create error rule", - "edit": "Edit Error Rule", - "editSuccess": "Error rule updated successfully", - "editFailed": "Failed to update error rule", - "delete": "Delete Error Rule", - "deleteSuccess": "Error rule deleted successfully", - "deleteFailed": "Delete failed", - "enable": "Error rule enabled", - "disable": "Error rule disabled", - "toggleFailed": "Toggle failed", - "toggleFailedError": "Toggle failed:", - "refreshCache": "Sync Rules", - "refreshCacheSuccess": "Rules synced successfully, loaded {count} error rules", - "refreshCacheFailed": "Failed to sync rules", - "cacheStats": "Cached {totalCount} error rules", - "emptyState": "No error rules yet. Click 'Add Error Rule' in the top right to start configuration.", - "confirmDelete": "Are you sure you want to delete error rule \"{pattern}\"?", - "dialog": { - "addTitle": "Add Error Rule", - "addDescription": "Configure error message regex patterns. Matched errors will be identified as non-retryable client errors.", - "editTitle": "Edit Error Rule", - "editDescription": "Modify error rule configuration. Changes will automatically refresh the cache.", - "patternLabel": "Regex Pattern *", - "patternPlaceholder": "Enter regular expression...", - "patternRequired": "Please enter regex pattern", - "patternHint": "Supports JavaScript regex syntax, e.g.: prompt is too long|invalid.*request", - "categoryLabel": "Rule Category *", - "categoryPlaceholder": "Select rule category", - "categoryRequired": "Please select rule category", - "categoryHint": "Choose the error category for classification and statistics", - "descriptionLabel": "Description", - "descriptionPlaceholder": "Optional: Add description...", - "invalidRegex": "Invalid regex syntax", - "regexTester": "Regex Tester", - "testMessageLabel": "Test Message", - "testMessagePlaceholder": "Enter error message to test...", - "matchSuccess": "Match Successful", - "matchFailed": "No Match", - "invalidPattern": "Invalid Regex", - "matchedText": "Matched Text", - "defaultRuleHint": "Default rule pattern cannot be modified", - "enableOverride": "Enable Error Override", - "enableOverrideHint": "When enabled, you can customize the error response and status code returned to clients. Original errors are still logged to the database. Currently only supports Claude API error format.", - "overrideResponseLabel": "Override Response (JSON)", - "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"Your custom message\"\n }\n}", - "overrideResponseHint": "Leave empty to only override status code.", - "overrideStatusCodeLabel": "Override Status Code (Optional)", - "overrideStatusCodePlaceholder": "e.g. 400", - "overrideStatusCodeHint": "Leave empty to use upstream status code. Range: 400-599.", - "useTemplate": "Claude Error Template", - "useTemplateConfirm": "Existing content will be replaced by the template. Continue?", - "validJson": "JSON format is valid", - "invalidJson": "Invalid JSON format", - "invalidStatusCode": "Status code must be between 400-599", - "creating": "Creating...", - "saving": "Saving..." - }, - "table": { - "pattern": "Regex Pattern", - "category": "Rule Category", - "description": "Description", - "status": "Status", - "default": "Default", - "isEnabled": "Enabled Status", - "isDefault": "Default Rule", - "createdAt": "Created At", - "actions": "Actions" - }, - "form": { - "fields": { - "pattern": "Regex Pattern", - "category": "Rule Category", - "description": "Description" - }, - "placeholders": { - "pattern": "e.g. prompt is too long", - "category": "Select category", - "description": "Optional: Add description..." - }, - "labels": { - "pattern": "Regex Pattern *", - "category": "Rule Category *", - "description": "Description", - "isEnabled": "Enabled Status" - } - }, - "actions": { - "add": "Add", - "edit": "Edit", - "delete": "Delete", - "refresh": "Refresh", - "test": "Test", - "messages": { - "success": "Operation successful", - "error": "Operation failed" - } - }, - "validation": { - "patternRequired": "Please enter regex pattern", - "categoryRequired": "Please select rule category", - "patternInvalid": "Invalid regex syntax", - "redosRisk": "Regex has ReDoS risk, please simplify pattern", - "patternTooComplex": "Regex is too complex" - }, - "categories": { - "prompt_limit": "Prompt Length Limit", - "content_filter": "Content Filter", - "pdf_limit": "PDF Page Limit", - "thinking_error": "Thinking Format Error", - "parameter_error": "Parameter Validation Failed", - "invalid_request": "Invalid Request", - "cache_limit": "Cache Control Limit" - }, - "regexTester": { - "title": "Regex Tester", - "testMessage": "Test Message", - "testMessagePlaceholder": "Enter error message to test...", - "matchResult": "Match Result", - "matched": "Matched", - "notMatched": "Not Matched", - "test": "Test" - }, - "defaultRules": { - "cannotDelete": "Default rules cannot be deleted", - "cannotDisable": "Recommend keeping default rules enabled" - } - }, - "mcpPassthroughConfig": "MCP Passthrough Configuration", - "mcpPassthroughConfigNone": "Disabled", - "mcpPassthroughConfigMinimax": "Minimax", - "mcpPassthroughConfigGlm": "GLM", - "mcpPassthroughConfigCustom": "Custom (Reserved)", - "mcpPassthroughDesc": "When enabled, pass through MCP tool calls to specified AI provider (e.g. minimax for image recognition, web search)", - "mcpPassthroughSelect": "Passthrough Type", - "mcpPassthroughNoneLabel": "Disabled", - "mcpPassthroughNoneDesc": "Do not enable MCP passthrough (default)", - "mcpPassthroughMinimaxLabel": "Minimax", - "mcpPassthroughMinimaxDesc": "Pass through to minimax MCP service (supports image recognition, web search, etc.)", - "mcpPassthroughGlmLabel": "GLM", - "mcpPassthroughGlmDesc": "Pass through to GLM MCP service (supports image analysis, video analysis, etc.)", - "mcpPassthroughCustomLabel": "Custom", - "mcpPassthroughCustomDesc": "Pass through to custom MCP service (reserved, not implemented yet)", - "mcpPassthroughHint": "Hint: MCP passthrough allows Claude Code client to use tool capabilities provided by third-party AI providers (e.g. image recognition, web search)", - "mcpPassthroughUrlLabel": "MCP Passthrough URL", - "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com", - "mcpPassthroughUrlDesc": "MCP service base URL. Leave empty to auto-extract from provider URL", - "mcpPassthroughUrlAuto": "Auto-extracted: {url}" -} diff --git a/messages/en/settings/clientVersions.json b/messages/en/settings/clientVersions.json new file mode 100644 index 000000000..0822fc2cb --- /dev/null +++ b/messages/en/settings/clientVersions.json @@ -0,0 +1,51 @@ +{ + "description": "Manage client version requirements to ensure users use latest stable version. VSCode plugin and CLI are managed separately.", + "empty": { + "description": "No active users using recognizable clients in past 7 days", + "title": "No client data available" + }, + "features": { + "activeWindow": "Active Window: ", + "activeWindowDesc": "Only counts users with requests in the past 7 days", + "autoDetect": "System automatically detects the latest stable version (GA version) for each client type", + "blockOldVersion": "Users with old versions will receive HTTP 400 error and cannot continue using the service", + "errorMessage": "Error message includes current version and required upgrade version", + "gaRule": "GA Rule: ", + "gaRuleDesc": "A version is considered GA when used by more than 1 user", + "recommendation": "Recommendation: ", + "recommendationDesc": "Monitor the version distribution below and confirm new version stability before enabling.", + "title": "Feature Description", + "whatHappens": "What happens when enabled:" + }, + "section": { + "distribution": { + "description": "Shows client version info for active users in past 7 days. Each client type independently tracks GA versions.", + "title": "Client Version Distribution" + }, + "settings": { + "description": "When enabled, system automatically detects client version and blocks old version users.", + "title": "Update Reminder Settings" + } + }, + "table": { + "currentGA": "Current GA Version: ", + "internalType": "Internal Type: ", + "lastActive": "Last Active", + "latest": "Latest", + "needsUpgrade": "Needs Upgrade", + "noUsers": "No user data available", + "status": "Status", + "unknown": "Unknown", + "user": "User", + "usersCount": "{count} users", + "version": "Current Version" + }, + "title": "Client Update Reminder", + "toggle": { + "description": "When enabled, system will block requests from old version clients", + "disableSuccess": "Client version check disabled", + "enable": "Enable Update Reminder", + "enableSuccess": "Client version check enabled", + "toggleFailed": "Update failed" + } +} diff --git a/messages/en/settings/common.json b/messages/en/settings/common.json new file mode 100644 index 000000000..523114174 --- /dev/null +++ b/messages/en/settings/common.json @@ -0,0 +1,31 @@ +{ + "cancel": "Cancel", + "completed": "Completed", + "confirm": "Confirm", + "copied": "Key copied to clipboard", + "copy": "Copy", + "copyFailed": "Copy failed", + "create": "Create", + "creating": "Creating...", + "delete": "Delete", + "disabled": "Disabled", + "edit": "Edit", + "empty": "No matching results found", + "enabled": "Enabled", + "error": "Unknown error", + "failed": "Failed", + "loading": "Loading...", + "none": "None (No users using this version)", + "refresh": "Refresh", + "reset": "Reset", + "save": "Save", + "saving": "Saving...", + "submit": "Submit", + "success": "Success", + "test": "Test", + "testing": "Testing...", + "unlimited": "Unlimited", + "unlimited_desc": "Unlimited", + "update": "Update", + "updating": "Updating..." +} diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json new file mode 100644 index 000000000..7b995c2b7 --- /dev/null +++ b/messages/en/settings/config.json @@ -0,0 +1,89 @@ +{ + "autoCleanup": "Auto Log Cleanup", + "autoCleanupDesc": "Automatically clean up historical log data on schedule to free up database storage space.", + "description": "Manage system basic parameters that affect site display and statistics behavior.", + "form": { + "allowGlobalView": "Allow Global Usage View", + "allowGlobalViewDesc": "When disabled, regular users can only view their own key usage statistics in the dashboard.", + "autoCleanupSaved": "Auto cleanup configuration saved", + "billingModelSource": "Billing Model Source", + "billingModelSourceDesc": "Configure which model to use for billing when model redirection occurs. 'Before Redirection' uses the original model requested by the user, 'After Redirection' uses the actual model called.", + "billingModelSourceOptions": { + "original": "Before Redirection (Original Model)", + "redirected": "After Redirection (Actual Model)" + }, + "billingModelSourcePlaceholder": "Select billing model source", + "cleanupBatchSize": "Batch Size", + "cleanupBatchSizeDesc": "Number of records to delete per batch (range: 1000-100000, recommended 10000)", + "cleanupBatchSizePlaceholder": "10000", + "cleanupBatchSizeRequired": "Batch Size *", + "cleanupRetentionDays": "Retention Days", + "cleanupRetentionDaysDesc": "Logs older than this number of days will be automatically cleaned (range: 1-365 days)", + "cleanupRetentionDaysPlaceholder": "30", + "cleanupRetentionDaysRequired": "Retention Days *", + "cleanupSchedule": "Cleanup Schedule", + "cleanupScheduleCronDesc": "Cron expression, default: 0 2 * * * (2 AM daily)", + "cleanupScheduleCronExample": "Example: 0 3 * * 0 (3 AM every Sunday)", + "cleanupScheduleDesc": "Select the execution schedule for automatic cleanup", + "cleanupScheduleLabel": "Execution Time (Cron)", + "cleanupSchedulePlaceholder": "0 2 * * *", + "cleanupScheduleRequired": "Execution Time (Cron) *", + "configUpdated": "System settings updated. The page will refresh to apply currency display changes.", + "currencies": { + "CNY": "¥ Chinese Yuan (CNY)", + "EUR": "€ Euro (EUR)", + "GBP": "£ British Pound (GBP)", + "HKD": "HK$ Hong Kong Dollar (HKD)", + "JPY": "¥ Japanese Yen (JPY)", + "KRW": "₩ South Korean Won (KRW)", + "SGD": "S$ Singapore Dollar (SGD)", + "TWD": "NT$ New Taiwan Dollar (TWD)", + "USD": "$ US Dollar (USD)" + }, + "currencyDisplay": "Currency Display Unit", + "currencyDisplayDesc": "After modification, all pages and API interfaces will use the corresponding currency symbol (symbol only, no exchange rate conversion).", + "currencyDisplayPlaceholder": "Select currency unit", + "enableAutoCleanup": "Enable Auto Cleanup", + "enableAutoCleanupDesc": "Automatically clean up historical log data on schedule", + "enableHttp2": "Enable HTTP/2", + "enableHttp2Desc": "When enabled, proxy requests will prefer HTTP/2 protocol. Automatically falls back to HTTP/1.1 on failure.", + "enableResponseFixer": "Enable Response Fixer", + "enableResponseFixerDesc": "Automatically repairs common upstream response issues (encoding, SSE, truncated JSON). Enabled by default.", + "enableThinkingSignatureRectifier": "Enable Thinking Signature Rectifier", + "enableThinkingSignatureRectifierDesc": "When Anthropic providers return thinking signature incompatibility or invalid request errors, automatically removes incompatible thinking blocks and retries once against the same provider (enabled by default).", + "interceptAnthropicWarmupRequests": "Intercept Warmup Requests (Anthropic)", + "interceptAnthropicWarmupRequestsDesc": "When enabled, Claude Code warmup probe requests will be answered by CCH directly to avoid upstream provider calls; the request is logged for audit but is not billed, not rate-limited, and excluded from statistics.", + "keepDays": "Retention Days", + "keepDaysDesc": "Clean up logs older than this number of days", + "responseFixerFixEncoding": "Fix encoding issues", + "responseFixerFixEncodingDesc": "Removes BOM/null bytes and normalizes invalid UTF-8.", + "responseFixerFixSseFormat": "Fix SSE format", + "responseFixerFixSseFormatDesc": "Adds missing data: prefix, normalizes line endings, and fixes common field formatting.", + "responseFixerFixTruncatedJson": "Fix truncated JSON", + "responseFixerFixTruncatedJsonDesc": "Closes unclosed braces/quotes, removes trailing commas, and fills missing values with null when needed.", + "saveConfig": "Save Configuration", + "saveError": "Save failed", + "saveFailed": "Save failed", + "saveSettings": "Save Settings", + "saveSuccess": "Saved successfully", + "siteTitle": "Site Title", + "siteTitleDesc": "Used to set browser tab title and system default display name.", + "siteTitlePlaceholder": "e.g. Claude Code Hub", + "siteTitleRequired": "Site title cannot be empty", + "verboseProviderError": "Verbose Provider Error", + "verboseProviderErrorDesc": "When enabled, return detailed error messages when all providers are unavailable (including provider count, rate limit reasons, etc.); when disabled, only return a simple error code." + }, + "section": { + "autoCleanup": { + "description": "Automatically clean up historical log data on schedule to free up database storage space.", + "title": "Auto Log Cleanup" + }, + "siteParams": { + "description": "Configure site title, currency display unit, and dashboard statistics display policy.", + "title": "Site Parameters" + } + }, + "siteSettings": "Site Parameters", + "siteSettingsDesc": "Configure site title, currency display unit, and dashboard statistics display policy.", + "title": "Basic Configuration" +} diff --git a/messages/en/settings/data.json b/messages/en/settings/data.json new file mode 100644 index 000000000..61bbf7b55 --- /dev/null +++ b/messages/en/settings/data.json @@ -0,0 +1,133 @@ +{ + "cleanup": { + "backupRecommendation": "Recommendation: Export database backup before cleanup in case recovery is needed.", + "button": "Clean Logs", + "cancel": "Cancel", + "cleaning": "Cleaning...", + "confirm": "Confirm Cleanup", + "confirmTitle": "Confirm Log Cleanup", + "confirmWarning": "This operation will permanently delete all log records from {range} and cannot be recovered.", + "descriptionWarning": "Clean up historical log data to free up database storage. Note: Statistics data will be retained, but log details will be permanently deleted.", + "error": "Failed to clean logs", + "failed": "Cleanup failed", + "logsDeleted": "✗ Log details will be deleted (request/response content, error info, etc.)", + "previewCount": "Will delete {count} log records", + "previewError": "Unable to get preview", + "previewLoading": "Counting...", + "range": { + "180days": "Logs older than 6 months (180 days)", + "30days": "Logs older than 1 month (30 days)", + "7days": "Logs older than 1 week (7 days)", + "90days": "Logs older than 3 months (90 days)" + }, + "rangeDescription": { + "180days": "6 months ago", + "30days": "1 month ago", + "7days": "1 week ago", + "90days": "3 months ago", + "default": "{days} days ago" + }, + "rangeLabel": "Cleanup Range", + "statisticsRetained": "✓ Statistics data will be retained (for trend analysis)", + "successMessage": "Successfully cleaned {count} log records ({batches} batches, took {duration}s)", + "willClean": "Will clean all log records from {range}" + }, + "description": "Manage database backup and recovery with full data import/export and log cleanup.", + "export": { + "button": "Export Database", + "descriptionFull": "Export complete database backup file (.dump format) for data migration or recovery. Backup uses PostgreSQL custom format, auto-compressed and compatible with different database versions.", + "error": "Failed to export database", + "exporting": "Exporting...", + "failed": "Export failed", + "successMessage": "Database exported successfully!" + }, + "guide": { + "items": { + "cleanup": { + "description": "Physically delete historical log data (irreversible). Statistics table will be retained. Recommend exporting database backup before cleanup.", + "title": "Log Cleanup" + }, + "environment": { + "description": "This feature requires Docker Compose deployment. Local development may not support it.", + "title": "Environment Requirements" + }, + "format": { + "description": "Uses PostgreSQL custom format (.dump), auto-compressed and compatible with different database versions.", + "title": "Backup Format" + }, + "merge": { + "description": "Retains existing data and attempts to insert backup data. Primary key conflicts may cause import failure.", + "title": "Merge Mode" + }, + "overwrite": { + "description": "Deletes all existing data before importing, ensuring database matches backup exactly. Best for complete recovery.", + "title": "Overwrite Mode" + }, + "safety": { + "description": "Before importing, recommend exporting current database as backup to avoid data loss.", + "title": "Security Recommendation" + } + }, + "title": "Usage Instructions and Precautions" + }, + "import": { + "backupFile": "Backup file:", + "backupRecommendation": "Recommend exporting current database as backup before proceeding.", + "button": "Import Database", + "cancel": "Cancel", + "cleanFirstDescription": "Delete all existing data before importing to ensure database matches backup exactly. If unchecked, will attempt to merge data but may fail due to primary key conflicts.", + "cleanFirstLabel": "Clear existing data (overwrite mode)", + "confirm": "Confirm Import", + "confirmMerge": "You selected 'Merge Mode', which will attempt to import backup while keeping existing data.", + "confirmOverwrite": "You selected 'Overwrite Mode', which will delete all existing data before importing backup.", + "confirmTitle": "Confirm Database Import", + "descriptionFull": "Restore database from backup file. Supports PostgreSQL custom format (.dump) backup files.", + "error": "Failed to import database", + "errorUnknown": "Unknown error", + "failedMessage": "Data import failed", + "fileError": "Please select .dump format backup file", + "fileSelected": "Selected: {name} ({size} MB)", + "importing": "Importing...", + "noFileSelected": "Please select backup file first", + "parseError": "Failed to parse response data", + "progressTitle": "Import Progress", + "selectFileLabel": "Select Backup File", + "streamError": "Cannot read response stream", + "streamInterrupted": "Data stream unexpectedly interrupted", + "streamInterruptedDesc": "Import progress did not complete normally. Please check the logs and verify data integrity. Re-import if needed.", + "successCleanModeDesc": "All data has been successfully restored. Refresh your browser if the page displays incorrectly.", + "successMergeModeDesc": "Data has been successfully imported and merged. Refresh your browser if the page displays incorrectly.", + "successMessage": "Data import completed!", + "successWithWarnings": "Data import completed (with warnings)", + "successWithWarningsDesc": "Data has been successfully imported, but some existing objects were skipped. Refresh your browser or restart the application if the page displays incorrectly.", + "warningMerge": "Note: Import may fail if primary key conflicts exist.", + "warningOverwrite": "Warning: This action is irreversible, all current data will be permanently deleted!" + }, + "section": { + "cleanup": { + "description": "Clean up historical log data to free up database storage. Note: Statistics data will be retained, but log details will be permanently deleted.", + "title": "Log Cleanup" + }, + "export": { + "description": "Export complete database backup file (.dump format) for data migration or recovery.", + "title": "Data Export" + }, + "import": { + "description": "Restore database from backup file. Supports PostgreSQL custom format (.dump) backup files.", + "title": "Data Import" + }, + "status": { + "description": "View current database connection status and basic information.", + "title": "Database Status" + } + }, + "status": { + "connected": "Database connected", + "error": "Failed to get database status", + "loading": "Loading...", + "retry": "Retry", + "tables": "{count} tables", + "unavailable": "Database unavailable" + }, + "title": "Data Management" +} diff --git a/messages/en/settings/errorRules.json b/messages/en/settings/errorRules.json new file mode 100644 index 000000000..13c970139 --- /dev/null +++ b/messages/en/settings/errorRules.json @@ -0,0 +1,157 @@ +{ + "actions": { + "add": "Add", + "delete": "Delete", + "edit": "Edit", + "messages": { + "error": "Operation failed", + "success": "Operation successful" + }, + "refresh": "Refresh", + "test": "Test" + }, + "add": "Add Error Rule", + "addFailed": "Failed to create error rule", + "addSuccess": "Error rule created successfully", + "cacheStats": "Cached {totalCount} error rules", + "categories": { + "cache_limit": "Cache Control Limit", + "content_filter": "Content Filter", + "invalid_request": "Invalid Request", + "parameter_error": "Parameter Validation Failed", + "pdf_limit": "PDF Page Limit", + "prompt_limit": "Prompt Length Limit", + "thinking_error": "Thinking Format Error" + }, + "confirmDelete": "Are you sure you want to delete error rule \"{pattern}\"?", + "defaultRules": { + "cannotDelete": "Default rules cannot be deleted", + "cannotDisable": "Recommend keeping default rules enabled" + }, + "delete": "Delete Error Rule", + "deleteFailed": "Delete failed", + "deleteSuccess": "Error rule deleted successfully", + "description": "Manage client error rules that should not trigger automatic retries. When configured, errors matching these rules will be returned directly to users without retrying or counting toward provider circuit breaker thresholds.", + "dialog": { + "addDescription": "Configure error message regex patterns. Matched errors will be identified as non-retryable client errors.", + "addTitle": "Add Error Rule", + "categoryHint": "Choose the error category for classification and statistics", + "categoryLabel": "Rule Category *", + "categoryPlaceholder": "Select rule category", + "categoryRequired": "Please select rule category", + "creating": "Creating...", + "defaultRuleHint": "Default rule pattern cannot be modified", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Optional: Add description...", + "editDescription": "Modify error rule configuration. Changes will automatically refresh the cache.", + "editTitle": "Edit Error Rule", + "enableOverride": "Enable Error Override", + "enableOverrideHint": "When enabled, you can customize the error response and status code returned to clients. Original errors are still logged to the database. Currently only supports Claude API error format.", + "invalidJson": "Invalid JSON format", + "invalidPattern": "Invalid Regex", + "invalidRegex": "Invalid regex syntax", + "invalidStatusCode": "Status code must be between 400-599", + "matchFailed": "No Match", + "matchSuccess": "Match Successful", + "matchedText": "Matched Text", + "overrideResponseHint": "Leave empty to only override status code.", + "overrideResponseLabel": "Override Response (JSON)", + "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"Your custom message\"\n }\n}", + "overrideStatusCodeHint": "Leave empty to use upstream status code. Range: 400-599.", + "overrideStatusCodeLabel": "Override Status Code (Optional)", + "overrideStatusCodePlaceholder": "e.g. 400", + "patternHint": "Supports JavaScript regex syntax, e.g.: prompt is too long|invalid.*request", + "patternLabel": "Regex Pattern *", + "patternPlaceholder": "Enter regular expression...", + "patternRequired": "Please enter regex pattern", + "regexTester": "Regex Tester", + "saving": "Saving...", + "testMessageLabel": "Test Message", + "testMessagePlaceholder": "Enter error message to test...", + "useTemplate": "Claude Error Template", + "useTemplateConfirm": "Existing content will be replaced by the template. Continue?", + "validJson": "JSON format is valid" + }, + "disable": "Error rule disabled", + "edit": "Edit Error Rule", + "editFailed": "Failed to update error rule", + "editSuccess": "Error rule updated successfully", + "emptyState": "No error rules yet. Click 'Add Error Rule' in the top right to start configuration.", + "enable": "Error rule enabled", + "form": { + "fields": { + "category": "Rule Category", + "description": "Description", + "pattern": "Regex Pattern" + }, + "labels": { + "category": "Rule Category *", + "description": "Description", + "isEnabled": "Enabled Status", + "pattern": "Regex Pattern *" + }, + "placeholders": { + "category": "Select category", + "description": "Optional: Add description...", + "pattern": "e.g. prompt is too long" + } + }, + "nav": "Error Rules", + "refreshCache": "Sync Rules", + "refreshCacheFailed": "Failed to sync rules", + "refreshCacheSuccess": "Rules synced successfully, loaded {count} error rules", + "regexTester": { + "matchResult": "Match Result", + "matched": "Matched", + "notMatched": "Not Matched", + "test": "Test", + "testMessage": "Test Message", + "testMessagePlaceholder": "Enter error message to test...", + "title": "Regex Tester" + }, + "section": { + "title": "Error Rules List" + }, + "table": { + "actions": "Actions", + "category": "Rule Category", + "createdAt": "Created At", + "default": "Default", + "description": "Description", + "isDefault": "Default Rule", + "isEnabled": "Enabled Status", + "pattern": "Regex Pattern", + "status": "Status" + }, + "tester": { + "category": "Category", + "description": "Input an error message to check if it matches configured rules and see the final response.", + "finalResponse": "Override response to return", + "inputLabel": "Test Error Message", + "inputPlaceholder": "Enter an error message to test...", + "matchType": "Match type", + "matched": "Matched an error rule", + "messageRequired": "Please enter an error message to test", + "noRule": "No rule matched", + "notMatched": "No rule matched", + "overrideStatusCode": "Override status code", + "pattern": "Pattern", + "ruleInfo": "Matched rule", + "statusCodeOnlyOverride": "Only status code will be overridden, response body will use upstream error", + "testButton": "Run Test", + "testFailed": "Test failed, please try again", + "testing": "Testing...", + "title": "Error Rule Tester", + "warnings": "Configuration Warnings" + }, + "title": "Error Rules Management", + "toggleFailed": "Toggle failed", + "toggleFailedError": "Toggle failed:", + "validation": { + "categoryRequired": "Please select rule category", + "patternInvalid": "Invalid regex syntax", + "patternRequired": "Please enter regex pattern", + "patternTooComplex": "Regex is too complex", + "redosRisk": "Regex has ReDoS risk, please simplify pattern" + } +} diff --git a/messages/en/settings/errors.json b/messages/en/settings/errors.json new file mode 100644 index 000000000..ec81fcabb --- /dev/null +++ b/messages/en/settings/errors.json @@ -0,0 +1,17 @@ +{ + "addFailed": "Failed to add provider", + "addSuccess": "Add succeeded", + "deleteFailed": "Failed to delete provider", + "deleteSuccess": "Delete succeeded", + "editFailed": "Failed to update provider", + "editSuccess": "Update succeeded", + "loadFailed": "Failed to load notification settings", + "saveFailed": "Save failed", + "saveFailed_error": "Failed to save settings", + "saveSuccess": "Save succeeded", + "syncFailed": "Sync failed", + "syncSuccess": "Sync succeeded", + "testFailed": "Test failed", + "testFailedRetry": "Test failed, please retry", + "unknownError": "An exception occurred during the operation" +} diff --git a/messages/en/settings/index.ts b/messages/en/settings/index.ts new file mode 100644 index 000000000..414b5daa4 --- /dev/null +++ b/messages/en/settings/index.ts @@ -0,0 +1,101 @@ +import clientVersions from "./clientVersions.json"; +import common from "./common.json"; +import config from "./config.json"; +import data from "./data.json"; +import errorRules from "./errorRules.json"; +import errors from "./errors.json"; +import logs from "./logs.json"; +import nav from "./nav.json"; +import notifications from "./notifications.json"; +import prices from "./prices.json"; +import requestFilters from "./requestFilters.json"; +import sensitiveWords from "./sensitiveWords.json"; +import strings from "./strings.json"; + +import providersAutoSort from "./providers/autoSort.json"; +import providersFilter from "./providers/filter.json"; +import providersGuide from "./providers/guide.json"; +import providersInlineEdit from "./providers/inlineEdit.json"; +import providersList from "./providers/list.json"; +import providersSchedulingDialog from "./providers/schedulingDialog.json"; +import providersSearch from "./providers/search.json"; +import providersSection from "./providers/section.json"; +import providersSort from "./providers/sort.json"; +import providersStrings from "./providers/strings.json"; +import providersTypes from "./providers/types.json"; + +import providersFormApiTest from "./providers/form/apiTest.json"; +import providersFormButtons from "./providers/form/buttons.json"; +import providersFormCommon from "./providers/form/common.json"; +import providersFormDeleteDialog from "./providers/form/deleteDialog.json"; +import providersFormErrors from "./providers/form/errors.json"; +import providersFormFailureThresholdConfirmDialog from "./providers/form/failureThresholdConfirmDialog.json"; +import providersFormKey from "./providers/form/key.json"; +import providersFormMaxRetryAttempts from "./providers/form/maxRetryAttempts.json"; +import providersFormModelRedirect from "./providers/form/modelRedirect.json"; +import providersFormModelSelect from "./providers/form/modelSelect.json"; +import providersFormName from "./providers/form/name.json"; +import providersFormProviderTypes from "./providers/form/providerTypes.json"; +import providersFormProxyTest from "./providers/form/proxyTest.json"; +import providersFormSections from "./providers/form/sections.json"; +import providersFormStrings from "./providers/form/strings.json"; +import providersFormSuccess from "./providers/form/success.json"; +import providersFormTitle from "./providers/form/title.json"; +import providersFormUrl from "./providers/form/url.json"; +import providersFormUrlPreview from "./providers/form/urlPreview.json"; +import providersFormWebsiteUrl from "./providers/form/websiteUrl.json"; + +const providersForm = { + ...providersFormStrings, + apiTest: providersFormApiTest, + buttons: providersFormButtons, + common: providersFormCommon, + deleteDialog: providersFormDeleteDialog, + errors: providersFormErrors, + failureThresholdConfirmDialog: providersFormFailureThresholdConfirmDialog, + key: providersFormKey, + maxRetryAttempts: providersFormMaxRetryAttempts, + modelRedirect: providersFormModelRedirect, + modelSelect: providersFormModelSelect, + name: providersFormName, + providerTypes: providersFormProviderTypes, + proxyTest: providersFormProxyTest, + sections: providersFormSections, + success: providersFormSuccess, + title: providersFormTitle, + url: providersFormUrl, + urlPreview: providersFormUrlPreview, + websiteUrl: providersFormWebsiteUrl, +}; + +const providers = { + ...providersStrings, + autoSort: providersAutoSort, + filter: providersFilter, + form: providersForm, + guide: providersGuide, + inlineEdit: providersInlineEdit, + list: providersList, + schedulingDialog: providersSchedulingDialog, + search: providersSearch, + section: providersSection, + sort: providersSort, + types: providersTypes, +}; + +export default { + nav, + common, + config, + providers, + prices, + sensitiveWords, + requestFilters, + logs, + data, + clientVersions, + notifications, + errors, + errorRules, + ...strings, +}; diff --git a/messages/en/settings/logs.json b/messages/en/settings/logs.json new file mode 100644 index 000000000..6783e8695 --- /dev/null +++ b/messages/en/settings/logs.json @@ -0,0 +1,54 @@ +{ + "description": "Dynamically adjust system log level to control logging verbosity in real-time.", + "form": { + "changeNotice": "Current level is {current}, will switch to {selected} after saving", + "currentLevel": "Current Log Level", + "effectiveImmediately": "Log level changes take effect immediately without service restart.", + "failed": "Failed to set", + "failedError": "Failed to set log level", + "fetchFailed": "Failed to fetch log level", + "levelGuideDebug": "Debug (Recommended for Development): Includes detailed debug info, suitable for troubleshooting", + "levelGuideFatal": "Fatal/Error: Only errors shown, minimal logging, suitable for high-load production", + "levelGuideInfo": "Info (Recommended for Production): Shows key business events (provider selection, Session reuse, price sync) + Warnings + Errors", + "levelGuideTitle": "Log Level Guide", + "levelGuideTrace": "Trace: Extremely detailed trace information, includes all details", + "levelGuideWarn": "Warn: Includes warnings (rate limiting, circuit breaker opening, etc.) + Errors", + "save": "Save Settings", + "saving": "Saving...", + "selectLevel": "Select Log Level", + "success": "Log level set to: {level}" + }, + "levels": { + "debug": { + "description": "Debug info + All levels (Recommended for Development)", + "label": "Debug" + }, + "error": { + "description": "Error messages", + "label": "Error" + }, + "fatal": { + "description": "Fatal errors only", + "label": "Fatal" + }, + "info": { + "description": "Key business events + Warnings + Errors (Recommended for Production)", + "label": "Info" + }, + "trace": { + "description": "Extremely detailed tracing + All levels", + "label": "Trace" + }, + "warn": { + "description": "Warnings + Errors", + "label": "Warn" + } + }, + "section": { + "description": "Changes take effect immediately without service restart.", + "title": "Log Level Control" + }, + "subtitle": "Log Level Control", + "subtitleDesc": "Changes take effect immediately without restart. Useful for troubleshooting in production.", + "title": "Log Management" +} diff --git a/messages/en/settings/nav.json b/messages/en/settings/nav.json new file mode 100644 index 000000000..6262ad8e3 --- /dev/null +++ b/messages/en/settings/nav.json @@ -0,0 +1,15 @@ +{ + "apiDocs": "API Docs", + "clientVersions": "Updates", + "config": "Config", + "data": "Data", + "docs": "Documentation", + "errorRules": "Errors", + "feedback": "Feedback", + "logs": "Logs", + "notifications": "Notifications", + "prices": "Pricing", + "providers": "Providers", + "requestFilters": "Requests", + "sensitiveWords": "Filters" +} diff --git a/messages/en/settings/notifications.json b/messages/en/settings/notifications.json new file mode 100644 index 000000000..2ea6d7763 --- /dev/null +++ b/messages/en/settings/notifications.json @@ -0,0 +1,146 @@ +{ + "bindings": { + "advanced": "Advanced", + "bindTarget": "Bind target", + "boundCount": "Bound: {count}", + "editTemplateOverride": "Edit Override", + "enable": "Enable", + "enableType": "Enable this notification", + "enabledCount": "Enabled: {count}", + "noTargets": "No push targets available.", + "scheduleCron": "Cron", + "scheduleCronPlaceholder": "e.g. 0 9 * * *", + "scheduleTimezone": "Timezone", + "templateOverride": "Template Override", + "templateOverrideTitle": "Edit Template Override", + "title": "Bindings" + }, + "circuitBreaker": { + "description": "Send alert immediately when provider is fully circuit broken", + "enable": "Enable Circuit Breaker Alert", + "test": "Test Connection", + "title": "Circuit Breaker Alert", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." + }, + "costAlert": { + "description": "Trigger alert when user/provider consumption exceeds quota threshold", + "enable": "Enable Cost Alert", + "interval": "Check Interval (minutes)", + "test": "Test Connection", + "threshold": "Alert Threshold", + "thresholdHelp": "Alert when consumption reaches {percent}% of quota", + "thresholdLabel": "Alert Threshold: {percent}%", + "title": "Cost Alert", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeFeishu": "Feishu", + "webhookTypeUnknown": "Unknown platform. Please use WeCom or Feishu webhook URL", + "webhookTypeWeCom": "WeCom" + }, + "dailyLeaderboard": { + "description": "Send daily scheduled user consumption Top N leaderboard", + "enable": "Enable Daily Leaderboard", + "test": "Test Connection", + "time": "Send Time", + "timeError": "Time format error, should be HH:mm", + "timePlaceholder": "09:00", + "title": "Daily User Consumption Leaderboard", + "topN": "Show Top N", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." + }, + "description": "Configure Webhook push notifications", + "form": { + "loadError": "Failed to load notification settings", + "loading": "Loading...", + "save": "Save Settings", + "saveError": "Failed to save settings", + "saveFailed": "Save failed", + "saving": "Saving...", + "success": "Notification settings saved and tasks rescheduled", + "testError": "Test connection failed", + "testFailed": "Test failed", + "testFailedRetry": "Test failed, please retry", + "testNoResult": "Test succeeded but no result returned", + "testSuccess": "Test message sent", + "webhookRequired": "Please fill in Webhook URL first" + }, + "global": { + "description": "Enable or disable all push notification features", + "enable": "Enable Push Notifications", + "legacyModeDescription": "You are using legacy single-URL notifications. Create a push target to switch to multi-target mode.", + "legacyModeTitle": "Legacy Mode", + "title": "Notification Master Switch" + }, + "targetDialog": { + "createTitle": "Add Push Target", + "customHeaders": "Custom Headers (JSON)", + "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", + "dingtalkSecret": "DingTalk Secret", + "dingtalkSecretPlaceholder": "Optional, used for signing", + "editTitle": "Edit Push Target", + "enable": "Enable", + "errors": { + "headersInvalidJson": "Headers must be valid JSON", + "headersMustBeObject": "Headers must be a JSON object", + "headersValueMustBeString": "Header values must be strings" + }, + "name": "Target Name", + "namePlaceholder": "e.g. Ops Group", + "proxy": { + "fallbackToDirect": "Fallback to direct on proxy failure", + "title": "Proxy", + "toggle": "Toggle proxy settings", + "url": "Proxy URL", + "urlPlaceholder": "http://127.0.0.1:7890" + }, + "selectType": "Select platform type", + "telegramBotToken": "Telegram Bot Token", + "telegramBotTokenPlaceholder": "e.g. 123456:ABCDEF...", + "telegramChatId": "Telegram Chat ID", + "telegramChatIdPlaceholder": "e.g. -1001234567890", + "type": "Platform Type", + "types": { + "custom": "Custom Webhook", + "dingtalk": "DingTalk", + "feishu": "Feishu", + "telegram": "Telegram", + "wechat": "WeCom" + }, + "webhookUrl": "Webhook URL", + "webhookUrlPlaceholder": "https://example.com/webhook" + }, + "targets": { + "add": "Add Target", + "bindingsSaved": "Bindings saved", + "created": "Target created", + "delete": "Delete", + "deleteConfirm": "Are you sure you want to delete this target? Related bindings will also be removed.", + "deleteConfirmTitle": "Delete Push Target", + "deleted": "Target deleted", + "description": "Manage push targets. Supports WeCom, Feishu, DingTalk, Telegram and custom Webhook.", + "edit": "Edit", + "emptyHint": "No push targets yet. Click \"Add Target\" to create one.", + "enable": "Enable Target", + "lastTestAt": "Last Test", + "lastTestFailed": "Test Failed", + "lastTestNever": "Never tested", + "lastTestSuccess": "Test OK", + "statusDisabled": "Disabled", + "statusEnabled": "Enabled", + "test": "Test", + "testSelectType": "Select test type", + "title": "Push Targets", + "update": "Save Target", + "updated": "Target updated" + }, + "templateEditor": { + "insert": "Insert", + "jsonInvalid": "Invalid JSON", + "placeholder": "Enter JSON template...", + "placeholders": "Placeholders", + "title": "Template (JSON)" + }, + "title": "Push Notifications" +} diff --git a/messages/en/settings/prices.json b/messages/en/settings/prices.json new file mode 100644 index 000000000..0596fe13f --- /dev/null +++ b/messages/en/settings/prices.json @@ -0,0 +1,191 @@ +{ + "title": "Pricing", + "description": "Manage platform basic configuration and model pricing", + "section": { + "title": "Model Pricing", + "description": "Manage AI model pricing configuration" + }, + "searchPlaceholder": "Search model name...", + "filters": { + "all": "All", + "local": "Local", + "anthropic": "Anthropic", + "openai": "OpenAI", + "vertex": "Vertex" + }, + "badges": { + "local": "Local" + }, + "capabilities": { + "assistantPrefill": "Assistant prefill", + "computerUse": "Computer use", + "functionCalling": "Function calling", + "pdfInput": "PDF input", + "promptCaching": "Prompt caching", + "reasoning": "Reasoning", + "responseSchema": "Response schema", + "toolChoice": "Tool choice", + "vision": "Vision", + "statusSupported": "Supported", + "statusUnsupported": "Not supported", + "tooltip": "{label}: {status}" + }, + "sync": { + "button": "Sync Cloud Price Table", + "syncing": "Syncing...", + "checking": "Checking conflicts...", + "successWithChanges": "Price table updated: {added} added, {updated} updated, {unchanged} unchanged", + "successNoChanges": "Price table is up to date, no updates needed", + "failed": "Sync failed", + "failedError": "Sync failed: {error}", + "failedNoResult": "Price table updated but no result returned", + "noModels": "No model prices found", + "partialFailure": "Partial update succeeded, but {failed} models failed", + "failedModels": "Failed models: {models}", + "skippedConflicts": "Skipped {count} manual models" + }, + "conflict": { + "title": "Select Items to Overwrite", + "description": "The following models have manual prices. Check the ones to overwrite with LiteLLM prices, unchecked ones will be kept unchanged", + "searchPlaceholder": "Search models...", + "table": { + "modelName": "Model", + "manualPrice": "Manual Price", + "litellmPrice": "LiteLLM Price", + "action": "Action" + }, + "viewDiff": "View Diff", + "diffTitle": "Price Difference", + "diff": { + "field": "Field", + "manual": "Manual", + "litellm": "LiteLLM", + "inputPrice": "Input Price", + "outputPrice": "Output Price", + "imagePrice": "Image Price", + "provider": "Provider", + "mode": "Type" + }, + "pagination": { + "showing": "Showing {from}-{to} of {total}" + }, + "selectedCount": "Selected {count}/{total} models", + "noMatch": "No matching models found", + "noConflicts": "No conflicts", + "applyOverwrite": "Apply Overwrite", + "applying": "Applying..." + }, + "table": { + "modelName": "Model Name", + "provider": "Provider", + "capabilities": "Capabilities", + "price": "Price", + "inputPrice": "Input Price ($/M)", + "outputPrice": "Output Price ($/M)", + "priceInput": "In", + "priceOutput": "Out", + "pricePerRequest": "Req", + "cacheReadPrice": "Cache Read ($/M)", + "cacheCreationPrice": "Cache Create ($/M)", + "cache5m": "5m", + "cache1h": "1h+", + "copyModelId": "Copy model ID", + "updatedAt": "Updated At", + "actions": "Actions", + "typeChat": "Chat", + "typeImage": "Image", + "typeCompletion": "Completion", + "typeUnknown": "Unknown", + "loading": "Loading...", + "noMatch": "No matching models found", + "noDataTitle": "No price data available", + "noDataHint": "System has built-in price table. Use buttons above to sync or update." + }, + "pagination": { + "showing": "Showing {from}-{to} of {total}", + "previous": "Previous", + "next": "Next", + "perPageLabel": "Per page", + "perPage": "{size} per page" + }, + "stats": { + "totalModels": "{count} models total", + "searchResults": "{count} search results", + "lastUpdated": "Last updated: {time}" + }, + "dialog": { + "title": "Update Model Price Table", + "description": "Select and upload JSON or TOML file containing model pricing data", + "selectFile": "Click to select JSON/TOML file or drag and drop here", + "fileSizeLimit": "File size cannot exceed 10MB", + "fileSizeLimitSmall": "File size not exceeding 10MB", + "invalidFileType": "Please select a JSON or TOML file", + "fileTooLarge": "File size exceeds 10MB limit", + "upload": "Upload and Update", + "uploading": "Uploading...", + "updatePriceTable": "Update Price Table", + "updating": "Updating...", + "selectJson": "Select File", + "updateSuccess": "Price table updated successfully, {count} models updated", + "updateFailed": "Update failed", + "systemHasBuiltIn": "System has built-in price table", + "manualDownload": "You can also manually download", + "latestPriceTable": "cloud price table", + "andUploadViaButton": ", and upload via button above", + "cloudModelCountLoading": "Loading cloud model count...", + "cloudModelCountFailed": "Failed to load cloud model count", + "supportedModels": "Currently supports {count} models", + "results": { + "title": "Update Results", + "total": "Total: {total} models", + "success": "Success: {success}", + "failed": "Failed: {failed}", + "skipped": "Skipped: {skipped}", + "more": " (+{count})", + "details": "Details", + "viewDetails": "View detailed logs" + } + }, + "addModel": "Add Model", + "editModel": "Edit Model", + "deleteModel": "Delete Model", + "addModelDescription": "Manually add a new model price configuration", + "editModelDescription": "Edit the model price configuration", + "deleteConfirm": "Are you sure you want to delete model {name}? This action cannot be undone.", + "form": { + "modelName": "Model ID", + "modelNamePlaceholder": "e.g., gpt-5.2-codex", + "modelNameRequired": "Model ID is required", + "displayName": "Display Name (Optional)", + "displayNamePlaceholder": "e.g., GPT-5.2 Codex", + "type": "Type", + "provider": "Provider", + "providerPlaceholder": "e.g., openai", + "requestPrice": "Per-call Price ($/request)", + "inputPrice": "Input Price ($/M tokens)", + "outputPrice": "Output Price ($/M tokens)", + "outputPriceImage": "Output Price ($/image)", + "cacheReadPrice": "Cache Read Price ($/M tokens)", + "cacheCreationPrice5m": "Cache Creation Price (5m, $/M tokens)", + "cacheCreationPrice1h": "Cache Creation Price (1h+, $/M tokens)" + }, + "drawer": { + "prefillLabel": "Search existing models to prefill", + "prefillEmpty": "No matching models found", + "prefillFailed": "Search failed", + "promptCachingHint": "Enable if the model supports prompt caching", + "cachePricingTitle": "Cache Pricing" + }, + "actions": { + "edit": "Edit", + "more": "More actions", + "delete": "Delete" + }, + "toast": { + "createSuccess": "Model added", + "updateSuccess": "Model updated", + "deleteSuccess": "Model deleted", + "saveFailed": "Failed to save", + "deleteFailed": "Failed to delete" + } +} diff --git a/messages/en/settings/providers/autoSort.json b/messages/en/settings/providers/autoSort.json new file mode 100644 index 000000000..f3aae3bd7 --- /dev/null +++ b/messages/en/settings/providers/autoSort.json @@ -0,0 +1,16 @@ +{ + "button": "Auto Sort Priority", + "changeCount": "{count} providers will be updated", + "changesTitle": "Change Details", + "confirm": "Apply Changes", + "costMultiplierHeader": "Cost Multiplier", + "dialogDescription": "Automatically assign priority based on cost multiplier (lower cost = higher priority)", + "dialogTitle": "Auto Sort Provider Priority", + "error": "Failed to update priorities", + "noChanges": "No changes needed (already sorted)", + "priorityChangeHeader": "Priority Change", + "priorityHeader": "Priority", + "providerHeader": "Provider", + "providersHeader": "Providers", + "success": "Updated priority for {count} providers" +} diff --git a/messages/en/settings/providers/filter.json b/messages/en/settings/providers/filter.json new file mode 100644 index 000000000..1f1513993 --- /dev/null +++ b/messages/en/settings/providers/filter.json @@ -0,0 +1,13 @@ +{ + "circuitBroken": "Circuit Broken", + "groups": { + "all": "All", + "default": "default", + "label": "Groups:" + }, + "status": { + "active": "Active", + "all": "Any status", + "inactive": "Inactive" + } +} diff --git a/messages/en/settings/providers/form/apiTest.json b/messages/en/settings/providers/form/apiTest.json new file mode 100644 index 000000000..58649d0c2 --- /dev/null +++ b/messages/en/settings/providers/form/apiTest.json @@ -0,0 +1,157 @@ +{ + "apiFormat": "Provider type", + "apiFormatDesc": "Defaults to the routing configuration unless manually changed", + "chunksCount": "Received {count} chunks ({format})", + "chunksReceived": "Chunks received", + "close": "Close", + "copyFailed": "Failed to copy", + "copyFormat": { + "errorDetails": "Error details", + "message": "Message", + "testResult": "Test result" + }, + "copyResult": "Copy Result", + "copySuccess": "Copied to clipboard", + "customConfig": "Custom", + "customPayloadDesc": "Enter custom JSON payload to override default request body", + "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", + "disclaimer": { + "confirmConfig": "Please verify provider URL, API key, and model configuration", + "realRequest": "This test sends a real request to the provider and may consume a small quota", + "resultReference": "[IMPORTANT] Results may vary by provider and are for reference only", + "title": "Notice" + }, + "error": "Error message", + "failed": "Failed", + "fillKeyFirst": "Please fill in API key first", + "fillUrlFirst": "Please fill in provider URL first", + "formatAnthropicMessages": "Claude (Anthropic Messages API)", + "formatOpenAIChat": "OpenAI Compatible", + "formatOpenAIResponses": "Codex (Response API)", + "geminiAuthFallback": { + "desc": "Actual proxy forwarding only uses header auth, may cause request failures", + "warning": "Header auth failed, using URL parameter auth" + }, + "invalidUrl": "Provider URL is invalid (http/https only)", + "model": "Model", + "noResult": "Test succeeded but no result returned", + "presetConfig": "Preset", + "presetDesc": "Preset templates contain authentic CLI request patterns for relay service verification", + "requestConfig": "Request Configuration", + "response": "Response preview", + "responseModel": "Response model", + "responseTime": "Response time", + "resultCard": { + "copyText": { + "contentCheck": "Content Check", + "error": "Error", + "httpCheck": "HTTP Check", + "httpStatus": "HTTP Status", + "inputOutput": "Input {input} / Output {output} tokens", + "latency": "Latency", + "latencyCheck": "Latency Check", + "message": "Message", + "model": "Model", + "response": "Response", + "status": "Status", + "testedAt": "Tested At", + "usage": "Usage", + "validationDetails": "Validation Details" + }, + "dialogTitle": "Provider Test Details", + "errorDetails": { + "title": "Error Details", + "type": "Error Type" + }, + "judgment": "Judgment", + "labels": { + "content": "Content", + "error": "Error", + "firstByte": "First Byte", + "http": "HTTP", + "latency": "Latency", + "model": "Model", + "responsePreview": "Response Preview", + "totalLatency": "Total Latency" + }, + "rawResponse": { + "hint": "This shows the raw response content. You can check if the keyword exists in the response here.", + "title": "Raw Response Body" + }, + "status": { + "green": "Available", + "red": "Unavailable", + "yellow": "Degraded" + }, + "streamInfo": { + "chunksCount": "Chunks Count", + "isStreaming": "Streaming", + "no": "No", + "title": "Stream Response Info", + "yes": "Yes" + }, + "timing": { + "firstByte": "First Byte", + "testedAt": "Tested At", + "title": "Timing Info", + "totalLatency": "Total Latency" + }, + "tokenUsage": { + "cacheCreation": "Cache Creation", + "cacheRead": "Cache Read", + "input": "Input", + "output": "Output", + "title": "Token Usage" + }, + "validation": { + "content": { + "failed": "Target not found", + "passed": "Contains target string", + "target": "Target", + "title": "Tier 3: Content Validation" + }, + "failed": "Failed", + "http": { + "failed": "4xx/5xx Failed", + "passed": "2xx/3xx Success", + "statusCode": "Status Code", + "title": "Tier 1: HTTP Status" + }, + "latency": { + "actual": "Actual Latency", + "failed": "Exceeded threshold", + "passed": "Within threshold", + "title": "Tier 2: Latency Threshold" + }, + "passed": "Passed", + "timeout": "Timeout", + "title": "Three-tier Validation Details" + } + }, + "selectApiFormat": "Select provider type to test", + "selectPreset": "Select preset template", + "streamFormat": "Stream format", + "streamInfo": "Stream response info", + "streamResponse": "Stream response", + "success": "Success", + "successContains": "Success Keyword", + "successContainsDesc": "Response must contain this keyword to be considered successful", + "successContainsPlaceholder": "pong", + "testApi": "Provider Model Test", + "testFailed": "Test failed", + "testFailedRetry": "Test failed, please retry", + "testModel": "Test model", + "testModelDesc": "Leave empty to use the default model or type one manually", + "testSuccess": "Model test succeeded", + "testing": "Testing...", + "timeout": { + "desc": "Max wait time for test request (5-120 sec)", + "geminiHint": ", Gemini Thinking models recommend 60+ sec", + "label": "Timeout (seconds)" + }, + "truncatedBrief": "Showing first {length} characters, click \"View Details\" for more", + "truncatedPreview": "Showing first {length} characters, copy to see full content", + "unknown": "Unknown", + "usage": "Token usage", + "viewDetails": "View Details" +} diff --git a/messages/en/settings/providers/form/buttons.json b/messages/en/settings/providers/form/buttons.json new file mode 100644 index 000000000..9ddb50ff3 --- /dev/null +++ b/messages/en/settings/providers/form/buttons.json @@ -0,0 +1,9 @@ +{ + "collapseAll": "Collapse All Advanced Settings", + "delete": "Delete", + "expandAll": "Expand All Advanced Settings", + "submit": "Confirm Add", + "submitting": "Adding...", + "update": "Confirm Update", + "updating": "Updating..." +} diff --git a/messages/en/settings/providers/form/common.json b/messages/en/settings/providers/form/common.json new file mode 100644 index 000000000..9b1cb78c9 --- /dev/null +++ b/messages/en/settings/providers/form/common.json @@ -0,0 +1,3 @@ +{ + "core": "Core" +} diff --git a/messages/en/settings/providers/form/deleteDialog.json b/messages/en/settings/providers/form/deleteDialog.json new file mode 100644 index 000000000..657954ac2 --- /dev/null +++ b/messages/en/settings/providers/form/deleteDialog.json @@ -0,0 +1,6 @@ +{ + "cancel": "Cancel", + "confirm": "Confirm Delete", + "description": "Are you sure you want to delete provider \"{name}\"? This action cannot be undone.", + "title": "Delete Provider" +} diff --git a/messages/en/settings/providers/form/errors.json b/messages/en/settings/providers/form/errors.json new file mode 100644 index 000000000..addcd8c2e --- /dev/null +++ b/messages/en/settings/providers/form/errors.json @@ -0,0 +1,8 @@ +{ + "addFailed": "Failed to add provider", + "deleteFailed": "Failed to delete provider", + "groupTagTooLong": "Provider group tags are too long (max {max} chars total)", + "invalidUrl": "Please enter a valid API address", + "invalidWebsiteUrl": "Please enter a valid provider website URL", + "updateFailed": "Failed to update provider" +} diff --git a/messages/en/settings/providers/form/failureThresholdConfirmDialog.json b/messages/en/settings/providers/form/failureThresholdConfirmDialog.json new file mode 100644 index 000000000..91dea0a78 --- /dev/null +++ b/messages/en/settings/providers/form/failureThresholdConfirmDialog.json @@ -0,0 +1,13 @@ +{ + "cancel": "Cancel", + "confirm": "Confirm Save", + "confirmQuestion": "Are you sure you want to save this configuration?", + "descriptionDisabledAction": "disabling the circuit breaker", + "descriptionDisabledMiddle": ", which means ", + "descriptionDisabledPrefix": "You are setting the circuit breaker failure threshold to ", + "descriptionDisabledSuffix": ". The provider will not be circuit-broken due to consecutive failures.", + "descriptionDisabledValue": "0", + "descriptionHighValuePrefix": "You are setting the circuit breaker failure threshold to ", + "descriptionHighValueSuffix": ", which is a high value and may cause the provider to be circuit-broken only after many failures.", + "title": "Confirm Special Configuration" +} diff --git a/messages/en/settings/providers/form/key.json b/messages/en/settings/providers/form/key.json new file mode 100644 index 000000000..74e91988b --- /dev/null +++ b/messages/en/settings/providers/form/key.json @@ -0,0 +1,7 @@ +{ + "currentKey": "Current key: {key}", + "label": "API Key", + "leaveEmpty": "(Leave empty to keep unchanged)", + "leaveEmptyDesc": "Leave empty to keep existing key", + "placeholder": "Enter API Key" +} diff --git a/messages/en/settings/providers/form/maxRetryAttempts.json b/messages/en/settings/providers/form/maxRetryAttempts.json new file mode 100644 index 000000000..7b4c1f026 --- /dev/null +++ b/messages/en/settings/providers/form/maxRetryAttempts.json @@ -0,0 +1,5 @@ +{ + "desc": "Including the first call, the maximum number of attempts for a single provider before switching. Leave empty to use the system default.", + "label": "Max attempts per provider", + "placeholder": "2" +} diff --git a/messages/en/settings/providers/form/modelRedirect.json b/messages/en/settings/providers/form/modelRedirect.json new file mode 100644 index 000000000..48628bb53 --- /dev/null +++ b/messages/en/settings/providers/form/modelRedirect.json @@ -0,0 +1,14 @@ +{ + "add": "Add", + "addNewRule": "Add New Rule", + "alreadyExists": "Model \"{model}\" already has a redirect rule", + "currentRules": "Current Rules ({count})", + "description": "Redirect Claude Code client requested models (e.g. claude-sonnet-4.5) to upstream provider supported models (e.g. glm-4.6, gemini-pro). For cost optimization or third-party AI integration.", + "emptyState": "No redirect rules yet. After adding rules, the system will automatically rewrite model names in requests.", + "sourceEmpty": "Source model name cannot be empty", + "sourceModel": "User Requested Model", + "sourcePlaceholder": "e.g. claude-sonnet-4-5-20250929", + "targetEmpty": "Target model name cannot be empty", + "targetModel": "Actual Forwarded Model", + "targetPlaceholder": "e.g. glm-4.6" +} diff --git a/messages/en/settings/providers/form/modelSelect.json b/messages/en/settings/providers/form/modelSelect.json new file mode 100644 index 000000000..03fbb53e6 --- /dev/null +++ b/messages/en/settings/providers/form/modelSelect.json @@ -0,0 +1,20 @@ +{ + "allowAllModels": "Allow all {type} models", + "claude": "Claude", + "clear": "Clear", + "gemini": "Gemini", + "loading": "Loading...", + "manualAdd": "Manually Add Model", + "manualDesc": "Support adding any model name (not limited to price table)", + "manualPlaceholder": "Enter model name (e.g. gpt-5-turbo)", + "notFound": "Model not found", + "openai": "OpenAI", + "refresh": "Refresh model list", + "searchPlaceholder": "Search model name...", + "selectAll": "Select All ({count})", + "selectedCount": "Selected {count} models", + "sourceFallback": "Local", + "sourceFallbackDesc": "Using local price list (upstream unavailable or unsupported)", + "sourceUpstream": "Upstream", + "sourceUpstreamDesc": "Model list from upstream provider API" +} diff --git a/messages/en/settings/providers/form/name.json b/messages/en/settings/providers/form/name.json new file mode 100644 index 000000000..c29e1b62f --- /dev/null +++ b/messages/en/settings/providers/form/name.json @@ -0,0 +1,4 @@ +{ + "label": "Provider Name *", + "placeholder": "e.g. Zhipu" +} diff --git a/messages/en/settings/providers/form/providerTypes.json b/messages/en/settings/providers/form/providerTypes.json new file mode 100644 index 000000000..30d2d8583 --- /dev/null +++ b/messages/en/settings/providers/form/providerTypes.json @@ -0,0 +1,10 @@ +{ + "claude": "Claude (Anthropic Messages API)", + "claudeAuth": "Claude (Anthropic Auth Token)", + "codex": "Codex (Response API)", + "gemini": "Gemini (Google Gemini API)", + "geminiCli": "Gemini CLI", + "geminiCliDisabled": " - in development", + "openaiCompatible": "OpenAI Compatible", + "openaiCompatibleDisabled": " - in development" +} diff --git a/messages/en/settings/providers/form/proxyTest.json b/messages/en/settings/providers/form/proxyTest.json new file mode 100644 index 000000000..1dfd1e474 --- /dev/null +++ b/messages/en/settings/providers/form/proxyTest.json @@ -0,0 +1,21 @@ +{ + "connectionFailed": "Connection failed", + "connectionMethod": "Connection method:", + "connectionSuccess": "Connection successful", + "direct": "Direct", + "errorType": "Error type:", + "fillUrlFirst": "Please fill in provider URL first", + "networkError": "Network error:", + "noResult": "Test succeeded but no result returned", + "proxy": "Proxy", + "proxyError": "Proxy error:", + "responseTime": "Response time:", + "statusCode": "Status code:", + "testConnection": "Test Connection", + "testFailed": "Test failed", + "testFailedRetry": "Test failed, please retry", + "testing": "Testing...", + "timeoutError": "Connection timeout (5s). Please check:\n1. Is proxy server accessible\n2. Are proxy address and port correct\n3. Are proxy credentials correct", + "viaDirect": "(direct)", + "viaProxy": "(via proxy)" +} diff --git a/messages/en/settings/providers/form/sections.json b/messages/en/settings/providers/form/sections.json new file mode 100644 index 000000000..c3f6ad646 --- /dev/null +++ b/messages/en/settings/providers/form/sections.json @@ -0,0 +1,313 @@ +{ + "apiTest": { + "desc": "Validate whether the selected provider type and model respond correctly. Defaults to the routing configuration unless overridden.", + "summary": "Verify provider & model connectivity", + "testLabel": "Provider Model Test", + "title": "Provider Model Test" + }, + "circuitBreaker": { + "desc": "Automatically break on consecutive failures to protect overall service quality", + "failureThreshold": { + "desc": "Number of consecutive failures to trigger break", + "label": "Failure Threshold", + "placeholder": "5" + }, + "maxRetryAttempts": { + "desc": "Total tries (including the first call) before switching providers. Leave empty to use the system default.", + "label": "Max Attempts Per Provider", + "placeholder": "2" + }, + "openDuration": { + "desc": "Time before switching to half-open", + "label": "Break Duration (minutes)", + "placeholder": "30" + }, + "successThreshold": { + "desc": "Number of successes in half-open to fully recover", + "label": "Recovery Threshold", + "placeholder": "2" + }, + "summary": "{failureThreshold} failures / {openDuration} min break / {successThreshold} successes to recover / {maxRetryAttempts} attempts per provider", + "title": "Circuit Breaker" + }, + "codexStrategy": { + "desc": "Control how to handle the instructions field in Codex requests; affects gateway compatibility", + "hint": "Hint: Some strict Codex gateways (e.g. 88code, foxcode) require official instructions. Choose \"Auto\" or \"Force official\".", + "select": { + "auto": { + "desc": "Pass through client instructions; on 400 error, retry with official prompt", + "label": "Auto (recommended)" + }, + "force": { + "desc": "Always use official Codex CLI instructions (~4000+ chars)", + "label": "Force official" + }, + "keep": { + "desc": "Always pass through client instructions, no auto retry (for permissive gateways)", + "label": "Pass-through" + }, + "label": "Strategy", + "placeholder": "Select a strategy" + }, + "summary": { + "auto": "Auto (recommended)", + "force": "Force official", + "keep": "Pass-through" + }, + "title": "Codex Instructions Policy" + }, + "mcpPassthrough": { + "desc": "When enabled, pass through MCP tool calls to specified AI provider (e.g. minimax for image recognition, web search)", + "hint": "Hint: MCP passthrough allows Claude Code client to use tool capabilities provided by third-party AI providers (e.g. image recognition, web search)", + "select": { + "custom": { + "desc": "Pass through to custom MCP service (reserved, not implemented yet)", + "label": "Custom" + }, + "glm": { + "desc": "Pass through to GLM MCP service (supports image analysis, video analysis, etc.)", + "label": "GLM" + }, + "label": "Passthrough Type", + "minimax": { + "desc": "Pass through to minimax MCP service (supports image recognition, web search, etc.)", + "label": "Minimax" + }, + "none": { + "desc": "Do not enable MCP passthrough (default)", + "label": "Disabled" + }, + "placeholder": "Select passthrough type" + }, + "summary": { + "custom": "Custom (Reserved)", + "glm": "GLM", + "minimax": "Minimax", + "none": "Disabled" + }, + "title": "MCP Passthrough Configuration", + "urlAuto": "Auto-extracted: {url}", + "urlDesc": "MCP service base URL. Leave empty to auto-extract from provider URL", + "urlLabel": "MCP Passthrough URL", + "urlPlaceholder": "https://api.minimaxi.com" + }, + "proxy": { + "desc": "Configure a proxy to improve connectivity (HTTP, HTTPS, SOCKS4, SOCKS5 supported)", + "fallback": { + "desc": "When enabled, will try direct connection if proxy fails", + "label": "Fallback to direct on proxy failure" + }, + "summary": { + "configured": "Proxy configured", + "fallback": " (fallback enabled)", + "none": "Not configured" + }, + "test": { + "desc": "Test accessing provider URL via proxy (HEAD request, no credits consumed)", + "label": "Connectivity Test" + }, + "title": "Proxy", + "url": { + "formats": "Supported formats:", + "label": "Proxy URL", + "optional": "(optional)", + "placeholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080" + } + }, + "rateLimit": { + "dailyResetMode": { + "desc": { + "fixed": "Reset quota at a fixed time each day", + "rolling": "Reset 24 hours after first API call" + }, + "label": "Daily Reset Mode", + "options": { + "fixed": "Fixed Time Reset", + "rolling": "Rolling Window (24h)" + } + }, + "dailyResetTime": { + "label": "Daily Reset Time (HH:mm)" + }, + "limit5h": { + "label": "5h Spend Limit (USD)", + "placeholder": "Leave empty for unlimited" + }, + "limitConcurrent": { + "label": "Concurrent Sessions Limit", + "placeholder": "0 means unlimited" + }, + "limitDaily": { + "label": "Daily Spend Limit (USD)", + "placeholder": "Leave empty for unlimited" + }, + "limitMonthly": { + "label": "Monthly Spend Limit (USD)", + "placeholder": "Leave empty for unlimited" + }, + "limitTotal": { + "label": "Total Spend Limit (USD)", + "placeholder": "Leave empty for unlimited" + }, + "limitWeekly": { + "label": "Weekly Spend Limit (USD)", + "placeholder": "Leave empty for unlimited" + }, + "summary": { + "concurrent": "Concurrent: {count}", + "daily": "Day: ${amount} (reset ${resetTime})", + "fiveHour": "5h: ${amount}", + "monthly": "Month: ${amount}", + "none": "Unlimited", + "total": "Total: ${amount}", + "weekly": "Week: ${amount}" + }, + "title": "Rate Limit" + }, + "routing": { + "cacheTtl": { + "desc": "Force prompt cache TTL; only affects requests with cache_control.", + "label": "Cache TTL Override", + "options": { + "1h": "1 hour", + "5m": "5 minutes", + "inherit": "No override (follow client)" + } + }, + "codexOverrides": { + "parallelToolCalls": { + "help": "Controls whether parallel tool calls are allowed. \"inherit\" follows the client request. Disabling may reduce tool-call concurrency.", + "label": "Parallel Tool Calls Override", + "options": { + "false": "Force disable", + "inherit": "No override (follow client)", + "true": "Force enable" + } + }, + "reasoningEffort": { + "help": "Controls how much reasoning effort the model uses before answering. \"inherit\" follows the client request; other values force override reasoning.effort. Note: \"none\" is only supported on GPT-5.1 models; \"xhigh\" is only supported on GPT-5.1-Codex-Max. Using an unsupported value will cause an upstream error.", + "label": "Reasoning Effort Override", + "options": { + "high": "high", + "inherit": "No override (follow client)", + "low": "low", + "medium": "medium (default)", + "minimal": "minimal", + "none": "none (GPT-5.1 only)", + "xhigh": "xhigh (GPT-5.1-Codex-Max only)" + } + }, + "reasoningSummary": { + "help": "Controls whether the Responses API returns reasoning summaries. \"auto\" returns a condensed summary, \"detailed\" returns a more comprehensive one. \"inherit\" follows the client request.", + "label": "Reasoning Summary Override", + "options": { + "auto": "auto", + "detailed": "detailed", + "inherit": "No override (follow client)" + } + }, + "textVerbosity": { + "help": "Controls how verbose the model output is. \"low\" is concise, \"high\" is verbose. \"inherit\" follows the client request.", + "label": "Text Verbosity Override", + "options": { + "high": "high", + "inherit": "No override (follow client)", + "low": "low", + "medium": "medium (default)" + } + } + }, + "context1m": { + "desc": "Configure 1M context window support. Only affects Sonnet models (claude-sonnet-4-5, claude-sonnet-4). Tiered pricing applies when enabled.", + "label": "1M Context Window", + "options": { + "disabled": "Disabled", + "forceEnable": "Force Enable (for supported models)", + "inherit": "Inherit (follow client request)" + } + }, + "joinClaudePool": { + "desc": "When enabled, this provider will participate in load balancing with Claude-type providers", + "help": "Available only when there is a redirect mapping to claude-* models. When users request claude-* models, this provider also joins scheduling.", + "label": "Join Claude Routing Pool" + }, + "modelRedirects": { + "label": "Model Redirects", + "optional": "(optional)" + }, + "modelWhitelist": { + "allowAll": "✓ Allow all models (recommended)", + "desc": "Restrict which models this provider can serve. By default, a provider can serve all models of its type.", + "label": "Allowed Models", + "moreModels": "+{count} more", + "optional": "(optional)", + "selectedOnly": "Only the selected {count} models are allowed. Other models will not be routed to this provider.", + "title": "Model Allowlist" + }, + "preserveClientIp": { + "desc": "Pass x-forwarded-for / x-real-ip to upstream providers (may expose real client IP)", + "help": "Keep off by default for privacy. Enable only when upstream must see the end-user IP.", + "label": "Forward client IP" + }, + "providerType": { + "desc": "(determines scheduling policy)", + "label": "Provider Type", + "placeholder": "Select provider type" + }, + "providerTypeDesc": "Choose the API format type of the provider.", + "providerTypeDisabledNote": "Note: OpenAI Compatible is under development and currently unavailable", + "scheduleParams": { + "costMultiplier": { + "desc": "Cost multiplier. Official provider = 1.0, 20% cheaper = 0.8, 20% more expensive = 1.2 (up to 4 decimals)", + "label": "Cost Multiplier", + "placeholder": "1.0" + }, + "group": { + "desc": "Group tag. Only users whose providerGroup matches can use this provider. Example: set to \"premium\" to serve users with providerGroup=\"premium\" only", + "label": "Provider Group", + "placeholder": "e.g. premium, economy" + }, + "priority": { + "desc": "Lower value = higher priority (0 is highest). The system only chooses from the highest priority tier. Suggested: primary=0, standby=1, emergency=2", + "label": "Priority", + "placeholder": "0" + }, + "title": "Scheduling", + "weight": { + "desc": "Weighted random. Within the same priority, higher weight increases selection probability. Example 1:2:3 ≈ 16%:33%:50%", + "label": "Weight", + "placeholder": "1" + } + }, + "summary": { + "models": "{count} whitelisted models", + "none": "Not configured", + "redirects": "{count} redirects" + }, + "title": "Routing" + }, + "timeout": { + "desc": "Configure request timeout duration, 0 means disable timeout", + "disableHint": "Set to 0 to disable the timeout (for canary rollback scenarios only, not recommended)", + "nonStreamingTotal": { + "core": "true", + "desc": "Non-streaming request total timeout, range 60-1200 seconds, default 600 seconds (10 minutes)", + "label": "Non-streaming Total Timeout (seconds)", + "placeholder": "600" + }, + "streamingFirstByte": { + "core": "true", + "desc": "Streaming request first byte timeout, range 1-120 seconds, default 30 seconds", + "label": "Streaming First Byte Timeout (seconds)", + "placeholder": "30" + }, + "streamingIdle": { + "core": "true", + "desc": "Streaming request idle timeout, range 60-600 seconds, enter 0 to disable (prevent mid-stream stalling)", + "label": "Streaming Idle Timeout (seconds)", + "placeholder": "60" + }, + "summary": "First byte: {streaming}s | Stream interval: {idle}s | Non-streaming: {nonStreaming}s", + "title": "Timeout Configuration" + } +} diff --git a/messages/en/settings/providers/form/strings.json b/messages/en/settings/providers/form/strings.json new file mode 100644 index 000000000..1ffa62aee --- /dev/null +++ b/messages/en/settings/providers/form/strings.json @@ -0,0 +1,204 @@ +{ + "addRedirect": "Add Redirect", + "allowAllModels": "✓ Allow All Models (Recommended)", + "apiAddress": "API Address", + "apiAddressPlaceholder": "e.g. https://open.bigmodel.cn/api/anthropic", + "apiAddressRequired": "API Address *", + "apiKey": "API Key", + "apiKeyCurrent": "Current key:", + "apiKeyLeaveEmpty": "(Leave empty to keep unchanged)", + "apiKeyLeaveEmptyDesc": "Leave empty to keep existing key", + "apiKeyOptional": "Leave empty to keep existing key", + "apiKeyPlaceholder": "Enter API key", + "apiKeyRequired": "API Key *", + "baseUrl": "Base URL", + "baseUrlPlaceholder": "e.g. https://open.bigmodel.cn/api/anthropic", + "baseUrlRequired": "Please fill in provider URL first", + "circuitBreakerConfig": "Circuit Breaker Configuration", + "circuitBreakerConfigSummary": "{failureThreshold} failures / {openDuration} min circuit break / {successThreshold} successes to recover / {maxRetryAttempts} attempts per provider", + "circuitBreakerDesc": "Auto circuit break on consecutive failures to avoid overall service quality impact", + "clearSearch": "Clear search", + "codexInstructions": "Codex Instructions Policy", + "codexInstructionsAuto": "Auto (Recommended)", + "codexInstructionsDesc": "(determines scheduling policy)", + "codexInstructionsForce": "Force Official", + "codexInstructionsKeep": "Keep Original", + "codexStrategyAutoDesc": "Pass through client instructions, auto retry with official prompt on 400 error", + "codexStrategyAutoLabel": "Auto (Recommended)", + "codexStrategyConfig": "Codex Instructions Strategy", + "codexStrategyConfigAuto": "Auto (Recommended)", + "codexStrategyConfigForce": "Force Official", + "codexStrategyConfigKeep": "Keep Original", + "codexStrategyDesc": "Control how to handle Codex request instructions field, affects upstream gateway compatibility", + "codexStrategyForceDesc": "Always use official Codex CLI instructions (~4000+ chars)", + "codexStrategyForceLabel": "Force Official", + "codexStrategyHint": "Hint: Some strict Codex gateways (e.g. 88code, foxcode) require official instructions. Choose \"Auto\" or \"Force Official\" strategy", + "codexStrategyKeepDesc": "Always pass through client instructions, no auto retry (for lenient gateways)", + "codexStrategyKeepLabel": "Keep Original", + "codexStrategySelect": "Strategy Selection", + "collapseAll": "Collapse All Advanced Configuration", + "confirmAdd": "Confirm Add", + "confirmAddPending": "Adding...", + "confirmUpdate": "Confirm Update", + "confirmUpdatePending": "Updating...", + "costMultiplier": "Cost Multiplier", + "costMultiplierDesc": "Cost calculation multiplier. Official=1.0, 20% cheaper=0.8, 20% more expensive=1.2 (up to 4 decimal places)", + "costMultiplierLabel": "Cost Multiplier", + "costMultiplierPlaceholder": "1.0", + "deleteButton": "Delete", + "dialogDescription": "Configure provider details and advanced settings.", + "enabled": "Enabled", + "expandAll": "Expand All Advanced Configuration", + "failureThreshold": "Failure Threshold (times)", + "failureThresholdDesc": "How many consecutive failures trigger circuit break", + "failureThresholdPlaceholder": "5", + "filterAllProviders": "All Providers", + "filterByType": "Filter by Provider Type", + "filterProvider": "Filter by Provider Type", + "group": "Group", + "groupPlaceholder": "e.g. premium, economy", + "joinClaudePool": "Join Claude Scheduling Pool", + "joinClaudePoolDesc": "When enabled, this provider will participate in load balancing with Claude type providers", + "joinClaudePoolHelp": "Only available when model redirect config contains mappings to claude-* models. When enabled, this provider will also participate in scheduling when users request claude-* models.", + "leaveEmpty": "Leave empty for unlimited", + "limit0Means": "0 means unlimited", + "limit5hLabel": "5-Hour Spending Limit (USD)", + "limitAmount5h": "5-Hour Spending Limit (USD)", + "limitAmount5hDesc": "e.g. Provider B has $10 limit, $9.8 consumed", + "limitAmountMonthly": "Monthly Spending Limit (USD)", + "limitAmountWeekly": "Weekly Spending Limit (USD)", + "limitConcurrent": "Concurrent Session Limit", + "limitConcurrentDesc": "e.g. Provider C has limit of 2, currently 2 active sessions", + "limitConcurrentLabel": "Concurrent Session Limit", + "limitMonthlyLabel": "Monthly Spending Limit (USD)", + "limitPlaceholder0": "0 means unlimited", + "limitPlaceholderUnlimited": "Leave empty for unlimited", + "limitWeeklyLabel": "Weekly Spending Limit (USD)", + "modelRedirects": "Model Redirects", + "modelRedirectsAddNew": "Add New Rule", + "modelRedirectsCurrentRules": "Current Rules ({count})", + "modelRedirectsDesc": "Redirect Claude Code client model requests (e.g. claude-sonnet-4.5) to upstream provider supported models (e.g. glm-4.6, gemini-pro). For cost optimization or third-party AI integration.", + "modelRedirectsEmpty": "No redirect rules yet. System will auto-rewrite model names after adding rules.", + "modelRedirectsExists": "Model \"{model}\" already has a redirect rule", + "modelRedirectsLabel": "Model Redirects Configuration", + "modelRedirectsOptional": "(Optional)", + "modelRedirectsSourceModel": "User Requested Model", + "modelRedirectsSourcePlaceholder": "e.g. claude-sonnet-4-5-20250929", + "modelRedirectsSourceRequired": "Source model name cannot be empty", + "modelRedirectsTargetModel": "Actual Forwarded Model", + "modelRedirectsTargetPlaceholder": "e.g. glm-4.6", + "modelRedirectsTargetRequired": "Target model name cannot be empty", + "modelWhitelist": "Model Whitelist", + "modelWhitelistAllowAll": "Allow all {type} models", + "modelWhitelistAllowAllClause": "Allow all Claude models", + "modelWhitelistAllowAllOpenAI": "Allow all OpenAI models", + "modelWhitelistClear": "Clear", + "modelWhitelistDesc": "Limit models this provider can handle. By default, provider can handle all models of its type.", + "modelWhitelistLabel": "Allowed Models", + "modelWhitelistLoading": "Loading...", + "modelWhitelistManualAdd": "Manually Add Model", + "modelWhitelistManualDesc": "Support adding any model name (not limited to price table)", + "modelWhitelistManualPlaceholder": "Enter model name (e.g. gpt-5-turbo)", + "modelWhitelistNotFound": "Model not found", + "modelWhitelistSearchPlaceholder": "Search model name...", + "modelWhitelistSelectAll": "Select All ({count})", + "modelWhitelistSelected": "Selected {count} models", + "modelWhitelistSelectedOnly": "Only allow selected {count} models. Requests for other models won't be routed to this provider.", + "namePlaceholder": "Enter provider name", + "openDuration": "Circuit Break Duration (minutes)", + "openDurationDesc": "How long before auto entering half-open state", + "openDurationPlaceholder": "30", + "priority": "Priority", + "priorityDesc": "Lower number = higher priority (0 is highest). System only selects from highest priority providers. Recommendation: Main=0, Backup=1, Emergency=2", + "priorityLabel": "Priority", + "priorityPlaceholder": "0", + "providerGroupDesc": "Provider group tag. Only users with matching providerGroup can use this provider. Example: Set to \"premium\" to allow only providerGroup=\"premium\" users", + "providerGroupLabel": "Provider Group", + "providerGroupPlaceholder": "e.g. premium, economy", + "providerName": "Provider Name", + "providerNamePlaceholder": "e.g. Zhipu", + "providerNameRequired": "Provider Name *", + "providerType": "Provider Type", + "providerTypeDesc": "Select the API format type for the provider.", + "providerTypeDisabledNote": "Note: Gemini CLI and OpenAI Compatible types are under development", + "proxy": "Proxy", + "proxyAddressFormats": "Supported formats:", + "proxyAddressLabel": "Proxy Address", + "proxyAddressOptional": "(Optional)", + "proxyAddressPlaceholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080", + "proxyConfig": "Proxy Configuration", + "proxyConfigDesc": "Configure proxy server to improve provider connectivity (supports HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyConfigNone": "Not configured", + "proxyConfigSummary": "Proxy configured", + "proxyConfigSummaryFallback": " (fallback enabled)", + "proxyConfigured": "Proxy configured", + "proxyFallback": "Proxy Fallback", + "proxyFallbackDesc": "When enabled, auto try direct connection on proxy failure", + "proxyFallbackLabel": "Fallback to direct on proxy failure", + "proxyNotConfigured": "Not configured", + "proxyTestButton": "Test Connection", + "proxyTestDesc": "Test provider URL access via configured proxy (uses HEAD request, no quota consumption)", + "proxyTestFailed": "Connection Failed", + "proxyTestFillUrl": "Please fill in provider URL first", + "proxyTestLabel": "Connection Test", + "proxyTestNetworkError": "Network error: {error}", + "proxyTestProxyError": "Proxy error: {error}", + "proxyTestResponseTime": "Response time: {time}", + "proxyTestResultConnectionMethod": "Connection method: {via}", + "proxyTestResultConnectionMethodDirect": "Direct", + "proxyTestResultConnectionMethodProxy": "Proxy", + "proxyTestResultErrorType": "Error type: {type}", + "proxyTestResultFailed": "Connection failed", + "proxyTestResultMessage": "{message}", + "proxyTestResultResponseTime": "Response time: {time}ms", + "proxyTestResultStatusCode": "Status code: {code}", + "proxyTestResultSuccess": "Connection successful {via}", + "proxyTestStatusCode": "| Status code: {code}", + "proxyTestSuccess": "Connection Successful", + "proxyTestTesting": "Testing...", + "proxyTestTimeout": "Connection timeout (5s). Please check:\n1. Is proxy server accessible\n2. Are proxy address and port correct\n3. Are proxy credentials correct", + "proxyTestViaDirect": "(direct)", + "proxyTestViaProxy": "(via proxy)", + "proxyUrl": "Proxy Address", + "proxyUrlPlaceholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080", + "rateLimitConfig": "Rate Limit Configuration", + "rateLimitConfigNone": "Unlimited", + "rateLimitConfigSummary": "5h: ${fiveHour}, Weekly: ${weekly}, Monthly: ${monthly}, Concurrent: {concurrent}", + "remark": "Remark", + "remarkPlaceholder": "Optional: Add notes...", + "removeRedirect": "Remove Redirect", + "routingConfig": "Routing Configuration", + "routingConfigNone": "Not configured", + "routingConfigSummary": "{models} model whitelist, {redirects} redirects", + "scheduleParams": "Scheduling Parameters", + "searchClear": "Clear search", + "searchPlaceholder": "Search provider name, URL, remark...", + "selectProviderType": "Select provider type", + "sort": "Sort Providers", + "sortByCost": "By Cost", + "sortByCreated": "By Created (New-Old)", + "sortByName": "By Name (A-Z)", + "sortByPriority": "By Priority (High-Low)", + "sortByWeight": "By Weight (High-Low)", + "sourceModel": "Source Model Name", + "sourceModelPlaceholder": "e.g. claude-sonnet-4-5-20250929", + "sourceModelRequired": "Source model name cannot be empty", + "successThreshold": "Recovery Threshold (times)", + "successThresholdDesc": "How many successes in half-open state to fully recover", + "successThresholdPlaceholder": "2", + "targetModel": "Target Model Name", + "targetModelPlaceholder": "e.g. glm-4.6", + "targetModelRequired": "Target model name cannot be empty", + "testProxy": "Test Connection", + "testProxyFailed": "Failed to test proxy connection", + "testProxyFailedError": "Connection test failed:", + "testProxySuccess": "Proxy connection successful", + "validUrlRequired": "Please enter a valid API address", + "websiteUrlDesc": "Provider website URL for quick access", + "websiteUrlInvalid": "Please enter a valid provider website URL", + "websiteUrlPlaceholder": "https://example.com", + "weight": "Weight", + "weightDesc": "Weighted random probability. Within same priority, higher weight = higher selection probability. E.g. weights 1:2:3 = probabilities 16%:33%:50%", + "weightLabel": "Weight", + "weightPlaceholder": "1" +} diff --git a/messages/en/settings/providers/form/success.json b/messages/en/settings/providers/form/success.json new file mode 100644 index 000000000..d877e6686 --- /dev/null +++ b/messages/en/settings/providers/form/success.json @@ -0,0 +1,4 @@ +{ + "created": "Provider added successfully", + "createdDesc": "Provider \"{name}\" has been added" +} diff --git a/messages/en/settings/providers/form/title.json b/messages/en/settings/providers/form/title.json new file mode 100644 index 000000000..fb44c8b68 --- /dev/null +++ b/messages/en/settings/providers/form/title.json @@ -0,0 +1,4 @@ +{ + "create": "Add Provider", + "edit": "Edit Provider" +} diff --git a/messages/en/settings/providers/form/url.json b/messages/en/settings/providers/form/url.json new file mode 100644 index 000000000..7b3c8a6ed --- /dev/null +++ b/messages/en/settings/providers/form/url.json @@ -0,0 +1,4 @@ +{ + "label": "API Address *", + "placeholder": "e.g. https://open.bigmodel.cn/api/anthropic" +} diff --git a/messages/en/settings/providers/form/urlPreview.json b/messages/en/settings/providers/form/urlPreview.json new file mode 100644 index 000000000..20db3a9d5 --- /dev/null +++ b/messages/en/settings/providers/form/urlPreview.json @@ -0,0 +1,9 @@ +{ + "copy": "Copy", + "copyFailed": "Copy failed", + "copySuccess": "Copied {name} to clipboard", + "duplicatePath": "Duplicate path detected", + "invalidUrl": "Invalid URL format", + "invalidUrlDesc": "Please enter a valid HTTP/HTTPS address", + "title": "URL Concatenation Preview" +} diff --git a/messages/en/settings/providers/form/websiteUrl.json b/messages/en/settings/providers/form/websiteUrl.json new file mode 100644 index 000000000..173b16d85 --- /dev/null +++ b/messages/en/settings/providers/form/websiteUrl.json @@ -0,0 +1,5 @@ +{ + "desc": "Provider official website for quick access", + "label": "Provider Website", + "placeholder": "https://example.com" +} diff --git a/messages/en/settings/providers/guide.json b/messages/en/settings/providers/guide.json new file mode 100644 index 000000000..e668ab37a --- /dev/null +++ b/messages/en/settings/providers/guide.json @@ -0,0 +1,120 @@ +{ + "after": "After:", + "before": "Before:", + "bestPracticesConcurrent": "• Concurrent Control: Set session concurrency by provider API limits", + "bestPracticesCost": "• Cost Multiplier: Official=1.0, Self-hosted can be 0.8-1.2", + "bestPracticesLimit": "• Limit Settings: Set 5h, 7d, 30d limits based on budget", + "bestPracticesPriority": "• Priority Settings: Core providers=0, Backup=1-3", + "bestPracticesTitle": "Best Practices", + "bestPracticesWeight": "• Weight Config: Set weight by capacity (higher capacity = higher weight)", + "circuitBreaker": "Circuit Breaker Check", + "circuitBreakerOpen": "A filtered, remaining: B, C, D", + "circuitBreakerRecovery": "A automatically recovers to half-open after 60 seconds", + "circuitBreakerRecovery5h": "Auto recovery after 5-hour sliding window", + "costOptimize": "2. Cost Optimization: Within same priority, lower cost multiplier has higher probability", + "costSort": "Cost-based Sorting Fallback", + "costSortExample": "All providers: A (default), B (premium), C (premium), D (economy)", + "costSortProb": "Lower cost C has higher selection probability", + "costSortResult": "After sorting: C (0.8x), A (1.0x)", + "decision": "Decision:", + "group": "User Group Filtering", + "groupDesc": "If user has provider group specified, system prioritizes selection from that group", + "groupDowngrade": "Log warning and select from global provider pool", + "groupExample": "User configured providerGroup = 'premium'", + "groupFallback": "If no available providers in user group, fallback to all providers", + "groupFiltered": "Select only from A and C, B and D filtered", + "groupUnavailable": "All providers in user group 'vip' are disabled or over limit", + "health": "Health Filtering (Circuit Breaker + Rate Limit)", + "healthCheck": "Check if B is enabled and healthy", + "healthCheckAmountLimit": "Check if spending exceeds limits (5h, 7d, 30d)", + "healthCheckAmountLimitExample": "Provider B has $10 limit (5h), $9.8 consumed", + "healthCheckCircuit": "Provider A failed 5 times, circuit breaker: open", + "healthCheckConcurrent": "Check if current active session count exceeds limit", + "healthCheckConcurrentExample": "Provider C limit 2, currently 2 active sessions", + "healthFilter": "3. Health Filtering: Auto skip circuit-broken or over-limit providers", + "healthFiltered": "B filtered (near limit), remaining: C, D", + "healthFiltered2": "C filtered (full), remaining: D", + "history": "Check Request History", + "historyDesc": "Query providers used by this API Key in last 10 seconds", + "priority": "Priority Layering", + "priorityExample": "4 enabled providers with different priorities", + "priorityFirst": "1. Priority First: Select only from highest priority (lowest number) providers", + "priorityResult": "Filtered to highest priority (0) providers: A, C", + "priorityStep": "System first filters by priority, selecting only from highest priority providers", + "randomResult": "Finally selected C randomly", + "randomSelect": "Weighted Random", + "reset": "Manual Circuit Breaker Reset", + "resetSuccess": "Circuit breaker reset", + "scenario1Desc": "System first filters by priority, selecting only from highest priority providers", + "scenario1Step1": "Initial State", + "scenario1Step1After": "Filtered to highest priority (0) providers: A, C", + "scenario1Step1Before": "Provider A (priority 0), B (priority 1), C (priority 0), D (priority 2)", + "scenario1Step1Decision": "Select only from A and C, B and D filtered out", + "scenario1Step1Desc": "4 enabled providers with different priorities", + "scenario1Step2": "Cost Sorting", + "scenario1Step2After": "After sorting: C (0.8x), A (1.0x)", + "scenario1Step2Before": "A (cost 1.0x), C (cost 0.8x)", + "scenario1Step2Decision": "Lower cost C has higher selection probability", + "scenario1Step2Desc": "Within same priority, sort by cost multiplier low to high", + "scenario1Step3": "Weighted Random", + "scenario1Step3After": "C has 75% probability, A has 25%", + "scenario1Step3Before": "C (weight 3), A (weight 1)", + "scenario1Step3Decision": "Finally randomly selected C", + "scenario1Step3Desc": "Use weight for random selection, higher weight = higher probability", + "scenario1Title": "Priority Layering", + "scenario2Desc": "If user has provider group specified, system prioritizes selection from that group", + "scenario2Step1": "Check User Group", + "scenario2Step1After": "Filtered to 'premium' group: B, C", + "scenario2Step1Before": "All providers: A (default), B (premium), C (premium), D (economy)", + "scenario2Step1Decision": "Select only from B and C", + "scenario2Step1Desc": "User configured providerGroup = 'premium'", + "scenario2Step2": "Group Fallback", + "scenario2Step2After": "Fallback to all enabled providers: A, B, C, D", + "scenario2Step2Before": "All providers in user group 'vip' disabled or over limit", + "scenario2Step2Decision": "Log warning and select from global provider pool", + "scenario2Step2Desc": "If no available providers in user group, fallback to all providers", + "scenario2Title": "User Group Filtering", + "scenario3Desc": "System auto filters circuit-broken or over-limit providers", + "scenario3Step1": "Circuit Breaker Check", + "scenario3Step1After": "A filtered, remaining: B, C, D", + "scenario3Step1Before": "Provider A failed 5 times, circuit breaker: open", + "scenario3Step1Decision": "A auto recovers to half-open after 60s", + "scenario3Step1Desc": "Circuit breaker opens after 5 consecutive failures, unavailable for 60s", + "scenario3Step2": "Amount Rate Limit", + "scenario3Step2After": "B filtered (near limit), remaining: C, D", + "scenario3Step2Before": "Provider B 5h limit $10, consumed $9.8", + "scenario3Step2Decision": "Auto recovery after 5h sliding window", + "scenario3Step2Desc": "Check if spending exceeds limits (5h, 7d, 30d)", + "scenario3Step3": "Concurrent Session Limit", + "scenario3Step3After": "C filtered (full), remaining: D", + "scenario3Step3Before": "Provider C concurrent limit 2, currently 2 active sessions", + "scenario3Step3Decision": "Auto release after session expiry (5 min)", + "scenario3Step3Desc": "Check if active session count exceeds configured concurrent limit", + "scenario3Title": "Health Filtering (Circuit Breaker + Rate Limit)", + "scenario4Desc": "Consecutive chats prioritize using same provider, leveraging Claude context cache", + "scenario4Step1": "Check Request History", + "scenario4Step1After": "Check if B is enabled and healthy", + "scenario4Step1Before": "Last request used provider B", + "scenario4Step1Decision": "B available, reuse directly, skip random selection", + "scenario4Step1Desc": "Query providers used by this API Key in last 10 seconds", + "scenario4Step2": "Reuse Invalidation", + "scenario4Step2After": "Enter normal selection flow", + "scenario4Step2Before": "Last used provider B disabled or circuit-broken", + "scenario4Step2Decision": "Select from other available providers", + "scenario4Step2Desc": "If last used provider unavailable, reselect", + "scenario4Title": "Session Reuse Mechanism", + "scenariosTitle": "Interactive Scenario Demos", + "session": "Session Reuse Mechanism", + "sessionDesc": "If the last used provider is unavailable, reselect", + "sessionExample": "Last request used provider B", + "sessionExpired": "Session automatically released after expiration (5 minutes)", + "sessionFallback": "Select from other available providers", + "sessionLastUsed": "B is available, reuse directly, skip random selection", + "sessionReuse": "4. Session Reuse: Consecutive chats reuse same provider, saving context costs", + "sessionUnavailable": "Last used provider B is disabled or circuit-broken", + "step": "Step", + "title": "Core Principles", + "weight": "Weighted random selection based on weight", + "weightCalc": "C has 75% selection probability, A has 25%", + "weightExample": "C (weight 3), A (weight 1)" +} diff --git a/messages/en/settings/providers/inlineEdit.json b/messages/en/settings/providers/inlineEdit.json new file mode 100644 index 000000000..69669e2cf --- /dev/null +++ b/messages/en/settings/providers/inlineEdit.json @@ -0,0 +1,12 @@ +{ + "cancel": "Cancel", + "costMultiplierInvalid": "Please enter a non-negative number", + "costMultiplierLabel": "Cost Multiplier", + "priorityInvalid": "Please enter an integer >= 0", + "priorityLabel": "Priority", + "save": "Save", + "saveFailed": "Save failed", + "saveSuccess": "Saved successfully", + "weightInvalid": "Please enter an integer between 1 and 100", + "weightLabel": "Weight" +} diff --git a/messages/en/settings/providers/list.json b/messages/en/settings/providers/list.json new file mode 100644 index 000000000..6708c1d1c --- /dev/null +++ b/messages/en/settings/providers/list.json @@ -0,0 +1,37 @@ +{ + "cancelButton": "Cancel", + "circuitBroken": "Circuit Broken", + "clipboardUnavailable": "Clipboard access is blocked in this environment. Select and copy the key manually.", + "confirmDeleteMessage": "Are you sure you want to delete provider \"{name}\"? This action cannot be undone.", + "confirmDeleteTitle": "Confirm Delete Provider?", + "copyFailed": "Copy failed", + "costMultiplier": "Cost Multiplier", + "deleteButton": "Delete", + "deleteError": "An error occurred during operation", + "deleteFailed": "Delete failed", + "deleteSuccess": "Deleted successfully", + "deleteSuccessDesc": "Provider \"{name}\" has been deleted", + "getKeyFailed": "Failed to get key", + "keyCopied": "Key copied to clipboard", + "keyLoading": "Loading...", + "officialWebsite": "Official", + "priority": "Priority", + "resetCircuitFailed": "Failed to reset circuit breaker", + "resetCircuitSuccess": "Circuit breaker reset", + "resetCircuitSuccessDesc": "Provider \"{name}\" circuit breaker status cleared", + "resetUsageFailed": "Failed to reset total usage", + "resetUsageSuccess": "Total usage reset", + "resetUsageSuccessDesc": "Provider \"{name}\" total usage has been reset", + "resetUsageTitle": "Reset total usage", + "statusDisabled": "disabled", + "statusEnabled": "enabled", + "todayUsageCount": "{count} times", + "todayUsageLabel": "Today's Usage", + "toggleFailed": "Toggle failed", + "toggleSuccess": "Provider {status}", + "toggleSuccessDesc": "Provider \"{name}\" status updated", + "unknownError": "Unknown error", + "viewFullKey": "View Complete API Key", + "viewFullKeyDesc": "Please keep it safe and don't share it with others", + "weight": "Weight" +} diff --git a/messages/en/settings/providers/schedulingDialog.json b/messages/en/settings/providers/schedulingDialog.json new file mode 100644 index 000000000..a939d36a1 --- /dev/null +++ b/messages/en/settings/providers/schedulingDialog.json @@ -0,0 +1,9 @@ +{ + "after": "After:", + "before": "Before:", + "decision": "Decision:", + "description": "Understand how the system intelligently selects upstream providers for high availability and cost optimization", + "step": "Step", + "title": "Provider Scheduling Rules", + "triggerButton": "Rules" +} diff --git a/messages/en/settings/providers/search.json b/messages/en/settings/providers/search.json new file mode 100644 index 000000000..488255ba3 --- /dev/null +++ b/messages/en/settings/providers/search.json @@ -0,0 +1,7 @@ +{ + "clear": "Clear search", + "found": "Found {count} matching provider(s)", + "notFound": "No matching providers found", + "placeholder": "Search provider name, URL, notes...", + "showing": "Showing {filtered} / {total} providers" +} diff --git a/messages/en/settings/providers/section.json b/messages/en/settings/providers/section.json new file mode 100644 index 000000000..635ff4603 --- /dev/null +++ b/messages/en/settings/providers/section.json @@ -0,0 +1,5 @@ +{ + "description": "Configure upstream provider rate limiting and concurrent session limits. Leave empty for unlimited.", + "leaderboard": "Leaderboard", + "title": "Provider Management" +} diff --git a/messages/en/settings/providers/sort.json b/messages/en/settings/providers/sort.json new file mode 100644 index 000000000..7630801c9 --- /dev/null +++ b/messages/en/settings/providers/sort.json @@ -0,0 +1,8 @@ +{ + "byActualPriority": "By Actual Selection Priority", + "byCreatedAt": "By Created Time (New-Old)", + "byName": "By Name (A-Z)", + "byPriority": "By Priority (High-Low)", + "byWeight": "By Weight (High-Low)", + "placeholder": "Sort Providers" +} diff --git a/messages/en/settings/providers/strings.json b/messages/en/settings/providers/strings.json new file mode 100644 index 000000000..ce754df33 --- /dev/null +++ b/messages/en/settings/providers/strings.json @@ -0,0 +1,47 @@ +{ + "add": "Add Provider", + "addFailed": "Failed to add provider", + "addProvider": "Add Provider", + "addSuccess": "Provider added successfully", + "circuitBroken": "Circuit Broken", + "clone": "Clone Provider", + "cloneFailed": "Copy failed", + "confirmDelete": "Are you sure you want to delete this provider?", + "confirmDeleteDesc": "Are you sure you want to delete provider \"{name}\"? This action cannot be undone.", + "confirmDeleteProvider": "Confirm Delete Provider?", + "confirmDeleteProviderDesc": "Are you sure you want to delete provider \"{name}\"? This action is irreversible.", + "createProvider": "Add Provider", + "delete": "Delete Provider", + "deleteFailed": "Failed to delete provider", + "deleteSuccess": "Deleted successfully", + "description": "Configure API service providers and maintain availability status.", + "disabledStatus": "disabled", + "displayCount": "Showing {filtered} / {total} providers", + "edit": "Edit Provider", + "editFailed": "Failed to update provider", + "editProvider": "Edit Provider", + "enabledStatus": "enabled", + "keyLoading": "Loading...", + "noProviders": "No providers configured", + "noProvidersDesc": "Add your first API provider", + "notFound": "No matching providers found", + "official": "Official", + "resetCircuit": "Circuit breaker reset", + "resetCircuitDesc": "Provider \"{name}\" circuit breaker status cleared", + "resetCircuitFailed": "Failed to reset circuit breaker", + "scheduling": "Scheduling Strategy Details", + "schedulingDesc": "Understand how provider selection works with priority layering, session reuse, load balancing and failover", + "searchNoResults": "No matching providers found", + "searchResults": "Found {count} matching providers", + "subtitle": "Provider Management", + "subtitleDesc": "Configure upstream provider rate limiting and concurrent session limits. Leave empty for unlimited.", + "title": "Provider Management", + "todayUsage": "Today's Usage", + "todayUsageCount": "{count} times", + "toggleFailed": "Toggle failed", + "toggleSuccess": "Provider {status}", + "toggleSuccessDesc": "Provider \"{name}\" status updated", + "updateFailed": "Failed to update provider", + "viewKey": "View Complete API Key", + "viewKeyDesc": "Please keep it safe and don't share it with others" +} diff --git a/messages/en/settings/providers/types.json b/messages/en/settings/providers/types.json new file mode 100644 index 000000000..eafc26f00 --- /dev/null +++ b/messages/en/settings/providers/types.json @@ -0,0 +1,26 @@ +{ + "claude": { + "description": "Anthropic Official API", + "label": "Claude" + }, + "claudeAuth": { + "description": "Claude Relay Service", + "label": "Claude Auth" + }, + "codex": { + "description": "Codex CLI API", + "label": "Codex" + }, + "gemini": { + "description": "Google Gemini API", + "label": "Gemini" + }, + "geminiCli": { + "description": "Gemini CLI API", + "label": "Gemini CLI" + }, + "openaiCompatible": { + "description": "OpenAI Compatible API", + "label": "OpenAI Compatible" + } +} diff --git a/messages/en/settings/requestFilters.json b/messages/en/settings/requestFilters.json new file mode 100644 index 000000000..24a387881 --- /dev/null +++ b/messages/en/settings/requestFilters.json @@ -0,0 +1,84 @@ +{ + "actionLabel": { + "json_path": "JSON Path Replace", + "remove": "Remove Header", + "set": "Set Header", + "text_replace": "Text Replace" + }, + "add": "Add Filter", + "addFailed": "Failed to create filter", + "addSuccess": "Filter created", + "applyToAll": "Applied to all requests", + "confirmDelete": "Delete filter \"{name}\"?", + "delete": "Delete Filter", + "deleteFailed": "Delete failed", + "deleteSuccess": "Filter deleted", + "description": "Configure header removal/override and body replacement rules to sanitize requests before forwarding upstream.", + "dialog": { + "action": "Action", + "bindingGlobal": "All Providers (Global)", + "bindingGroups": "Provider Groups", + "bindingProviders": "Specific Providers", + "bindingType": "Apply To", + "clear": "Clear", + "createTitle": "Add Filter", + "description": "Description (optional)", + "editTitle": "Edit Filter", + "groupsSelected": "{count} group(s) selected", + "jsonPathPlaceholder": "e.g. messages.0.content or data.items[0].token", + "loading": "Loading...", + "matchType": "Match Type", + "matchTypeContains": "Contains", + "matchTypeExact": "Exact", + "matchTypeRegex": "Regex", + "name": "Name", + "noGroupsFound": "No groups found", + "noProvidersFound": "No providers found", + "priority": "Priority", + "providersSelected": "{count} provider(s) selected", + "replacement": "Replacement (optional)", + "replacementPlaceholder": "String or JSON, leave blank to clear", + "save": "Save", + "saving": "Saving...", + "scope": "Scope", + "searchGroups": "Search groups...", + "searchProviders": "Search providers...", + "selectAll": "Select All", + "selectGroups": "Select groups...", + "selectProviders": "Select providers...", + "target": "Target field/path", + "targetPlaceholder": "Header name or text/path", + "validation": { + "fieldRequired": "Name and target are required" + } + }, + "disable": "Disabled", + "edit": "Edit Filter", + "editFailed": "Failed to update filter", + "editSuccess": "Filter updated", + "empty": "No filters yet. Click Add Filter to configure.", + "enable": "Enabled", + "groups": "Groups", + "nav": "Request Filters", + "providers": "Providers", + "refreshCache": "Refresh Cache", + "refreshFailed": "Refresh failed", + "refreshSuccess": "Cache refreshed, loaded {count} filters", + "scopeLabel": { + "body": "Body", + "header": "Header" + }, + "table": { + "action": "Action", + "actions": "Actions", + "apply": "Apply", + "createdAt": "Created At", + "name": "Name", + "priority": "Priority", + "replacement": "Replacement", + "scope": "Scope", + "status": "Status", + "target": "Target" + }, + "title": "Request Filters" +} diff --git a/messages/en/settings/sensitiveWords.json b/messages/en/settings/sensitiveWords.json new file mode 100644 index 000000000..e3ba0ea09 --- /dev/null +++ b/messages/en/settings/sensitiveWords.json @@ -0,0 +1,55 @@ +{ + "add": "Add Sensitive Word", + "addFailed": "Failed to create sensitive word", + "addSuccess": "Sensitive word created successfully", + "cacheStats": "Cache stats: Contains({containsCount}) Exact({exactCount}) Regex({regexCount})", + "confirmDelete": "Are you sure you want to delete the sensitive word \"{word}\"?", + "delete": "Delete Sensitive Word", + "deleteFailed": "Delete failed", + "deleteSuccess": "Sensitive word deleted successfully", + "description": "Configure sensitive word filtering rules to block requests with sensitive content.", + "dialog": { + "addDescription": "Configure sensitive word filtering rules. Matched requests will not be forwarded upstream.", + "addTitle": "Add Sensitive Word", + "creating": "Creating...", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Optional: Add description...", + "editDescription": "Modify sensitive word configuration. Changes will automatically refresh the cache.", + "editTitle": "Edit Sensitive Word", + "matchTypeContains": "Contains Match - Block if text contains this word", + "matchTypeExact": "Exact Match - Block only if exact match", + "matchTypeLabel": "Match Type *", + "matchTypeRegex": "Regular Expression - Support complex pattern matching", + "saving": "Saving...", + "wordLabel": "Sensitive Word *", + "wordPlaceholder": "Enter sensitive word...", + "wordRequired": "Please enter a sensitive word" + }, + "disable": "Sensitive word disabled", + "edit": "Edit Sensitive Word", + "editFailed": "Failed to update sensitive word", + "editSuccess": "Sensitive word updated successfully", + "emptyState": "No sensitive words yet. Click 'Add Sensitive Word' in the top right to start configuration.", + "enable": "Sensitive word enabled", + "refreshCache": "Refresh Cache", + "refreshCacheFailed": "Failed to refresh cache", + "refreshCacheSuccess": "Cache refreshed successfully, loaded {count} sensitive words", + "section": { + "description": "Requests blocked by sensitive words will not be forwarded upstream and will not be charged. Supports contains matching, exact matching, and regex patterns.", + "title": "Sensitive Words List" + }, + "table": { + "actions": "Actions", + "createdAt": "Created At", + "description": "Description", + "matchType": "Match Type", + "matchTypeContains": "Contains Match", + "matchTypeExact": "Exact Match", + "matchTypeRegex": "Regular Expression", + "status": "Status", + "word": "Sensitive Word" + }, + "title": "Sensitive Words Management", + "toggleFailed": "Toggle failed", + "toggleFailedError": "Toggle failed:" +} diff --git a/messages/en/settings/strings.json b/messages/en/settings/strings.json new file mode 100644 index 000000000..9ce73cf72 --- /dev/null +++ b/messages/en/settings/strings.json @@ -0,0 +1,22 @@ +{ + "mcpPassthroughConfig": "MCP Passthrough Configuration", + "mcpPassthroughConfigCustom": "Custom (Reserved)", + "mcpPassthroughConfigGlm": "GLM", + "mcpPassthroughConfigMinimax": "Minimax", + "mcpPassthroughConfigNone": "Disabled", + "mcpPassthroughCustomDesc": "Pass through to custom MCP service (reserved, not implemented yet)", + "mcpPassthroughCustomLabel": "Custom", + "mcpPassthroughDesc": "When enabled, pass through MCP tool calls to specified AI provider (e.g. minimax for image recognition, web search)", + "mcpPassthroughGlmDesc": "Pass through to GLM MCP service (supports image analysis, video analysis, etc.)", + "mcpPassthroughGlmLabel": "GLM", + "mcpPassthroughHint": "Hint: MCP passthrough allows Claude Code client to use tool capabilities provided by third-party AI providers (e.g. image recognition, web search)", + "mcpPassthroughMinimaxDesc": "Pass through to minimax MCP service (supports image recognition, web search, etc.)", + "mcpPassthroughMinimaxLabel": "Minimax", + "mcpPassthroughNoneDesc": "Do not enable MCP passthrough (default)", + "mcpPassthroughNoneLabel": "Disabled", + "mcpPassthroughSelect": "Passthrough Type", + "mcpPassthroughUrlAuto": "Auto-extracted: {url}", + "mcpPassthroughUrlDesc": "MCP service base URL. Leave empty to auto-extract from provider URL", + "mcpPassthroughUrlLabel": "MCP Passthrough URL", + "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com" +} diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index b8ece37ac..bc0467dc5 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -81,7 +81,7 @@ "apiKey": "API キー", "statusCode": "ステータスコード", "minRetryCount": "リトライ回数≥", - "minRetryCountPlaceholder": "回数を入力(0 で制限なし)", + "minRetryCountPlaceholder": "回数を入力(0 で制限なし)", "apply": "フィルターを適用", "reset": "リセット", "last7days": "7日", @@ -165,7 +165,7 @@ "skipped": { "title": "スキップ情報", "reason": "理由", - "warmup": "Warmup 即時応答(CCH)", + "warmup": "Warmup 即時応答(CCH)", "desc": "このリクエストは Warmup プローブとして識別され、CCH が上流プロバイダーへ転送せずに直接応答しました。課金/レート制限/統計には含まれません。" }, "blocked": { @@ -189,12 +189,12 @@ "requestModel": "リクエストモデル", "actualModel": "実際の呼び出し", "billing": "課金説明", - "billingDescription": "システムはリクエストモデル({original})の価格を優先して課金します。価格表にそのモデルが存在しない場合、実際の呼び出しモデル({current})の価格を使用します。", + "billingDescription": "システムはリクエストモデル({original})の価格を優先して課金します。価格表にそのモデルが存在しない場合、実際の呼び出しモデル({current})の価格を使用します。", "billingModel": "課金モデル", "actualModelTooltip": "実際のモデル: {model}", "originalModelTooltip": "元のモデル: {model}", - "billingDescription_original": "現在の課金モード:リダイレクト前の元のモデル({original})で課金", - "billingDescription_redirected": "現在の課金モード:リダイレクト後の実際のモデル({current})で課金", + "billingDescription_original": "現在の課金モード:リダイレクト前の元のモデル({original})で課金", + "billingDescription_redirected": "現在の課金モード:リダイレクト後の実際のモデル({current})で課金", "billingOriginal": "課金: 元", "billingRedirected": "課金: 実際" }, @@ -232,8 +232,8 @@ "targetModel": "ターゲットモデル" }, "statusCodes": { - "not200": "非 200(エラー/ブロック)", - "200": "200 (成功)", + "not200": "非 200(エラー/ブロック)", + "200": "200(成功)", "400": "400 (不正なリクエスト)", "401": "401 (未認証)", "429": "429 (レート制限)", @@ -270,7 +270,7 @@ "providerRanking": "プロバイダーランキング", "providerCacheHitRateRanking": "プロバイダーキャッシュ命中率", "modelRanking": "モデルランキング", - "dailyRanking": "今日", + "dailyRanking": "本日", "weeklyRanking": "今週", "monthlyRanking": "今月", "allTimeRanking": "全期間" @@ -295,13 +295,13 @@ "provider": "プロバイダー", "model": "モデル", "cost": "コスト", - "cacheHitRequests": "キャッシュ対象リクエスト数(命中率計算対象)", + "cacheHitRequests": "キャッシュ対象リクエスト数(命中率計算対象)", "cacheHitRate": "キャッシュ命中率", "cacheReadTokens": "キャッシュ読取トークン数", "totalTokens": "総トークン数", "cacheCreationConsumedAmount": "キャッシュ作成消費額", "totalConsumedAmount": "総消費額", - "successRate": "成功率", + "successRate": "成功率(%)", "avgResponseTime": "平均応答時間", "avgTtfbMs": "平均TTFB", "avgTokensPerSecond": "平均トークン/秒" @@ -377,7 +377,7 @@ "totalInput": "総入力", "totalOutput": "総出力", "cacheCreation": "キャッシュ作成", - "cacheTtlMixed": "混合", + "cacheTtlMixed": "混在", "cacheRead": "キャッシュ読み取り", "total": "合計", "totalFee": "総費用", @@ -474,8 +474,8 @@ "unknownError": "不明なエラー", "prev": "前へ", "next": "次へ", - "orderAsc": "昇順に切り替え(古い順)", - "orderDesc": "降順に切り替え(新しい順)" + "orderAsc": "昇順に切り替え(古い順)", + "orderDesc": "降順に切り替え(新しい順)" }, "back": "戻る", "loadingError": "読み込み失敗" @@ -589,7 +589,7 @@ "todayCalls": "今日の呼び出し", "todayCost": "今日の消費", "lastUsed": "最終使用", - "actions": "操作" + "actions": "アクション" }, "detailsButton": "詳細情報", "modelStats": "モデル統計", @@ -605,7 +605,7 @@ "copyFailed": "コピーに失敗しました:", "timesUnit": "回", "provider": "プロバイダー", - "neverUsed": "未使用", + "neverUsed": "未使用(未利用)", "viewLogsTooltip": "詳細なログを表示", "logsButton": "ログ", "emptyState": { @@ -631,7 +631,7 @@ "renew90d": "90日間更新", "renew1y": "1年間更新", "renewCustom": "カスタム...", - "customPrompt": "新しい有効期限を入力してください(YYYY-MM-DD)。キャンセルするには空のままにしてください。", + "customPrompt": "新しい有効期限を入力してください(YYYY-MM-DD)。キャンセルするには空のままにしてください。", "invalidDate": "有効な日付を入力してください", "enable": "有効化", "disable": "無効化", @@ -758,15 +758,15 @@ "providerGroup": { "label": "プロバイダーグループ", "placeholder": "プロバイダーグループタグを入力し、Enterで追加", - "description": "このキーのプロバイダーグループ(既定: default)", + "description": "このキーのプロバイダーグループ(既定: default)", "defaultDescription": "default は groupTag 未設定のプロバイダーを含みます", - "descriptionWithUserGroup": "このキーのプロバイダーグループ(ユーザーのグループ: {group}、既定: default)" + "descriptionWithUserGroup": "このキーのプロバイダーグループ(ユーザーのグループ: {group}、既定: default)" }, "successTitle": "キーが正常に作成されました", "successDescription": "APIキーが正常に作成されました。", "generatedKey": { "label": "生成されたキー", - "hint": "このキーはキー一覧で管理できます(有効/無効、上限設定など)。ただし完全なキーは一度しか表示されません。今すぐコピーして安全に保管してください。" + "hint": "このキーはキー一覧で管理できます(有効/無効、上限設定など)。ただし完全なキーは一度しか表示されません。今すぐコピーして安全に保管してください。" }, "errors": { "userIdMissing": "ユーザーIDが存在しません", @@ -789,7 +789,7 @@ }, "loadingText": { "add": "作成中...", - "edit": "保存中..." + "edit": "保存しています..." }, "username": { "label": "ユーザー名", @@ -797,18 +797,18 @@ }, "note": { "label": "備考", - "placeholder": "備考を入力してください(オプション)", + "placeholder": "備考を入力してください(オプション)", "description": "ユーザーの用途や備考情報を説明するために使用されます" }, "providerGroup": { "label": "プロバイダーグループ", "placeholder": "例: default または premium,economy", - "description": "ユーザーのプロバイダーグループ(既定: default)。groupTag が未設定のプロバイダーは default に属します。" + "description": "ユーザーのプロバイダーグループ(既定: default)。groupTag が未設定のプロバイダーは default に属します。" }, "tags": { "label": "ユーザータグ", - "placeholder": "タグを入力(Enterで追加)", - "description": "分類とフィルタリングのためにタグを追加します(最大20個、各32文字まで)" + "placeholder": "タグを入力(Enterで追加)", + "description": "分類とフィルタリングのためにタグを追加します(最大20個、各32文字まで)" }, "rpm": { "label": "RPM制限", @@ -854,12 +854,12 @@ "label": "許可されたクライアント", "description": "このアカウントを使用できるCLI/IDEクライアントを制限します。空の場合は制限なし。", "customLabel": "カスタムクライアントパターン", - "customPlaceholder": "カスタムパターンを入力(例:'xcode', 'my-ide')" + "customPlaceholder": "カスタムパターンを入力(例:'xcode', 'my-ide')" }, "allowedModels": { "label": "許可モデル", - "placeholder": "モデル名を入力(Enterで追加)", - "description": "ユーザーを特定のAIモデルに制限します。空白の場合は制限なし(最大50モデル、各64文字)" + "placeholder": "モデル名を入力(Enterで追加)", + "description": "ユーザーを特定のAIモデルに制限します。空白の場合は制限なし(最大50モデル、各64文字)" } }, "deleteKeyConfirm": { @@ -890,7 +890,7 @@ "description": "プロバイダーの可用性とパフォーマンス指標をリアルタイムで監視", "nav": "可用性モニター", "status": { - "green": "正常", + "green": "正常(OK)", "red": "異常", "unknown": "不明" }, @@ -902,7 +902,7 @@ "metrics": { "systemAvailability": "システム可用性", "totalRequests": "総リクエスト数", - "successRate": "成功率", + "successRate": "成功率(%)", "avgLatency": "平均遅延", "p50Latency": "P50 遅延", "p95Latency": "P95 遅延", @@ -926,7 +926,7 @@ }, "sort": { "label": "並び替え", - "availability": "可用性", + "availability": "可用性(稼働率)", "name": "名前", "requests": "リクエスト数" }, @@ -934,12 +934,12 @@ "provider": "プロバイダー", "type": "タイプ", "status": "ステータス", - "availability": "可用性", + "availability": "可用性(稼働率)", "requests": "リクエスト", - "successRate": "成功率", + "successRate": "成功率(%)", "avgLatency": "平均遅延", "lastRequest": "最終リクエスト", - "actions": "操作" + "actions": "アクション" }, "chart": { "title": "可用性トレンド", @@ -973,8 +973,8 @@ }, "legend": { "green": "優秀 (可用性 95%+)", - "lime": "良好 (可用性 80-95%)", - "orange": "警告 (可用性 50-80%)", + "lime": "良好(可用性 80-95%)", + "orange": "警告(可用性 50-80%)", "red": "異常 (可用性 <50%)", "noData": "データなし" }, @@ -1003,7 +1003,7 @@ "loading": "読み込み中...", "error": "読み込み失敗", "totalEvents": "総イベント数", - "avgUsage": "平均使用率", + "avgUsage": "平均利用率", "affectedUsers": "影響を受けたユーザー数", "noData": "データなし", "noDataHint": "選択した時間範囲にはレート制限イベントがありません", @@ -1118,7 +1118,7 @@ "todayUsage": "本日の使用量", "todayCost": "本日の消費", "lastUsed": "最終使用", - "actions": "操作", + "actions": "アクション", "quotaButton": "クォータ使用状況を表示", "fields": { "callsLabel": "呼び出し", @@ -1182,7 +1182,7 @@ "7days": "7 日", "30days": "30 日", "90days": "90 日", - "1year": "1 年" + "1year": "1年" }, "customDate": "カスタム日付", "enableOnRenew": "同時にユーザーを有効化", @@ -1208,7 +1208,7 @@ "userEnabled": "ユーザーが有効化されました", "deleteFailed": "ユーザーの削除に失敗しました", "userDeleted": "ユーザーが削除されました", - "saving": "保存中..." + "saving": "保存しています..." }, "batchEdit": { "enterMode": "一括編集", @@ -1232,7 +1232,7 @@ "keyFields": "キーフィールド", "goBack": "戻って修正", "update": "更新を確定", - "updating": "更新中..." + "updating": "更新しています..." }, "toast": { "usersUpdated": "{count} ユーザーを更新しました", @@ -1292,12 +1292,12 @@ "description": "新規ユーザーを作成し、APIキーを設定", "saveFailed": "ユーザーの作成に失敗しました", "keyCreateFailed": "キーの作成に失敗しました", - "rollbackFailed": "ロールバックに失敗しました。ユーザーのみが作成され、キーが作成されていない可能性があります。手動でユーザーを削除してください(userId: {userId})。", + "rollbackFailed": "ロールバックに失敗しました。ユーザーのみが作成され、キーが作成されていない可能性があります。手動でユーザーを削除してください(userId: {userId})。", "createSuccess": "ユーザーが作成されました", "successTitle": "作成完了", "successDescription": "ユーザーとキーが作成されました", "generatedKey": "生成されたキー", - "keyHint": "このキーはユーザー管理ページで管理できます(有効/無効、上限設定など)。ただし完全なキーは一度しか表示されません。今すぐコピーして安全に保管してください。", + "keyHint": "このキーはユーザー管理ページで管理できます(有効/無効、上限設定など)。ただし完全なキーは一度しか表示されません。今すぐコピーして安全に保管してください。", "keysSection": "キー", "addKey": "キーを追加", "removeKey": "キーを削除", @@ -1320,7 +1320,7 @@ }, "dailyMode": { "fixed": "固定時刻でリセット", - "rolling": "ローリングウィンドウ(24時間)" + "rolling": "ローリングウィンドウ(24時間)" }, "quickValues": { "unlimited": "無制限", @@ -1346,7 +1346,7 @@ "providerGroup": { "label": "プロバイダーグループ", "placeholder": "プロバイダーグループを選択", - "noRestriction": "制限なし(すべてのプロバイダー)", + "noRestriction": "制限なし(すべてのプロバイダー)", "providerCount": "{count} 件のプロバイダー" }, "dangerZone": { @@ -1398,7 +1398,7 @@ }, "cacheTtlOverride": { "label": "Cache TTL の上書き", - "inherit": "上書きしない(プロバイダー/クライアントに従う)", + "inherit": "上書きしない(プロバイダー/クライアントに従う)", "5m": "5分", "1h": "1時間" } @@ -1416,7 +1416,7 @@ "keyStatus": { "enabled": "有効", "disabled": "無効", - "active": "正常", + "active": "正常(アクティブ)", "expired": "期限切れ", "expiringSoon": "まもなく期限切れ", "keyEnabled": "キーが有効になりました", @@ -1430,7 +1430,7 @@ "userStatus": { "enabled": "有効", "disabled": "無効", - "active": "正常", + "active": "正常(アクティブ)", "expired": "期限切れ", "expiringSoon": "まもなく期限切れ", "userEnabled": "ユーザーが有効になりました", @@ -1456,11 +1456,11 @@ }, "description": { "label": "メモ", - "placeholder": "メモを入力(任意)" + "placeholder": "メモを入力(任意)" }, "tags": { "label": "ユーザータグ", - "placeholder": "タグを入力(Enterで追加)" + "placeholder": "タグを入力(Enterで追加)" }, "providerGroup": { "label": "プロバイダーグループ", @@ -1470,7 +1470,7 @@ "label": "クライアント制限", "description": "このアカウントを使用できるCLI/IDEクライアントを制限します。空欄は制限なし。", "customLabel": "カスタムクライアントパターン", - "customPlaceholder": "パターンを入力(例:'xcode', 'my-ide')" + "customPlaceholder": "パターンを入力(例:'xcode', 'my-ide')" }, "allowedModels": { "label": "モデル制限", @@ -1533,7 +1533,7 @@ "label": "Cache TTL上書き", "description": "cache_controlを含むリクエストに対してAnthropic prompt cache TTLを強制します。", "options": { - "inherit": "上書きしない(プロバイダー/クライアントに従う)", + "inherit": "上書きしない(プロバイダー/クライアントに従う)", "5m": "5m", "1h": "1h" } @@ -1548,7 +1548,7 @@ "daily": { "mode": { "fixed": "固定時間リセット", - "rolling": "ローリングウィンドウ(24時間)" + "rolling": "ローリングウィンドウ(24時間)" } }, "overwriteHint": "このタイプは既に存在します。保存すると既存の値が上書きされます" diff --git a/messages/ja/index.ts b/messages/ja/index.ts index d5bad5de2..70f0e4ca7 100644 --- a/messages/ja/index.ts +++ b/messages/ja/index.ts @@ -11,7 +11,7 @@ import notifications from "./notifications.json"; import providerChain from "./provider-chain.json"; import providers from "./providers.json"; import quota from "./quota.json"; -import settings from "./settings.json"; +import settings from "./settings"; import ui from "./ui.json"; import usage from "./usage.json"; import users from "./users.json"; diff --git a/messages/ja/provider-chain.json b/messages/ja/provider-chain.json index a2e6c09e4..c24086d2d 100644 --- a/messages/ja/provider-chain.json +++ b/messages/ja/provider-chain.json @@ -86,7 +86,7 @@ "remaining": "残り{count}回", "status": "状態", "alreadyBroken": "すでに遮断済み", - "circuitTriggered": "⚠️ サーキットブレーカーが作動しました", + "circuitTriggered": "警告:サーキットブレーカーが作動しました", "errorDetails": "エラー詳細", "systemError": "システムエラー", "systemErrorFailed": "システムエラー(試行{attempt})", @@ -97,7 +97,7 @@ "errorMeaning": "意味: {meaning}", "meaning": "意味", "notCountedInCircuit": "このエラーはプロバイダーサーキットブレーカーにカウントされません", - "systemErrorNote": "ℹ️ このエラーはプロバイダーサーキットブレーカーにカウントされません", + "systemErrorNote": "注記:このエラーはプロバイダーサーキットブレーカーにカウントされません", "reselection": "プロバイダー再選択", "reselect": "プロバイダー再選択", "excluded": "除外済み: {providers}", diff --git a/messages/ja/settings.json b/messages/ja/settings.json deleted file mode 100644 index 01d0e9cab..000000000 --- a/messages/ja/settings.json +++ /dev/null @@ -1,2126 +0,0 @@ -{ - "clientVersions": { - "description": "クライアントバージョン要件を管理し、ユーザーが最新の安定版を使用していることを確認します。VSCodeとCLIは個別に管理されます。", - "empty": { - "description": "過去7日間に認識可能なクライアントを使用したアクティブユーザーがいません", - "title": "クライアントデータなし" - }, - "features": { - "activeWindow": "アクティブウィンドウ:", - "activeWindowDesc": "過去7日間にリクエストがあったユーザーのみを集計", - "autoDetect": "システムは各クライアントの最新安定版(GAバージョン)を自動検出します", - "blockOldVersion": "旧バージョンを使用するユーザーはHTTP 400エラーを受信し、サービスを継続使用できません", - "errorMessage": "エラーメッセージには現在のバージョンとアップグレード必要バージョン番号が含まれます", - "gaRule": "判定ルール:", - "gaRuleDesc": "1人以上のユーザーが使用しているバージョンをGAバージョンとみなします", - "recommendation": "推奨方法:", - "recommendationDesc": "まず下記のバージョン分布を確認し、新バージョンが安定していることを確認してから有効にしてください。", - "title": "機能説明", - "whatHappens": "有効化後の動作:" - }, - "section": { - "distribution": { - "description": "過去7日間のアクティブユーザーのクライアントバージョン情報を表示します。各クライアントタイプごとにGAバージョンを独立して集計します。", - "title": "クライアントバージョン分布" - }, - "settings": { - "description": "有効にすると、システムはクライアントバージョンを自動的に検出し、旧バージョンユーザーのリクエストをブロックします。", - "title": "アップグレードリマインダー設定" - } - }, - "table": { - "currentGA": "現在のGAバージョン:", - "internalType": "内部タイプ:", - "lastActive": "最終アクティブ時間", - "latest": "最新", - "needsUpgrade": "アップグレード必要", - "noUsers": "ユーザーデータなし", - "status": "ステータス", - "unknown": "不明", - "user": "ユーザー", - "usersCount": "{count}名のユーザー", - "version": "現在のバージョン" - }, - "title": "クライアント更新リマインダー", - "toggle": { - "description": "有効にすると、システムはクライアントバージョンを自動的に検出し、古いバージョンをブロックします。", - "disableSuccess": "クライアントバージョンチェックが無効になりました", - "enable": "クライアントバージョンチェックを有効にする", - "enableSuccess": "クライアントバージョンチェックが有効になりました", - "toggleFailed": "トグルに失敗しました" - } - }, - "common": { - "cancel": "キャンセル", - "completed": "完了", - "confirm": "確認", - "copied": "キーをクリップボードにコピーしました", - "copy": "コピー", - "copyFailed": "コピーに失敗しました", - "create": "作成", - "creating": "作成中...", - "delete": "削除", - "disabled": "無効", - "edit": "編集", - "empty": "結果が見つかりません", - "enabled": "有効", - "error": "不明なエラー", - "failed": "失敗", - "loading": "読み込み中...", - "none": "なし(このバージョンを使用しているユーザーなし)", - "refresh": "更新", - "reset": "リセット", - "save": "保存", - "saving": "保存中...", - "submit": "送信", - "success": "成功", - "test": "テスト", - "testing": "テスト中...", - "unlimited": "無制限", - "unlimited_desc": "無制限", - "update": "更新", - "updating": "更新中..." - }, - "config": { - "autoCleanup": "ログ自動クリーンアップ", - "autoCleanupDesc": "スケジュールに従って履歴ログを自動的にクリーンアップし、データベース容量を解放します。", - "description": "システムの基本パラメータを管理し、サイト表示と統計動作に影響します。", - "section": { - "siteParams": { - "title": "サイトパラメータ", - "description": "サイトタイトル、通貨表示単位、ダッシュボード統計表示ポリシーを設定します。" - }, - "autoCleanup": { - "title": "ログ自動クリーンアップ", - "description": "スケジュールに従って履歴ログを自動的にクリーンアップし、データベース容量を解放します。" - } - }, - "form": { - "allowGlobalView": "グローバル使用量表示を許可", - "allowGlobalViewDesc": "無効にすると、一般ユーザーはダッシュボードで自分のキーの使用統計のみを表示できます。", - "verboseProviderError": "詳細なプロバイダーエラー", - "verboseProviderErrorDesc": "有効にすると、すべてのプロバイダーが利用不可の場合に詳細なエラーメッセージ(プロバイダー数、レート制限の理由など)を返します。無効の場合は簡潔なエラーコードのみを返します。", - "interceptAnthropicWarmupRequests": "Warmup リクエストを遮断(Anthropic)", - "interceptAnthropicWarmupRequestsDesc": "有効にすると、Claude Code の Warmup プローブ要求は CCH が直接短い応答を返し、上流プロバイダーへのリクエストを回避します。ログには残りますが、課金/レート制限/統計には含まれません。", - "enableThinkingSignatureRectifier": "thinking 署名整流を有効化", - "enableThinkingSignatureRectifierDesc": "Anthropic プロバイダーで thinking 署名の不整合や不正なリクエストエラーが発生した場合、thinking 関連ブロックを削除して同一プロバイダーへ1回だけ再試行します(既定で有効)。", - "enableResponseFixer": "レスポンス整流を有効化", - "enableResponseFixerDesc": "上流応答の一般的な形式問題(エンコーディング、SSE、途切れた JSON)を自動修復します(既定で有効)。", - "responseFixerFixEncoding": "エンコーディングを修復", - "responseFixerFixEncodingDesc": "BOM/NULL バイトを除去し、無効な UTF-8 を正規化します。", - "responseFixerFixSseFormat": "SSE 形式を修復", - "responseFixerFixSseFormatDesc": "不足している data: 前置きを補い、改行を正規化し、よくあるフィールド形式を修正します。", - "responseFixerFixTruncatedJson": "途切れた JSON を修復", - "responseFixerFixTruncatedJsonDesc": "未閉じの括弧/引用符を補い、末尾カンマを除去し、必要に応じて null を補完します。", - "cleanupSchedule": "クリーンアップスケジュール", - "cleanupScheduleDesc": "自動クリーンアップの実行スケジュールを選択します", - "configUpdated": "システム設定が更新されました。ページが更新され、通貨表示の変更が適用されます。", - "currencyDisplay": "通貨表示単位", - "currencyDisplayPlaceholder": "通貨単位を選択", - "currencyDisplayDesc": "変更後、システムのすべてのページとAPIインターフェースは対応する通貨記号を使用します(記号のみ、為替レート変換なし)。", - "keepDays": "保持日数", - "keepDaysDesc": "指定した日数より古いログをクリーンアップします", - "saveFailed": "保存に失敗しました", - "saveSuccess": "正常に保存されました", - "saveError": "保存に失敗しました", - "saveSettings": "設定を保存", - "siteTitle": "サイトタイトル", - "siteTitlePlaceholder": "例:Claude Code Hub", - "siteTitleRequired": "サイトタイトルは空にできません", - "siteTitleDesc": "ブラウザタブのタイトルとシステムのデフォルト表示名を設定するために使用されます。", - "enableAutoCleanup": "自動クリーンアップを有効にする", - "enableAutoCleanupDesc": "スケジュールに従って履歴ログを自動的にクリーンアップします", - "cleanupRetentionDays": "保持日数", - "cleanupRetentionDaysRequired": "保持日数 *", - "cleanupRetentionDaysPlaceholder": "30", - "cleanupRetentionDaysDesc": "この日数を超えるログは自動的にクリーンアップされます(範囲:1-365日)", - "cleanupScheduleLabel": "実行時間(Cron)", - "cleanupScheduleRequired": "実行時間(Cron)*", - "cleanupSchedulePlaceholder": "0 2 * * *", - "cleanupScheduleCronDesc": "Cron式、デフォルト:0 2 * * *(毎日午前2時)", - "cleanupScheduleCronExample": "例:0 3 * * 0(毎週日曜日午前3時)", - "cleanupBatchSize": "バッチサイズ", - "cleanupBatchSizeRequired": "バッチサイズ *", - "cleanupBatchSizePlaceholder": "10000", - "cleanupBatchSizeDesc": "バッチごとに削除するレコード数(範囲:1000-100000、推奨10000)", - "saveConfig": "設定を保存", - "autoCleanupSaved": "自動クリーンアップ設定が保存されました", - "currencies": { - "USD": "$ 米ドル (USD)", - "CNY": "¥ 人民元 (CNY)", - "EUR": "€ ユーロ (EUR)", - "JPY": "¥ 日本円 (JPY)", - "GBP": "£ 英ポンド (GBP)", - "HKD": "HK$ 香港ドル (HKD)", - "TWD": "NT$ 新台湾ドル (TWD)", - "KRW": "₩ 韓国ウォン (KRW)", - "SGD": "S$ シンガポールドル (SGD)" - }, - "billingModelSource": "課金モデルソース", - "billingModelSourcePlaceholder": "課金モデルソースを選択", - "billingModelSourceDesc": "モデルリダイレクト時に課金に使用するモデルを設定します。「リダイレクト前」はユーザーがリクエストした元のモデル、「リダイレクト後」は実際に呼び出されたモデルを使用します。", - "billingModelSourceOptions": { - "original": "リダイレクト前(元のモデル)", - "redirected": "リダイレクト後(実際のモデル)" - } - }, - "siteSettings": "サイトパラメータ", - "siteSettingsDesc": "サイトタイトル、通貨表示単位、ダッシュボード統計表示方針を設定します。", - "title": "基本設定" - }, - "data": { - "cleanup": { - "rangeLabel": "クリーンアップ範囲", - "range": { - "7days": "1週間前のログ(7日)", - "30days": "1ヶ月前のログ(30日)", - "90days": "3ヶ月前のログ(90日)", - "180days": "6ヶ月前のログ(180日)" - }, - "rangeDescription": { - "7days": "1週間前", - "30days": "1ヶ月前", - "90days": "3ヶ月前", - "180days": "6ヶ月前", - "default": "{days}日前" - }, - "willClean": "{range}のすべてのログレコードをクリーンアップします", - "button": "ログをクリーンアップ", - "confirmTitle": "ログクリーンアップの確認", - "confirmWarning": "この操作は{range}のすべてのログレコードを完全に削除し、復元できません。", - "previewLoading": "集計中...", - "previewCount": "{count}件のログレコードを削除します", - "previewError": "プレビュー情報を取得できません", - "statisticsRetained": "✓ 統計データは保持されます(トレンド分析用)", - "logsDeleted": "✗ ログ詳細は削除されます(リクエスト/レスポンス内容、エラー情報など)", - "backupRecommendation": "推奨:クリーンアップ前にデータベースバックアップをエクスポートして、データ復元が必要な場合に備えてください。", - "cancel": "キャンセル", - "confirm": "クリーンアップを確認", - "cleaning": "クリーンアップ中...", - "successMessage": "{count}件のログレコードをクリーンアップしました({batches}バッチ、所要時間{duration}秒)", - "failed": "クリーンアップ失敗", - "error": "ログのクリーンアップに失敗しました", - "descriptionWarning": "履歴ログデータをクリーンアップしてデータベースストレージを解放します。注:統計データは保持されますが、ログ詳細は完全に削除されます。" - }, - "description": "データベースのバックアップと復元を管理し、完全なインポート/エクスポートとログクリーンアップをサポートします。", - "export": { - "button": "データベースをエクスポート", - "exporting": "エクスポート中...", - "successMessage": "データベースのエクスポートに成功しました!", - "failed": "エクスポート失敗", - "error": "データベースのエクスポートに失敗しました", - "descriptionFull": "完全なデータベースバックアップファイル(.dump形式)をエクスポートし、データ移行または復旧に使用できます。バックアップはPostgreSQL custom formatを使用し、自動圧縮され、異なるデータベースバージョンと互換性があります。" - }, - "guide": { - "title": "使用説明と注意事項", - "items": { - "cleanup": { - "title": "ログクリーンアップ", - "description": "履歴ログを物理的に削除します(取り消せません)。統計テーブルは保持されます。クリーンアップ前にデータベースバックアップをエクスポートすることをお勧めします。" - }, - "format": { - "title": "バックアップ形式", - "description": "PostgreSQL custom format(.dump)を使用し、自動圧縮で異なるデータベースバージョンと互換性があります。" - }, - "overwrite": { - "title": "上書きモード", - "description": "インポート前にすべての既存データを削除します。完全復元に最適です。" - }, - "merge": { - "title": "統合モード", - "description": "既存データを保持し、バックアップからのデータを挿入しようとします。主キーの競合がインポート失敗を引き起こす可能性があります。" - }, - "safety": { - "title": "セキュリティ推奨", - "description": "インポート前に現在のデータベースをバックアップとしてエクスポートすることをお勧めします。" - }, - "environment": { - "title": "環境要件", - "description": "Docker Composeデプロイメントが必要です。ローカル開発環境ではサポートされない可能性があります。" - } - } - }, - "import": { - "selectFileLabel": "バックアップファイルを選択", - "fileSelected": "選択済み:{name}({size} MB)", - "fileError": ".dump形式のバックアップファイルを選択してください", - "noFileSelected": "最初にバックアップファイルを選択してください", - "cleanFirstLabel": "既存データをクリア(上書きモード)", - "cleanFirstDescription": "インポート前にすべての既存データを削除し、データベースがバックアップと完全に一致するようにします。チェックしない場合、データのマージを試みますが、主キーの競合により失敗する可能性があります。", - "button": "データベースをインポート", - "importing": "インポート中...", - "progressTitle": "インポート進行状況", - "confirmTitle": "データベースインポートの確認", - "confirmOverwrite": "「上書きモード」を選択しました。これにより、すべての既存データが削除された後、バックアップがインポートされます。", - "confirmMerge": "「マージモード」を選択しました。これにより、既存データを保持しながらバックアップのインポートを試みます。", - "warningOverwrite": "⚠️ 警告:この操作は元に戻せません。すべての現在のデータが完全に削除されます!", - "warningMerge": "⚠️ 注意:主キーの競合が存在する場合、インポートが失敗する可能性があります。", - "backupFile": "バックアップファイル:", - "backupRecommendation": "この操作を実行する前に、現在のデータベースをバックアップとしてエクスポートすることをお勧めします。", - "cancel": "キャンセル", - "confirm": "インポートを確認", - "successMessage": "データインポート完了!", - "failedMessage": "データインポート失敗、詳細ログを確認してください", - "error": "データベースのインポートに失敗しました", - "streamError": "レスポンスストリームを読み取れません", - "errorUnknown": "不明なエラー", - "descriptionFull": "バックアップファイルからデータベースを復元します。PostgreSQL custom format(.dump)形式のバックアップファイルをサポートします。" - }, - "status": { - "loading": "読み込み中...", - "error": "データベースステータスの取得に失敗しました", - "retry": "再試行", - "connected": "データベース接続正常", - "unavailable": "データベース利用不可", - "tables": "{count} テーブル" - }, - "title": "データ管理", - "section": { - "status": { - "title": "データベースステータス", - "description": "現在のデータベース接続状態と基本情報を表示します。" - }, - "cleanup": { - "title": "ログクリーンアップ", - "description": "履歴ログデータをクリーンアップしてデータベースストレージを解放します。注:統計データは保持されますが、ログ詳細は完全に削除されます。" - }, - "export": { - "title": "データエクスポート", - "description": "完全なデータベースバックアップファイル(.dump形式)をエクスポートし、データ移行または復旧に使用できます。" - }, - "import": { - "title": "データインポート", - "description": "バックアップファイルからデータベースを復元します。PostgreSQL custom format(.dump)形式のバックアップファイルをサポートします。" - } - } - }, - "errors": { - "saveSuccess": "保存に成功しました", - "saveFailed": "保存に失敗しました", - "saveFailed_error": "設定の保存に失敗しました", - "addSuccess": "追加に成功しました", - "addFailed": "プロバイダーの追加に失敗しました", - "editSuccess": "更新に成功しました", - "editFailed": "プロバイダーの更新に失敗しました", - "deleteSuccess": "削除に成功しました", - "deleteFailed": "プロバイダーの削除に失敗しました", - "syncSuccess": "同期に成功しました", - "syncFailed": "同期に失敗しました", - "testFailed": "テストに失敗しました", - "testFailedRetry": "テストに失敗しました。再試行してください", - "loadFailed": "通知設定の読み込みに失敗しました", - "unknownError": "操作中に例外が発生しました" - }, - "logs": { - "description": "システムログレベルを動的に調整してロギング詳細度をリアルタイムで制御します。", - "subtitle": "ログレベルコントロール", - "subtitleDesc": "変更はすぐに有効になります、再起動不要。本番環境でのトラブルシューティングに便利です。", - "section": { - "title": "ログレベルコントロール", - "description": "変更はすぐに有効になります、サービス再起動不要。" - }, - "levels": { - "fatal": { - "label": "Fatal", - "description": "致命的エラーのみ" - }, - "error": { - "label": "Error", - "description": "エラーメッセージ" - }, - "warn": { - "label": "Warn", - "description": "警告 + エラー" - }, - "info": { - "label": "Info", - "description": "主要なビジネスイベント + 警告 + エラー(本番環境推奨)" - }, - "debug": { - "label": "Debug", - "description": "デバッグ情報 + 全レベル(開発環境推奨)" - }, - "trace": { - "label": "Trace", - "description": "非常に詳細なトレース + 全レベル" - } - }, - "form": { - "currentLevel": "現在のログレベル", - "selectLevel": "ログレベルを選択", - "save": "設定を保存", - "saving": "保存中...", - "success": "ログレベルを設定しました: {level}", - "failed": "設定に失敗しました", - "failedError": "ログレベルの設定に失敗しました", - "fetchFailed": "ログレベルの取得に失敗しました", - "effectiveImmediately": "ログレベルの変更はすぐに有効になります、サービス再起動不要。", - "levelGuideTitle": "ログレベルガイド", - "levelGuideFatal": "Fatal/Error: エラーのみ表示、最小限のログ、高負荷本番環境に適しています", - "levelGuideWarn": "Warn: 警告(レート制限、サーキットブレーカー開放など)+ エラーを含む", - "levelGuideInfo": "Info(本番環境推奨): 主要なビジネスイベント(プロバイダー選択、セッション再利用、価格同期)+ 警告 + エラーを表示", - "levelGuideDebug": "Debug(開発環境推奨): 詳細なデバッグ情報を含む、トラブルシューティングに適しています", - "levelGuideTrace": "Trace: 非常に詳細なトレース情報、すべての詳細を含む", - "changeNotice": "現在のレベルは {current} です、保存後 {selected} に切り替わります" - }, - "title": "ログ管理" - }, - "nav": { - "apiDocs": "API文書", - "clientVersions": "更新通知", - "config": "設定", - "data": "データ", - "errorRules": "エラー", - "feedback": "報告", - "docs": "ドキュメント", - "logs": "ログ", - "notifications": "通知", - "prices": "価格表", - "providers": "供給元", - "sensitiveWords": "フィルター", - "requestFilters": "リクエスト" - }, - "notifications": { - "title": "プッシュ通知", - "description": "Webhook プッシュ通知を設定", - "global": { - "title": "通知マスタースイッチ", - "description": "すべてのプッシュ通知機能を有効または無効にする", - "enable": "プッシュ通知を有効にする", - "legacyModeTitle": "互換モード", - "legacyModeDescription": "現在は旧来の単一URL通知設定を使用しています。プッシュ先を作成するとマルチターゲットモードに切り替わります。" - }, - "targets": { - "title": "プッシュ先", - "description": "プッシュ先を管理します。WeCom、Feishu、DingTalk、Telegram、カスタムWebhookに対応。", - "add": "追加", - "update": "保存", - "edit": "編集", - "delete": "削除", - "deleteConfirmTitle": "プッシュ先を削除", - "deleteConfirm": "このプッシュ先を削除しますか?関連するバインドも削除されます。", - "enable": "有効化", - "statusEnabled": "有効", - "statusDisabled": "無効", - "lastTestAt": "最終テスト", - "lastTestNever": "未テスト", - "lastTestSuccess": "テスト成功", - "lastTestFailed": "テスト失敗", - "test": "テスト", - "testSelectType": "テスト種類を選択", - "emptyHint": "プッシュ先がありません。「追加」で作成してください。", - "created": "プッシュ先を作成しました", - "updated": "プッシュ先を更新しました", - "deleted": "プッシュ先を削除しました", - "bindingsSaved": "バインドを保存しました" - }, - "targetDialog": { - "createTitle": "プッシュ先を追加", - "editTitle": "プッシュ先を編集", - "name": "名前", - "namePlaceholder": "例: Ops グループ", - "type": "プラットフォーム", - "selectType": "プラットフォームを選択", - "enable": "有効化", - "webhookUrl": "Webhook URL", - "webhookUrlPlaceholder": "https://example.com/webhook", - "telegramBotToken": "Telegram Bot Token", - "telegramBotTokenPlaceholder": "例: 123456:ABCDEF...", - "telegramChatId": "Telegram Chat ID", - "telegramChatIdPlaceholder": "例: -1001234567890", - "dingtalkSecret": "DingTalk シークレット", - "dingtalkSecretPlaceholder": "任意(署名用)", - "customHeaders": "カスタムヘッダー(JSON)", - "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", - "errors": { - "headersInvalidJson": "Headers は有効な JSON である必要があります", - "headersMustBeObject": "Headers は JSON オブジェクトである必要があります", - "headersValueMustBeString": "Headers の値は文字列である必要があります" - }, - "types": { - "wechat": "WeCom", - "feishu": "Feishu", - "dingtalk": "DingTalk", - "telegram": "Telegram", - "custom": "カスタムWebhook" - }, - "proxy": { - "title": "プロキシ", - "toggle": "プロキシ設定を切り替え", - "url": "プロキシURL", - "urlPlaceholder": "http://127.0.0.1:7890", - "fallbackToDirect": "プロキシ失敗時に直結へフォールバック" - } - }, - "bindings": { - "title": "バインド", - "noTargets": "プッシュ先がありません", - "bindTarget": "プッシュ先をバインド", - "enable": "有効", - "enableType": "この通知を有効化", - "advanced": "詳細", - "scheduleCron": "Cron", - "scheduleCronPlaceholder": "例: 0 9 * * *", - "scheduleTimezone": "タイムゾーン", - "templateOverride": "テンプレート上書き", - "editTemplateOverride": "上書きを編集", - "templateOverrideTitle": "テンプレート上書きを編集", - "boundCount": "バインド: {count}", - "enabledCount": "有効: {count}" - }, - "templateEditor": { - "title": "テンプレート(JSON)", - "placeholder": "JSON テンプレートを入力...", - "jsonInvalid": "JSON が不正です", - "placeholders": "プレースホルダー", - "insert": "挿入" - }, - "circuitBreaker": { - "title": "サーキットブレーカーアラート", - "description": "プロバイダーが完全に遮断された時に即座にアラートを送信", - "enable": "サーキットブレーカーアラートを有効にする", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "test": "接続テスト" - }, - "dailyLeaderboard": { - "title": "日次ユーザー消費ランキング", - "description": "毎日定時でユーザー消費トップNランキングを送信", - "enable": "日次ランキングを有効にする", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "time": "送信時刻", - "timePlaceholder": "09:00", - "timeError": "時刻形式エラー、HH:mm形式である必要があります", - "topN": "トップN件表示", - "test": "接続テスト" - }, - "costAlert": { - "title": "コストアラート", - "description": "ユーザー/プロバイダーの消費がクォータしきい値を超えた時にアラートをトリガー", - "enable": "コストアラートを有効にする", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "webhookTypeWeCom": "WeCom", - "webhookTypeFeishu": "Feishu", - "webhookTypeUnknown": "不明なプラットフォームです。WeComまたはFeishuのWebhook URLを使用してください", - "threshold": "アラートしきい値", - "thresholdLabel": "アラートしきい値: {percent}%", - "thresholdHelp": "消費がクォータの{percent}%に達した時にアラート", - "interval": "チェック間隔(分)", - "test": "接続テスト" - }, - "form": { - "save": "設定を保存", - "saving": "保存中...", - "loading": "読み込み中...", - "success": "通知設定を保存し、タスクを再スケジュールしました", - "saveFailed": "保存に失敗しました", - "saveError": "設定の保存に失敗しました", - "loadError": "通知設定の読み込みに失敗しました", - "webhookRequired": "まずWebhook URLを入力してください", - "testSuccess": "テストメッセージを送信しました", - "testFailed": "テストに失敗しました", - "testFailedRetry": "テストに失敗しました。再試行してください", - "testError": "接続テストに失敗しました", - "testNoResult": "テストは成功しましたが、結果が返されませんでした" - } - }, - "prices": { - "title": "価格表", - "description": "プラットフォーム基本設定とモデル価格を管理します", - "section": { - "title": "モデル価格", - "description": "AIモデルの価格設定を管理します" - }, - "searchPlaceholder": "モデル名を検索...", - "filters": { - "all": "すべて", - "local": "ローカル", - "anthropic": "Anthropic", - "openai": "OpenAI", - "vertex": "Vertex" - }, - "badges": { - "local": "ローカル" - }, - "capabilities": { - "assistantPrefill": "アシスタント事前入力", - "computerUse": "コンピューター利用", - "functionCalling": "関数呼び出し", - "pdfInput": "PDF入力", - "promptCaching": "プロンプトキャッシュ", - "reasoning": "推論", - "responseSchema": "レスポンススキーマ", - "toolChoice": "ツール選択", - "vision": "ビジョン", - "statusSupported": "対応", - "statusUnsupported": "未対応", - "tooltip": "{label}: {status}" - }, - "sync": { - "button": "クラウド価格表を同期", - "syncing": "同期中...", - "checking": "競合を確認中...", - "successWithChanges": "価格表を更新: {added}件追加、{updated}件更新、{unchanged}件変化なし", - "successNoChanges": "価格表は最新です。更新の必要はありません", - "failed": "同期に失敗しました", - "failedError": "同期に失敗しました: {error}", - "failedNoResult": "価格表は更新されましたが結果が返されていません", - "noModels": "モデル価格が見つかりません", - "partialFailure": "一部更新が成功しましたが、{failed}件のモデルが失敗しました", - "failedModels": "失敗モデル: {models}", - "skippedConflicts": "{count}件の手動モデルをスキップしました" - }, - "conflict": { - "title": "上書きする項目を選択", - "description": "以下のモデルには手動で設定された価格があります。チェックした項目はLiteLLM価格で上書きされ、チェックしない項目は現在のままです", - "searchPlaceholder": "モデルを検索...", - "table": { - "modelName": "モデル", - "manualPrice": "手動価格", - "litellmPrice": "LiteLLM価格", - "action": "操作" - }, - "viewDiff": "差異を表示", - "diffTitle": "価格差異", - "diff": { - "field": "フィールド", - "manual": "手動", - "litellm": "LiteLLM", - "inputPrice": "入力価格", - "outputPrice": "出力価格", - "imagePrice": "画像価格", - "provider": "プロバイダー", - "mode": "タイプ" - }, - "pagination": { - "showing": "{from}〜{to}件を表示(全{total}件)" - }, - "selectedCount": "{count}/{total}件のモデルを選択", - "noMatch": "一致するモデルが見つかりません", - "noConflicts": "競合なし", - "applyOverwrite": "上書きを適用", - "applying": "適用中..." - }, - "table": { - "modelName": "モデル名", - "provider": "プロバイダー", - "capabilities": "機能", - "price": "価格", - "inputPrice": "入力価格 ($/M)", - "outputPrice": "出力価格 ($/M)", - "priceInput": "入力", - "priceOutput": "出力", - "pricePerRequest": "回", - "cacheReadPrice": "キャッシュ読み取り ($/M)", - "cacheCreationPrice": "キャッシュ作成 ($/M)", - "cache5m": "5m", - "cache1h": "1h+", - "copyModelId": "モデルIDをコピー", - "updatedAt": "更新日時", - "actions": "操作", - "typeChat": "チャット", - "typeImage": "画像生成", - "typeCompletion": "補完", - "typeUnknown": "不明", - "loading": "読み込み中...", - "noMatch": "一致するモデルが見つかりません", - "noDataTitle": "価格データがありません", - "noDataHint": "システムは組み込み価格表を持っています。上のボタンを使用して同期または更新してください。" - }, - "pagination": { - "showing": "{from}〜{to}件を表示(全{total}件)", - "previous": "前へ", - "next": "次へ", - "perPageLabel": "1ページあたり", - "perPage": "1ページあたり{size}件" - }, - "stats": { - "totalModels": "合計{count}個のモデル", - "searchResults": "{count}件の検索結果", - "lastUpdated": "最終更新: {time}" - }, - "dialog": { - "title": "モデル価格表を更新", - "description": "モデル価格データを含むJSONまたはTOMLファイルを選択してアップロード", - "selectFile": "JSON/TOMLファイルをクリックして選択、またはドラッグしてください", - "fileSizeLimit": "ファイルサイズは10MBを超えることはできません", - "fileSizeLimitSmall": "ファイルサイズは10MB以下です", - "invalidFileType": "JSONまたはTOML形式のファイルを選択してください", - "fileTooLarge": "ファイルサイズが10MBを超えています", - "upload": "アップロードして更新", - "uploading": "アップロード中...", - "updatePriceTable": "価格表を更新", - "updating": "更新中...", - "selectJson": "ファイルを選択", - "updateSuccess": "価格表が正常に更新されました。{count}個のモデルを更新しました", - "updateFailed": "更新に失敗しました", - "systemHasBuiltIn": "システムは組み込み価格表を持っています", - "manualDownload": "手動でダウンロードすることもできます", - "latestPriceTable": "クラウド価格表", - "andUploadViaButton": "、上のボタンでアップロードしてください", - "cloudModelCountLoading": "クラウドモデル数を読み込み中...", - "cloudModelCountFailed": "クラウドモデル数の読み込みに失敗しました", - "supportedModels": "現在{count}個のモデルをサポート", - "results": { - "title": "更新結果", - "total": "合計: {total}個のモデル", - "success": "成功: {success}", - "failed": "失敗: {failed}", - "skipped": "スキップ: {skipped}", - "more": " (+{count})", - "details": "詳細", - "viewDetails": "詳細ログを表示" - } - }, - "addModel": "モデルを追加", - "editModel": "モデルを編集", - "deleteModel": "モデルを削除", - "addModelDescription": "新しいモデル価格設定を手動で追加します", - "editModelDescription": "モデルの価格設定を編集します", - "deleteConfirm": "モデル {name} を削除してもよろしいですか?この操作は元に戻せません。", - "form": { - "modelName": "モデルID", - "modelNamePlaceholder": "例: gpt-5.2-codex", - "modelNameRequired": "モデルIDは必須です", - "displayName": "表示名(任意)", - "displayNamePlaceholder": "例: GPT-5.2 Codex", - "type": "タイプ", - "provider": "プロバイダー", - "providerPlaceholder": "例: openai", - "requestPrice": "呼び出し単価 ($/request)", - "inputPrice": "入力価格 ($/M tokens)", - "outputPrice": "出力価格 ($/M tokens)", - "outputPriceImage": "出力価格 ($/image)", - "cacheReadPrice": "キャッシュ読み取り価格 ($/M tokens)", - "cacheCreationPrice5m": "キャッシュ作成価格(5m,$/M tokens)", - "cacheCreationPrice1h": "キャッシュ作成価格(1h+,$/M tokens)" - }, - "drawer": { - "prefillLabel": "既存モデルを検索してプリフィル", - "prefillEmpty": "一致するモデルが見つかりません", - "prefillFailed": "検索に失敗しました", - "promptCachingHint": "モデルがキャッシュに対応している場合のみ有効化し、下のキャッシュ価格を設定してください", - "cachePricingTitle": "キャッシュ価格" - }, - "actions": { - "edit": "編集", - "more": "その他の操作", - "delete": "削除" - }, - "toast": { - "createSuccess": "モデルを追加しました", - "updateSuccess": "モデルを更新しました", - "deleteSuccess": "モデルを削除しました", - "saveFailed": "保存に失敗しました", - "deleteFailed": "削除に失敗しました" - } - }, - "providers": { - "add": "プロバイダーを追加", - "addFailed": "プロバイダーの追加に失敗しました", - "addProvider": "プロバイダーを追加", - "addSuccess": "プロバイダーが正常に追加されました", - "autoSort": { - "button": "優先度を自動ソート", - "dialogTitle": "プロバイダー優先度の自動ソート", - "dialogDescription": "コスト倍率に基づいて優先度を自動割り当て(低コスト = 高優先度)", - "changeCount": "{count} 件のプロバイダーが更新されます", - "noChanges": "変更不要(ソート済み)", - "costMultiplierHeader": "コスト倍率", - "priorityHeader": "優先度", - "providersHeader": "プロバイダー", - "changesTitle": "変更詳細", - "providerHeader": "プロバイダー", - "priorityChangeHeader": "優先度変更", - "confirm": "変更を適用", - "success": "{count} 件のプロバイダーの優先度を更新しました", - "error": "優先度の更新に失敗しました" - }, - "circuitBroken": "サーキットブレーカー作動中", - "clone": "プロバイダーを複製", - "cloneFailed": "コピーに失敗しました", - "confirmDelete": "このプロバイダーを削除してもよろしいですか?", - "confirmDeleteDesc": "プロバイダー「{name}」を削除してもよろしいですか?この操作は元に戻せません。", - "confirmDeleteProvider": "プロバイダーの削除を確認しますか?", - "confirmDeleteProviderDesc": "サービスプロバイダー「{name}」を削除してもよろしいですか?この操作は復元できません。", - "createProvider": "サービスプロバイダーを追加", - "delete": "プロバイダーを削除", - "deleteFailed": "プロバイダーの削除に失敗しました", - "deleteSuccess": "削除成功", - "description": "APIサービスプロバイダーを設定し、可用性ステータスを維持します。", - "disabledStatus": "無効", - "displayCount": "{filtered} / {total}個のプロバイダーを表示", - "edit": "プロバイダーを編集", - "editFailed": "プロバイダーの更新に失敗しました", - "editProvider": "サービスプロバイダーを編集", - "enabledStatus": "有効", - "form": { - "proxyTest": { - "fillUrlFirst": "まずプロバイダーURLを入力してください", - "testFailed": "テスト失敗", - "testFailedRetry": "テストに失敗しました。再試行してください", - "noResult": "テスト成功ですが結果が返されませんでした", - "connectionSuccess": "接続成功", - "connectionFailed": "接続失敗", - "viaProxy": "(プロキシ経由)", - "viaDirect": "(直接接続)", - "responseTime": "応答時間:", - "statusCode": "ステータスコード:", - "connectionMethod": "接続方式:", - "proxy": "プロキシ", - "direct": "直接接続", - "errorType": "エラータイプ:", - "testing": "テスト中...", - "testConnection": "接続テスト", - "timeoutError": "接続タイムアウト(5秒)。以下を確認してください:\n1. プロキシサーバーにアクセスできるか\n2. プロキシアドレスとポートが正しいか\n3. プロキシ認証情報が正しいか", - "proxyError": "プロキシエラー:", - "networkError": "ネットワークエラー:" - }, - "apiTest": { - "fillUrlFirst": "まずプロバイダーURLを入力してください", - "invalidUrl": "プロバイダーURLが無効です(http/httpsのみ対応)", - "fillKeyFirst": "まずAPIキーを入力してください", - "testFailed": "テスト失敗", - "testFailedRetry": "テスト失敗、再試行してください", - "noResult": "テスト成功ですが結果が返されませんでした", - "testSuccess": "モデルテスト成功", - "testApi": "プロバイダーモデルテスト", - "testing": "テスト中...", - "apiFormat": "プロバイダータイプ", - "selectApiFormat": "テストするプロバイダータイプを選択", - "apiFormatDesc": "手動で変更しない限り、ルーティング設定のプロバイダータイプと同期", - "formatAnthropicMessages": "Claude (Anthropic Messages API)", - "formatOpenAIChat": "OpenAI Compatible", - "formatOpenAIResponses": "Codex (Response API)", - "testModel": "テストモデル", - "testModelDesc": "空欄の場合はデフォルトモデルを使用、手動入力も可能", - "requestConfig": "リクエスト設定", - "presetConfig": "プリセット", - "customConfig": "カスタム", - "selectPreset": "プリセットテンプレートを選択", - "presetDesc": "プリセットテンプレートには、リレーサービス検証用の本物のCLIリクエストパターンが含まれています", - "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", - "customPayloadDesc": "カスタムJSONペイロードを入力してデフォルトのリクエストボディを上書き", - "successContains": "成功検出キーワード", - "successContainsPlaceholder": "pong", - "successContainsDesc": "成功と見なすには、レスポンスにこのキーワードが含まれている必要があります", - "model": "モデル", - "responseModel": "応答モデル", - "responseTime": "応答時間", - "usage": "トークン使用量", - "response": "応答内容", - "error": "エラーメッセージ", - "unknown": "不明", - "viewDetails": "詳細を見る", - "copySuccess": "クリップボードにコピーしました", - "copyFailed": "コピー失敗", - "copyResult": "結果をコピー", - "close": "閉じる", - "success": "成功", - "failed": "失敗", - "streamInfo": "ストリーム応答情報", - "chunksReceived": "受信したチャンク", - "streamFormat": "ストリーム形式", - "streamResponse": "ストリーム応答", - "chunksCount": "{count} チャンク受信 ({format})", - "truncatedPreview": "先頭 {length} 文字を表示、全文はコピーして確認", - "truncatedBrief": "先頭 {length} 文字を表示、全文は「詳細を見る」をクリック", - "timeout": { - "label": "タイムアウト(秒)", - "desc": "テストリクエストの最大待機時間(5〜120秒)", - "geminiHint": "、Gemini Thinkingモデルは60秒以上を推奨" - }, - "geminiAuthFallback": { - "warning": "ヘッダー認証に失敗し、URLパラメータ認証を使用しました", - "desc": "実際のプロキシ転送はヘッダー認証のみを使用するため、リクエストが失敗する可能性があります" - }, - "copyFormat": { - "testResult": "テスト結果", - "message": "メッセージ", - "errorDetails": "エラー詳細" - }, - "disclaimer": { - "title": "注意", - "realRequest": "テストはプロバイダーに実際のリクエストを送信し、少量のクォータを消費する可能性があります", - "resultReference": "プロバイダーによって結果が異なる場合があり、参考用です", - "confirmConfig": "プロバイダーURL、APIキー、モデル設定を確認してください" - }, - "resultCard": { - "status": { - "green": "利用可能", - "yellow": "不安定", - "red": "利用不可" - }, - "dialogTitle": "プロバイダーテスト詳細", - "validation": { - "title": "三層検証詳細", - "http": { - "title": "Tier 1: HTTPステータス", - "statusCode": "ステータスコード", - "passed": "2xx/3xx 成功", - "failed": "4xx/5xx 失敗" - }, - "latency": { - "title": "Tier 2: レイテンシ閾値", - "actual": "実際のレイテンシ", - "passed": "閾値内", - "failed": "閾値超過" - }, - "content": { - "title": "Tier 3: コンテンツ検証", - "target": "ターゲット", - "passed": "ターゲット文字列を含む", - "failed": "ターゲットが見つかりません" - }, - "passed": "合格", - "failed": "失敗", - "timeout": "タイムアウト" - }, - "labels": { - "http": "HTTP", - "latency": "レイテンシ", - "content": "コンテンツ", - "model": "モデル", - "firstByte": "最初のバイト", - "totalLatency": "合計レイテンシ", - "error": "エラー", - "responsePreview": "応答プレビュー" - }, - "timing": { - "title": "タイミング情報", - "totalLatency": "合計レイテンシ", - "firstByte": "最初のバイト", - "testedAt": "テスト日時" - }, - "tokenUsage": { - "title": "トークン使用量", - "input": "入力", - "output": "出力", - "cacheCreation": "キャッシュ作成", - "cacheRead": "キャッシュ読取" - }, - "streamInfo": { - "title": "ストリーム応答情報", - "isStreaming": "ストリーミング", - "chunksCount": "チャンク数", - "yes": "はい", - "no": "いいえ" - }, - "rawResponse": { - "title": "生のレスポンスボディ", - "hint": "ここに生のレスポンス内容が表示されます。キーワードがレスポンスに含まれているか確認できます。" - }, - "errorDetails": { - "title": "エラー詳細", - "type": "エラータイプ" - }, - "copyText": { - "status": "ステータス", - "message": "メッセージ", - "latency": "レイテンシ", - "httpStatus": "HTTPステータス", - "model": "モデル", - "usage": "使用量", - "inputOutput": "入力 {input} / 出力 {output} トークン", - "response": "応答", - "error": "エラー", - "testedAt": "テスト日時", - "validationDetails": "検証詳細", - "httpCheck": "HTTPチェック", - "latencyCheck": "レイテンシチェック", - "contentCheck": "コンテンツ検証" - }, - "judgment": "判定" - } - }, - "urlPreview": { - "title": "URL結合プレビュー", - "invalidUrl": "無効なURL形式", - "invalidUrlDesc": "有効なHTTP/HTTPSアドレスを入力してください", - "duplicatePath": "重複パス検出", - "copy": "コピー", - "copySuccess": "{name} をクリップボードにコピーしました", - "copyFailed": "コピーに失敗しました" - }, - "modelSelect": { - "allowAllModels": "すべての {type} モデルを許可", - "selectedCount": "{count} 個のモデルを選択済み", - "searchPlaceholder": "モデル名を検索...", - "loading": "読み込み中...", - "notFound": "モデルが見つかりません", - "selectAll": "すべて選択 ({count})", - "clear": "クリア", - "manualAdd": "手動でモデルを追加", - "manualPlaceholder": "モデル名を入力(例:gpt-5-turbo)", - "manualDesc": "任意のモデル名を追加できます(価格表のモデルに限定されません)", - "claude": "Claude", - "openai": "OpenAI", - "gemini": "Gemini", - "sourceUpstream": "上流", - "sourceUpstreamDesc": "モデルリストは上流プロバイダーAPIから取得", - "sourceFallback": "ローカル", - "sourceFallbackDesc": "ローカル価格表のモデルリストを使用(上流が利用不可または未対応)", - "refresh": "モデルリストを更新" - }, - "modelRedirect": { - "currentRules": "現在のルール ({count})", - "addNewRule": "新規ルールを追加", - "sourceModel": "ユーザーがリクエストするモデル", - "targetModel": "実際に転送されるモデル", - "sourcePlaceholder": "例:claude-sonnet-4-5-20250929", - "targetPlaceholder": "例:glm-4.6", - "add": "追加", - "sourceEmpty": "ソースモデル名を入力してください", - "targetEmpty": "ターゲットモデル名を入力してください", - "alreadyExists": "モデル \"{model}\" のリダイレクトルールは既に存在します", - "description": "Claude Code クライアントがリクエストするモデル(例:claude-sonnet-4.5)を、上流プロバイダーが実際にサポートするモデル(例:glm-4.6、gemini-pro)にリダイレクトします。コスト最適化やサードパーティAIサービスへの接続に使用します。", - "emptyState": "リダイレクトルールがありません。ルールを追加すると、システムは自動的にリクエスト内のモデル名を書き換えます。" - }, - "addRedirect": "リダイレクトを追加", - "allowAllModels": "✓ すべてのモデルを許可(推奨)", - "apiAddress": "API 地址", - "apiAddressPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic", - "apiAddressRequired": "API 地址 *", - "apiKey": "APIキー", - "apiKeyCurrent": "当前密钥:", - "apiKeyLeaveEmpty": "(留空不更改)", - "apiKeyLeaveEmptyDesc": "留空则不更改密钥", - "apiKeyOptional": "現在のキーを保持する場合は空のままにしてください", - "apiKeyPlaceholder": "APIキーを入力", - "apiKeyRequired": "API 密钥 *", - "baseUrl": "ベースURL", - "baseUrlPlaceholder": "例:https://open.bigmodel.cn/api/anthropic", - "baseUrlRequired": "プロバイダーURLを入力してください", - "circuitBreakerConfig": "サーキットブレーカー設定", - "circuitBreakerConfigSummary": "{failureThreshold} 回失敗 / {openDuration} 分間ブレーク / {successThreshold} 回成功で回復 / 各プロバイダー最大 {maxRetryAttempts} 回試行", - "circuitBreakerDesc": "連続失敗時に自動的にブレークして全体の品質を保護します", - "clearSearch": "検索をクリア", - "codexInstructions": "Codexインストラクション方針", - "codexInstructionsAuto": "自動(推奨)", - "codexInstructionsDesc": "(スケジューリング方針を決定)", - "codexInstructionsForce": "公式を強制", - "codexInstructionsKeep": "元の値を保持", - "codexStrategyAutoDesc": "透传客户端 instructions,400 错误时自动重试官方 prompt", - "codexStrategyAutoLabel": "自动 (推荐)", - "codexStrategyConfig": "Codex Instructions 策略", - "codexStrategyConfigAuto": "自动 (推荐)", - "codexStrategyConfigForce": "强制官方", - "codexStrategyConfigKeep": "透传原样", - "codexStrategyDesc": "控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性", - "codexStrategyForceDesc": "始终使用官方 Codex CLI instructions(约 4000+ 字)", - "codexStrategyForceLabel": "强制官方", - "codexStrategyHint": "提示: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方 instructions,选择\"自动\"或\"强制官方\"策略", - "codexStrategyKeepDesc": "始终透传客户端 instructions,不自动重试(适用于宽松中转站)", - "codexStrategyKeepLabel": "透传原样", - "codexStrategySelect": "策略选择", - "collapseAll": "折叠全部高级配置", - "confirmAdd": "确认添加", - "confirmAddPending": "添加中...", - "confirmUpdate": "确认更新", - "confirmUpdatePending": "更新中...", - "costMultiplier": "コスト乗数", - "costMultiplierDesc": "例:A(コスト1.0x)、C(コスト0.8x)", - "costMultiplierLabel": "成本倍率", - "costMultiplierPlaceholder": "1.0", - "deleteButton": "删除", - "enabled": "有効", - "expandAll": "展开全部高级配置", - "failureThreshold": "失败阈值(次)", - "failureThresholdDesc": "连续失败多少次后触发熔断", - "failureThresholdPlaceholder": "5", - "filterAllProviders": "全部供应商", - "filterByType": "タイプでフィルタ", - "filterProvider": "筛选供应商类型", - "group": "グループ", - "groupPlaceholder": "例:premium, economy", - "joinClaudePool": "加入 Claude 调度池", - "joinClaudePoolDesc": "启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度", - "joinClaudePoolHelp": "仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用户请求 claude-* 模型时,此供应商也会参与调度选择。", - "leaveEmpty": "無制限の場合は空のままにしてください", - "limit0Means": "0は無制限を意味します", - "limit5hLabel": "5小时消费上限 (USD)", - "limitAmount5h": "5時間支出上限(USD)", - "limitAmount5hDesc": "例:プロバイダーBが$10制限、$9.8消費済み", - "limitAmountMonthly": "月間支出上限(USD)", - "limitAmountWeekly": "週間支出上限(USD)", - "limitConcurrent": "同時セッション数制限", - "limitConcurrentDesc": "例:プロバイダーC制限2、現在アクティブセッション数:2", - "limitConcurrentLabel": "并发 Session 上限", - "limitMonthlyLabel": "月消费上限 (USD)", - "limitPlaceholder0": "0 表示无限制", - "limitPlaceholderUnlimited": "留空表示无限制", - "limitWeeklyLabel": "周消费上限 (USD)", - "modelRedirects": "モデルリダイレクト", - "modelRedirectsAddNew": "添加新规则", - "modelRedirectsCurrentRules": "当前规则 ({count})", - "modelRedirectsDesc": "Claudeモデルリクエストを他のサポートされるモデルにリダイレクトします", - "modelRedirectsEmpty": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。", - "modelRedirectsExists": "模型 \"{model}\" 已存在重定向规则", - "modelRedirectsLabel": "模型重定向配置", - "modelRedirectsOptional": "(可选)", - "modelRedirectsSourceModel": "用户请求的模型", - "modelRedirectsSourcePlaceholder": "例如: claude-sonnet-4-5-20250929", - "modelRedirectsSourceRequired": "源模型名称不能为空", - "modelRedirectsTargetModel": "实际转发的模型", - "modelRedirectsTargetPlaceholder": "例如: glm-4.6", - "modelRedirectsTargetRequired": "目标模型名称不能为空", - "modelWhitelist": "模型白名单", - "modelWhitelistAllowAll": "允许所有 {type} 模型", - "modelWhitelistAllowAllClause": "允许所有 Claude 模型", - "modelWhitelistAllowAllOpenAI": "允许所有 OpenAI 模型", - "modelWhitelistClear": "清空", - "modelWhitelistDesc": "限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。", - "modelWhitelistLabel": "允许的模型", - "modelWhitelistLoading": "加载中...", - "modelWhitelistManualAdd": "手动添加模型", - "modelWhitelistManualDesc": "支持添加任意模型名称(不限于价格表中的模型)", - "modelWhitelistManualPlaceholder": "输入模型名称(如 gpt-5-turbo)", - "modelWhitelistNotFound": "未找到模型", - "modelWhitelistSearchPlaceholder": "搜索模型名称...", - "modelWhitelistSelectAll": "全选 ({count})", - "modelWhitelistSelected": "已选择 {count} 个模型", - "modelWhitelistSelectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。", - "name": { - "label": "プロバイダー名 *", - "placeholder": "例: Zhipu" - }, - "namePlaceholder": "プロバイダー名を入力", - "openDuration": "熔断时长(分钟)", - "openDurationDesc": "熔断后多久自动进入半开状态", - "openDurationPlaceholder": "30", - "priority": "優先度", - "priorityDesc": "同じ優先度内では、コスト乗数の低い順でソートされます", - "priorityLabel": "优先级", - "priorityPlaceholder": "0", - "providerGroupDesc": "供应商分组标签。只有用户的 providerGroup 与此值匹配时,该用户才能使用此供应商。示例:设置为 \"premium\" 表示只供 providerGroup=\"premium\" 的用户使用", - "providerGroupLabel": "供应商分组", - "providerGroupPlaceholder": "例如: premium, economy", - "providerName": "服务商名称", - "providerNamePlaceholder": "例如: 智谱", - "providerNameRequired": "服务商名称 *", - "providerType": "プロバイダータイプ", - "providerTypeDesc": "选择供应商的 API 格式类型。", - "providerTypeDisabledNote": "注:Gemini CLI 和 OpenAI Compatible 类型功能正在开发中,暂不可用", - "proxy": "プロキシ", - "proxyAddressFormats": "支持格式:", - "proxyAddressLabel": "代理地址", - "proxyAddressOptional": "(可选)", - "proxyAddressPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", - "proxyConfig": "代理配置", - "proxyConfigDesc": "配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)", - "proxyConfigNone": "未配置", - "proxyConfigSummary": "已配置代理", - "proxyConfigSummaryFallback": " (启用降级)", - "proxyConfigured": "プロキシが設定されています", - "proxyFallback": "プロキシ失敗時のフォールバック", - "proxyFallbackDesc": "プロキシ失敗時に直接接続にフォールバックします", - "proxyFallbackLabel": "代理失败时降级到直连", - "proxyNotConfigured": "未設定", - "proxyTestButton": "测试连接", - "proxyTestDesc": "测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)", - "proxyTestFailed": "连接失败", - "proxyTestFillUrl": "请先填写供应商 URL", - "proxyTestLabel": "连接测试", - "proxyTestNetworkError": "网络错误: {error}", - "proxyTestProxyError": "代理错误: {error}", - "proxyTestResponseTime": "响应时间: {time}", - "proxyTestResultConnectionMethod": "连接方式: {via}", - "proxyTestResultConnectionMethodDirect": "直连", - "proxyTestResultConnectionMethodProxy": "代理", - "proxyTestResultErrorType": "错误类型: {type}", - "proxyTestResultFailed": "连接失败", - "proxyTestResultMessage": "{message}", - "proxyTestResultResponseTime": "响应时间: {time}ms", - "proxyTestResultStatusCode": "状态码: {code}", - "proxyTestResultSuccess": "连接成功 {via}", - "proxyTestStatusCode": "| 状态码: {code}", - "proxyTestSuccess": "连接成功", - "proxyTestTesting": "测试中...", - "proxyTestTimeout": "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确", - "proxyTestViaDirect": "(直连)", - "proxyTestViaProxy": "(通过代理)", - "proxyUrl": "プロキシアドレス", - "proxyUrlPlaceholder": "例:http://proxy.example.com:8080 または socks5://127.0.0.1:1080", - "rateLimitConfig": "限流配置", - "rateLimitConfigNone": "无限制", - "rateLimitConfigSummary": "5h: ${fiveHour}, 周: ${weekly}, 月: ${monthly}, 并发: {concurrent}", - "remark": "備考", - "remarkPlaceholder": "オプション:説明を追加...", - "removeRedirect": "リダイレクトを削除", - "routingConfig": "路由配置", - "routingConfigNone": "未配置", - "routingConfigSummary": "{models} 个模型白名单, {redirects} 个重定向", - "scheduleParams": "调度参数", - "searchClear": "清除搜索", - "searchPlaceholder": "プロバイダー名、URL、備考で検索...", - "selectProviderType": "プロバイダータイプを選択", - "sort": "プロバイダーをソート", - "sortByCost": "コスト順", - "sortByCreated": "作成日順(新-旧)", - "sortByName": "名前順(A-Z)", - "sortByPriority": "優先度順(高-低)", - "sortByWeight": "重み順(高-低)", - "sourceModel": "ソースモデル名", - "sourceModelPlaceholder": "例:claude-sonnet-4-5-20250929", - "sourceModelRequired": "ソースモデル名は空にできません", - "successThreshold": "恢复阈值(次)", - "successThresholdDesc": "半开状态下成功多少次后完全恢复", - "successThresholdPlaceholder": "2", - "targetModel": "ターゲットモデル名", - "targetModelPlaceholder": "例:glm-4.6", - "targetModelRequired": "ターゲットモデル名は空にできません", - "testProxy": "接続をテスト", - "testProxyFailed": "プロキシテストに失敗しました", - "testProxyFailedError": "接続テスト失敗:", - "testProxySuccess": "プロキシ接続成功", - "validUrlRequired": "请输入有效的 API 地址", - "websiteUrl": { - "label": "プロバイダー公式サイト", - "placeholder": "https://example.com", - "desc": "管理ページへのクイックアクセス用" - }, - "websiteUrlDesc": "供应商官网地址,用于快速跳转管理", - "websiteUrlInvalid": "请输入有效的供应商官网地址", - "websiteUrlPlaceholder": "https://example.com", - "weight": "重み", - "weightDesc": "加重ランダム確率。同じ優先度内では、重みが高いほど選択される確率が高くなります。", - "weightLabel": "权重", - "weightPlaceholder": "1", - "title": { - "create": "プロバイダーを追加", - "edit": "プロバイダーを編集" - }, - "dialogDescription": "プロバイダーの詳細と高度な設定を構成します。", - "url": { - "label": "API アドレス *", - "placeholder": "例: https://open.bigmodel.cn/api/anthropic" - }, - "key": { - "label": "API キー", - "leaveEmpty": "(空欄のままにすると変更しません)", - "placeholder": "API キーを入力", - "leaveEmptyDesc": "空欄のままにすると既存のキーを保持します", - "currentKey": "現在のキー: {key}" - }, - "buttons": { - "expandAll": "高度な設定をすべて展開", - "collapseAll": "高度な設定をすべて折りたたむ", - "submit": "追加を確定", - "submitting": "追加中...", - "update": "更新を確定", - "updating": "更新中...", - "delete": "削除" - }, - "common": { - "core": "コア" - }, - "sections": { - "routing": { - "title": "ルーティング", - "summary": { - "models": "許可モデル {count} 件", - "redirects": "リダイレクト {count} 件", - "none": "未設定" - }, - "providerType": { - "label": "プロバイダー種別", - "desc": "(スケジューリングに影響)", - "placeholder": "プロバイダー種別を選択" - }, - "providerTypeDesc": "プロバイダーの API 形式を選択します。", - "providerTypeDisabledNote": "注: OpenAI Compatible は開発中のため、現在は使用できません", - "modelRedirects": { - "label": "モデルリダイレクト設定", - "optional": "(任意)" - }, - "joinClaudePool": { - "label": "Claude ルーティングプールに参加", - "desc": "有効にすると、Claude 系のプロバイダーと共に負荷分散に参加します", - "help": "claude-* へのリダイレクトがある場合のみ利用できます。ユーザーが claude-* モデルを要求した際に本プロバイダーも選択対象になります。" - }, - "preserveClientIp": { - "label": "クライアント IP を転送", - "desc": "x-forwarded-for / x-real-ip を上流に渡します(実際の IP が露出する可能性)", - "help": "プライバシー保護のためデフォルトはオフ。上流側で端末 IP が必要な場合のみ有効化してください。" - }, - "modelWhitelist": { - "title": "モデル許可リスト", - "desc": "このプロバイダーが処理できるモデルを制限します。既定では同タイプのすべてのモデルを処理できます。", - "label": "許可するモデル", - "optional": "(任意)", - "allowAll": "✓ すべてのモデルを許可(推奨)", - "selectedOnly": "選択した {count} 件のモデルのみ許可します。他のモデルはこのプロバイダーにルーティングされません。", - "moreModels": "+{count} 件 さらに表示" - }, - "scheduleParams": { - "title": "スケジューリング設定", - "priority": { - "label": "優先度", - "placeholder": "0", - "desc": "値が小さいほど優先度が高くなります(0 が最も高い)。システムは最も高い優先度のプロバイダーのみから選択します。推奨: メイン=0、予備=1、緊急=2" - }, - "weight": { - "label": "重み", - "placeholder": "1", - "desc": "重み付きランダム。同一優先度内では重みが高いほど選ばれる確率が上がります。例 1:2:3 ≈ 16%:33%:50%" - }, - "costMultiplier": { - "label": "コスト倍率", - "placeholder": "1.0", - "desc": "コスト計算の倍率。公式=1.0、20% 安い=0.8、20% 高い=1.2(小数4桁まで)" - }, - "group": { - "label": "プロバイダーグループ", - "placeholder": "例: premium, economy", - "desc": "グループタグ。ユーザーの providerGroup が一致する場合のみ利用可能。例: \"premium\" に設定すると providerGroup=\"premium\" のユーザーのみ対象" - } - }, - "cacheTtl": { - "label": "キャッシュTTLオーバーライド", - "options": { - "inherit": "オーバーライドしない(クライアントに従う)", - "5m": "5分", - "1h": "1時間" - }, - "desc": "プロンプトキャッシュのTTLを強制設定。cache_controlを含むリクエストにのみ適用されます。" - }, - "context1m": { - "label": "1M コンテキストウィンドウ", - "options": { - "inherit": "継承(クライアントに従う)", - "forceEnable": "強制有効化", - "disabled": "無効" - }, - "desc": "1M コンテキストウィンドウのサポートを設定します。Sonnet モデル(claude-sonnet-4-5、claude-sonnet-4)にのみ適用されます。有効時は段階的料金が適用されます。" - }, - "codexOverrides": { - "reasoningEffort": { - "label": "推論強度オーバーライド", - "help": "回答前にモデルが使う推論の強度(推論トークン量)を制御します。「クライアントに従う」はリクエストを変更しません。その他の値は reasoning.effort を強制します。注意: none は GPT-5.1 のみ、xhigh は GPT-5.1-Codex-Max のみ対応で、未対応モデルではエラーになります。", - "options": { - "inherit": "オーバーライドしない(クライアントに従う)", - "minimal": "minimal", - "low": "low", - "medium": "medium(デフォルト)", - "high": "high", - "xhigh": "xhigh(GPT-5.1-Codex-Max のみ)", - "none": "none(GPT-5.1 のみ)" - } - }, - "reasoningSummary": { - "label": "推論サマリーオーバーライド", - "help": "推論サマリーを返すかどうかを制御します。auto は簡潔、detailed は詳細です。「クライアントに従う」は reasoning.summary を変更しません。", - "options": { - "inherit": "オーバーライドしない(クライアントに従う)", - "auto": "auto", - "detailed": "detailed" - } - }, - "textVerbosity": { - "label": "出力の詳細度オーバーライド", - "help": "モデル出力の冗長さを制御します。low は簡潔、high は詳細です。「クライアントに従う」は text.verbosity を変更しません。", - "options": { - "inherit": "オーバーライドしない(クライアントに従う)", - "low": "low", - "medium": "medium(デフォルト)", - "high": "high" - } - }, - "parallelToolCalls": { - "label": "並列ツール呼び出しオーバーライド", - "help": "並列の tool calls を許可するかどうかを制御します。「クライアントに従う」は parallel_tool_calls を変更しません。無効化すると並列度が下がる可能性があります。", - "options": { - "inherit": "オーバーライドしない(クライアントに従う)", - "true": "強制有効", - "false": "強制無効" - } - } - } - }, - "rateLimit": { - "title": "レート制限", - "summary": { - "fiveHour": "5h: ${amount}", - "weekly": "週: ${amount}", - "monthly": "月: ${amount}", - "total": "総: ${amount}", - "concurrent": "同時: {count}", - "none": "無制限" - }, - "limit5h": { - "label": "5時間の上限 (USD)", - "placeholder": "空欄で無制限" - }, - "limitWeekly": { - "label": "週の上限 (USD)", - "placeholder": "空欄で無制限" - }, - "limitMonthly": { - "label": "月の上限 (USD)", - "placeholder": "空欄で無制限" - }, - "limitTotal": { - "label": "総消費上限 (USD)", - "placeholder": "空欄で無制限" - }, - "limitConcurrent": { - "label": "同時セッション上限", - "placeholder": "0 は無制限" - } - }, - "circuitBreaker": { - "title": "サーキットブレーカー設定", - "summary": "{failureThreshold} 回失敗 / {openDuration} 分間ブレーク / {successThreshold} 回成功で回復 / 各プロバイダー最大 {maxRetryAttempts} 回試行", - "desc": "連続失敗時に自動的にブレークして全体の品質を保護します", - "failureThreshold": { - "label": "失敗しきい値(回)", - "placeholder": "5", - "desc": "何回連続失敗でブレークするか" - }, - "openDuration": { - "label": "ブレーク時間(分)", - "placeholder": "30", - "desc": "ブレーク後、半開に移行するまでの時間" - }, - "successThreshold": { - "label": "回復しきい値(回)", - "placeholder": "2", - "desc": "半開状態で何回成功したら完全回復するか" - }, - "maxRetryAttempts": { - "label": "プロバイダーごとの最大試行回数", - "placeholder": "2", - "desc": "初回呼び出しを含め、同一プロバイダーで試行する上限。超えると他のプロバイダーへ切り替えます。未入力の場合はデフォルト値を使用。" - } - }, - "proxy": { - "title": "プロキシ設定", - "summary": { - "configured": "プロキシ設定済み", - "fallback": "(フォールバック有効)", - "none": "未設定" - }, - "desc": "プロキシを設定して接続性を改善します(HTTP、HTTPS、SOCKS4、SOCKS5)", - "url": { - "label": "プロキシ URL", - "optional": "(任意)", - "placeholder": "例: http://proxy.example.com:8080 または socks5://127.0.0.1:1080", - "formats": "対応フォーマット:" - }, - "fallback": { - "label": "プロキシ失敗時は直接接続にフォールバック", - "desc": "有効にすると、プロキシ接続が失敗した場合に直接接続を試行します" - }, - "test": { - "label": "接続テスト", - "desc": "設定したプロキシ経由でプロバイダー URL への接続をテストします(HEAD リクエスト、課金なし)" - } - }, - "timeout": { - "title": "タイムアウト設定", - "summary": "初回バイト: {streaming}s | ストリーム間隔: {idle}s | 非ストリーミング: {nonStreaming}s", - "desc": "リクエストのタイムアウト時間を設定します。0 は無効を意味します", - "streamingFirstByte": { - "label": "ストリーミング初バイトタイムアウト(秒)", - "placeholder": "30", - "desc": "ストリーミングリクエストの初バイトタイムアウト、範囲1~120秒、デフォルト30秒", - "core": "true" - }, - "streamingIdle": { - "label": "ストリーミングアイドルタイムアウト(秒)", - "placeholder": "60", - "desc": "ストリーミングリクエストのアイドルタイムアウト、範囲60~600秒、0で無効化(途中停止防止)", - "core": "true" - }, - "nonStreamingTotal": { - "label": "非ストリーミング総タイムアウト(秒)", - "placeholder": "600", - "desc": "非ストリーミングリクエストの総タイムアウト、範囲60~1200秒、デフォルト600秒(10分)", - "core": "true" - }, - "disableHint": "0に設定するとタイムアウトを無効にします(カナリアロールバックシナリオのみ、非推奨)" - }, - "codexStrategy": { - "title": "Codex Instructions ポリシー", - "summary": { - "auto": "自動(推奨)", - "force": "公式を強制", - "keep": "そのまま透過" - }, - "desc": "Codex リクエストの instructions フィールドの扱いを制御します。上流ゲートウェイとの互換性に影響します。", - "select": { - "label": "ポリシー選択", - "placeholder": "戦略を選択", - "auto": { - "label": "自動(推奨)", - "desc": "クライアントの instructions を透過し、400 エラー時は公式プロンプトで自動再試行" - }, - "force": { - "label": "公式を強制", - "desc": "常に公式の Codex CLI instructions を使用(約 4000+ 文字)" - }, - "keep": { - "label": "そのまま透過", - "desc": "常にクライアントの instructions を透過し、自動再試行しない(緩い中継向け)" - } - }, - "hint": "ヒント: 88code や foxcode など一部の厳格な Codex 中継では公式 instructions が必要です。「自動」または「公式を強制」を選択してください。" - }, - "mcpPassthrough": { - "title": "MCP パススルー設定", - "summary": { - "none": "無効", - "minimax": "Minimax", - "glm": "GLM", - "custom": "カスタム (予約)" - }, - "desc": "有効にすると、MCP ツール呼び出しを指定された AI プロバイダにパススルーします(例:minimax の画像認識、Web 検索)", - "select": { - "label": "パススルータイプ", - "none": { - "label": "無効", - "desc": "MCP パススルーを有効にしません(デフォルト)" - }, - "minimax": { - "label": "Minimax", - "desc": "minimax MCP サービスにパススルー(画像認識、Web 検索などをサポート)" - }, - "glm": { - "label": "GLM", - "desc": "GLM MCP サービスにパススルー(画像分析、動画分析などをサポート)" - }, - "custom": { - "label": "カスタム", - "desc": "カスタム MCP サービスにパススルー(予約、未実装)" - }, - "placeholder": "パススルータイプを選択" - }, - "hint": "ヒント: MCP パススルーにより、Claude Code クライアントは第三者の AI プロバイダー提供のツール機能(画像認識、Web 検索など)を使用できます", - "urlLabel": "MCP パススルー URL", - "urlPlaceholder": "https://api.minimaxi.com", - "urlDesc": "MCP サービスベース URL。空のままにすると、プロバイダー URL から自動的に抽出されます", - "urlAuto": "自動抽出: {url}" - } - }, - "providerTypes": { - "claude": "Claude (Anthropic Messages API)", - "claudeAuth": "Claude (Anthropic Auth Token)", - "codex": "Codex (Response API)", - "gemini": "Gemini (Google Gemini API)", - "geminiCli": "Gemini CLI", - "geminiCliDisabled": "", - "openaiCompatible": "OpenAI Compatible", - "openaiCompatibleDisabled": " - 開発中" - }, - "deleteDialog": { - "title": "プロバイダーを削除", - "description": "プロバイダー「{name}」を削除しますか?この操作は元に戻せません。", - "cancel": "キャンセル", - "confirm": "削除を確定" - }, - "failureThresholdConfirmDialog": { - "title": "特別な設定を確認", - "descriptionDisabledPrefix": "サーキットブレーカーの失敗閾値を", - "descriptionDisabledValue": "0", - "descriptionDisabledMiddle": "に設定しています。これは", - "descriptionDisabledAction": "サーキットブレーカーを無効化", - "descriptionDisabledSuffix": "することを意味し、プロバイダーは連続した失敗によって遮断されません。", - "descriptionHighValuePrefix": "サーキットブレーカーの失敗閾値を", - "descriptionHighValueSuffix": "に設定しています。これは高い値であり、プロバイダーが多数の失敗の後にのみ遮断される可能性があります。", - "confirmQuestion": "この設定を保存してもよろしいですか?", - "cancel": "キャンセル", - "confirm": "保存を確定" - }, - "errors": { - "invalidUrl": "有効な API アドレスを入力してください", - "invalidWebsiteUrl": "有効な公式サイト URL を入力してください", - "groupTagTooLong": "プロバイダーグループが長すぎます(合計{max}文字まで)", - "addFailed": "プロバイダーの追加に失敗しました", - "updateFailed": "プロバイダーの更新に失敗しました", - "deleteFailed": "プロバイダーの削除に失敗しました" - }, - "success": { - "created": "プロバイダーを追加しました", - "createdDesc": "「{name}」を追加しました" - } - }, - "guide": { - "after": "过滤后:", - "before": "过滤前:", - "bestPracticesConcurrent": "• 并发控制:根据供应商 API 限制设置 Session 并发数", - "bestPracticesCost": "• 成本倍率:官方倍率为 1.0,自建服务可设置为 0.8-1.2", - "bestPracticesLimit": "• 限额设置:根据预算设置 5 小时、7 天、30 天限额", - "bestPracticesPriority": "• 优先级设置:核心供应商设为 0,备用供应商设为 1-3", - "bestPracticesTitle": "最佳实践建议", - "bestPracticesWeight": "• 权重配置:根据供应商容量设置权重(容量大 = 权重高)", - "circuitBreaker": "サーキットブレーカーチェック", - "circuitBreakerOpen": "Aはフィルタされました、残り:B、C、D", - "circuitBreakerRecovery": "Aは60秒後に半開状態に自動復帰します", - "circuitBreakerRecovery5h": "5時間スライディングウィンドウ後に自動復帰", - "costOptimize": "2️⃣ 成本优化:同优先级内,成本倍率低的供应商有更高概率", - "costSort": "コストベースのソートフォールバック", - "costSortExample": "すべてのプロバイダー:A(default)、B(premium)、C(premium)、D(economy)", - "costSortProb": "より安いCがより高い選択確率を持ちます", - "costSortResult": "ソート後:C(0.8x)、A(1.0x)", - "decision": "决策:", - "group": "ユーザーグループフィルタリング", - "groupDesc": "ユーザーがプロバイダーグループを指定した場合、システムはそのグループから優先的に選択します", - "groupDowngrade": "警告をログに記録し、グローバルプロバイダープールから選択", - "groupExample": "ユーザーはproviderGroup = 'premium'を設定しました", - "groupFallback": "グループに利用可能なプロバイダーがない場合は、すべてのプロバイダーにフォールバック", - "groupFiltered": "AとCのみから選択、BとDはフィルタされます", - "groupUnavailable": "ユーザーグループ'vip'のすべてのプロバイダーが無効または制限超過です", - "health": "ヘルスフィルタリング(サーキットブレーカー+レート制限)", - "healthCheck": "Bが有効で健全かをチェック", - "healthCheckAmountLimit": "支出制限超過をチェック(5時間、7日、30日)", - "healthCheckAmountLimitExample": "プロバイダーB制限$10(5時間)、$9.8消費済み", - "healthCheckCircuit": "プロバイダーA 5回失敗、サーキットブレーカー:オープン", - "healthCheckConcurrent": "現在のアクティブセッション数がの制限をチェック", - "healthCheckConcurrentExample": "プロバイダーC制限2、現在アクティブ2セッション", - "healthFilter": "3️⃣ 健康过滤:自动跳过熔断或超限的供应商", - "healthFiltered": "Bはフィルタされました(制限に接近)、残り:C、D", - "healthFiltered2": "Cはフィルタされました(満杯)、残り:D", - "history": "リクエスト履歴をチェック", - "historyDesc": "このAPIキーが過去10秒間に使用したプロバイダーをクエリします", - "priority": "優先度階層化", - "priorityExample": "異なる優先度を持つ4つのプロバイダーが有効です", - "priorityFirst": "1️⃣ 优先级优先:只从最高优先级(数值最小)的供应商中选择", - "priorityResult": "最高優先度(0)にフィルタされました:A、C", - "priorityStep": "システムは優先度でフィルタし、最高優先度のプロバイダーのみを選択します", - "randomResult": "最終的にCがランダムに選択されました", - "randomSelect": "加重ランダム", - "reset": "サーキットブレーカーを手動リセット", - "resetSuccess": "サーキットブレーカーがリセットされました", - "scenario1Desc": "系统首先按优先级过滤,只从最高优先级的供应商中选择", - "scenario1Step1": "初始状态", - "scenario1Step1After": "筛选出最高优先级(0)的供应商:A, C", - "scenario1Step1Before": "供应商 A (优先级 0), B (优先级 1), C (优先级 0), D (优先级 2)", - "scenario1Step1Decision": "只从 A 和 C 中选择,B 和 D 被过滤", - "scenario1Step1Desc": "有 4 个已启用的供应商,优先级各不相同", - "scenario1Step2": "成本排序", - "scenario1Step2After": "排序后:C (0.8x), A (1.0x)", - "scenario1Step2Before": "A (成本 1.0x), C (成本 0.8x)", - "scenario1Step2Decision": "成本更低的 C 有更高的被选中概率", - "scenario1Step2Desc": "在同优先级内,按成本倍率从低到高排序", - "scenario1Step3": "加权随机", - "scenario1Step3After": "C 被选中概率 75%, A 被选中概率 25%", - "scenario1Step3Before": "C (权重 3), A (权重 1)", - "scenario1Step3Decision": "最终随机选择了 C", - "scenario1Step3Desc": "使用权重进行随机选择,权重越高被选中概率越大", - "scenario1Title": "优先级分层选择", - "scenario2Desc": "如果用户指定了供应商组,系统会优先从该组中选择", - "scenario2Step1": "检查用户分组", - "scenario2Step1After": "过滤出 'premium' 组:B, C", - "scenario2Step1Before": "所有供应商:A (default), B (premium), C (premium), D (economy)", - "scenario2Step1Decision": "只从 B 和 C 中选择", - "scenario2Step1Desc": "用户配置了 providerGroup = 'premium'", - "scenario2Step2": "分组降级", - "scenario2Step2After": "降级到所有启用的供应商:A, B, C, D", - "scenario2Step2Before": "用户组 'vip' 内的供应商全部禁用或超限", - "scenario2Step2Decision": "记录警告并从全局供应商池中选择", - "scenario2Step2Desc": "如果用户组内没有可用供应商,降级到所有供应商", - "scenario2Title": "用户分组过滤", - "scenario3Desc": "系统自动过滤掉熔断或超限的供应商", - "scenario3Step1": "熔断器检查", - "scenario3Step1After": "A 被过滤,剩余:B, C, D", - "scenario3Step1Before": "供应商 A 连续失败 5 次,熔断器状态:open", - "scenario3Step1Decision": "A 在 60 秒后自动恢复到半开状态", - "scenario3Step1Desc": "连续失败 5 次后熔断器打开,60 秒内不可用", - "scenario3Step2": "金额限流", - "scenario3Step2After": "B 被过滤(接近限额),剩余:C, D", - "scenario3Step2Before": "供应商 B 的 5 小时限额 $10,已消耗 $9.8", - "scenario3Step2Decision": "5 小时窗口滑动后自动恢复", - "scenario3Step2Desc": "检查 5 小时、7 天、30 天的消费额度是否超限", - "scenario3Step3": "并发 Session 限制", - "scenario3Step3After": "C 被过滤(已满),剩余:D", - "scenario3Step3Before": "供应商 C 并发限制 2,当前活跃 Session 数:2", - "scenario3Step3Decision": "Session 过期(5 分钟)后自动释放", - "scenario3Step3Desc": "检查当前活跃 Session 数是否超过配置的并发限制", - "scenario3Title": "健康度过滤(熔断器 + 限流)", - "scenario4Desc": "连续对话优先使用同一供应商,利用 Claude 的上下文缓存", - "scenario4Step1": "检查历史请求", - "scenario4Step1After": "检查 B 是否启用且健康", - "scenario4Step1Before": "最近一次请求使用了供应商 B", - "scenario4Step1Decision": "B 可用,直接复用,跳过随机选择", - "scenario4Step1Desc": "查询该 API Key 最近 10 秒内使用的供应商", - "scenario4Step2": "复用失效", - "scenario4Step2After": "进入正常选择流程", - "scenario4Step2Before": "上次使用的供应商 B 已被禁用或熔断", - "scenario4Step2Decision": "从其他可用供应商中选择", - "scenario4Step2Desc": "如果上次使用的供应商不可用,则重新选择", - "scenario4Title": "会话复用机制", - "scenariosTitle": "交互式场景演示", - "session": "セッション再利用メカニズム", - "sessionDesc": "前回使用したプロバイダーが利用できない場合は再選択します", - "sessionExample": "最後のリクエストはプロバイダーBを使用しました", - "sessionExpired": "セッションは期限切れ後に自動的に解放されます(5分)", - "sessionFallback": "他の利用可能なプロバイダーから選択", - "sessionLastUsed": "Bは利用可能、直接再利用、ランダム選択をスキップ", - "sessionReuse": "4️⃣ 会话复用:连续对话复用同一供应商,节省上下文成本", - "sessionUnavailable": "前回使用したプロバイダーBは無効またはサーキットオープン状態です", - "step": "步骤", - "title": "核心原则", - "weight": "重みに基づく加重ランダム選択", - "weightCalc": "Cは75%の選択確率、Aは25%", - "weightExample": "C(重み3)、A(重み1)" - }, - "keyLoading": "加载中...", - "noProviders": "暂无服务商配置", - "noProvidersDesc": "添加你的第一个 API 服务商", - "notFound": "プロバイダーが見つかりません", - "official": "官网", - "resetCircuit": "熔断器已重置", - "resetCircuitDesc": "供应商 \"{name}\" 的熔断状态已解除", - "resetCircuitFailed": "重置熔断器失败", - "scheduling": "スケジューリング戦略の詳細説明", - "schedulingDesc": "プロバイダー選択の仕組み:優先度階層化、セッション再利用、ロードバランシング、フェイルオーバーを理解します", - "searchNoResults": "未找到匹配的供应商", - "searchResults": "找到 {count} 个匹配的供应商", - "section": { - "description": "配置上游服务商的金额限流和并发限制,留空表示无限制。", - "leaderboard": "プロバイダーランキング", - "title": "服务商管理" - }, - "filter": { - "status": { - "all": "すべてのステータス", - "active": "有効", - "inactive": "無効" - }, - "groups": { - "label": "グループ:", - "all": "すべて", - "default": "default" - }, - "circuitBroken": "サーキットブレーカー" - }, - "subtitle": "プロバイダー管理", - "subtitleDesc": "上流プロバイダーの支出制限とセッション並行制限を設定します。空のままにすると無制限です。", - "title": "プロバイダー管理", - "todayUsage": "今日用量", - "todayUsageCount": "{count} 次", - "toggleFailed": "状态切换失败", - "toggleSuccess": "供应商已{status}", - "toggleSuccessDesc": "供应商 \"{name}\" 状态已更新", - "updateFailed": "更新服务商失败", - "viewKey": "查看完整 API Key", - "viewKeyDesc": "请妥善保管,不要泄露给他人", - "types": { - "claude": { - "label": "Claude", - "description": "Anthropic 公式 API" - }, - "claudeAuth": { - "label": "Claude Auth", - "description": "Claude リレーサービス" - }, - "codex": { - "label": "Codex", - "description": "Codex CLI API" - }, - "gemini": { - "label": "Gemini", - "description": "Google Gemini API" - }, - "geminiCli": { - "label": "Gemini CLI", - "description": "Gemini CLI API" - }, - "openaiCompatible": { - "label": "OpenAI Compatible", - "description": "OpenAI 互換 API" - } - }, - "list": { - "priority": "優先度", - "weight": "重み", - "costMultiplier": "コスト倍率", - "todayUsageLabel": "本日の使用量", - "todayUsageCount": "{count} 回", - "circuitBroken": "遮断中", - "officialWebsite": "公式", - "viewFullKey": "完全な API キーを表示", - "viewFullKeyDesc": "安全に保管し、他人と共有しないでください", - "keyLoading": "読み込み中...", - "confirmDeleteTitle": "プロバイダーの削除を確認しますか?", - "confirmDeleteMessage": "プロバイダー \"{name}\" を削除してもよろしいですか?この操作は元に戻せません。", - "deleteButton": "削除", - "cancelButton": "キャンセル", - "deleteSuccess": "削除に成功しました", - "deleteSuccessDesc": "プロバイダー \"{name}\" が削除されました", - "deleteFailed": "削除に失敗しました", - "deleteError": "操作中にエラーが発生しました", - "unknownError": "不明なエラー", - "getKeyFailed": "キーの取得に失敗しました", - "keyCopied": "キーがクリップボードにコピーされました", - "copyFailed": "コピーに失敗しました", - "clipboardUnavailable": "この環境ではクリップボードを使用できません。手動でコピーしてください。", - "resetCircuitSuccess": "サーキットブレーカーがリセットされました", - "resetCircuitSuccessDesc": "プロバイダー \"{name}\" のサーキットブレーカーステータスがクリアされました", - "resetCircuitFailed": "サーキットブレーカーのリセットに失敗しました", - "resetUsageTitle": "総用量をリセット", - "resetUsageSuccess": "総用量をリセットしました", - "resetUsageSuccessDesc": "プロバイダー \"{name}\" の総用量をリセットしました", - "resetUsageFailed": "総用量のリセットに失敗しました", - "toggleSuccess": "プロバイダーが{status}になりました", - "toggleSuccessDesc": "プロバイダー \"{name}\" のステータスが更新されました", - "toggleFailed": "切り替えに失敗しました", - "statusEnabled": "有効", - "statusDisabled": "無効" - }, - "inlineEdit": { - "save": "保存", - "cancel": "キャンセル", - "saveSuccess": "保存に成功しました", - "saveFailed": "保存に失敗しました", - "priorityLabel": "優先度", - "weightLabel": "重み", - "costMultiplierLabel": "コスト倍率", - "priorityInvalid": "0 以上の整数を入力してください", - "weightInvalid": "1〜100 の整数を入力してください", - "costMultiplierInvalid": "0以上の数値を入力してください" - }, - "schedulingDialog": { - "title": "プロバイダースケジューリングルール", - "description": "システムが高可用性とコスト最適化のために上流プロバイダーをインテリジェントに選択する方法を理解する", - "triggerButton": "スケジューリングルール", - "step": "ステップ", - "before": "前:", - "after": "後:", - "decision": "決定:" - }, - "sort": { - "byName": "名前順 (A-Z)", - "byPriority": "優先度順 (高-低)", - "byWeight": "重み順 (高-低)", - "byActualPriority": "実際の選択優先順", - "byCreatedAt": "作成日時順 (新-旧)", - "placeholder": "プロバイダーを並べ替え" - }, - "search": { - "placeholder": "プロバイダー名、URL、メモを検索...", - "clear": "検索をクリア", - "found": "{count}件のプロバイダーが見つかりました", - "notFound": "一致するプロバイダーが見つかりません", - "showing": "{filtered} / {total} プロバイダーを表示" - } - }, - "sensitiveWords": { - "add": "センシティブワードを追加", - "addFailed": "センシティブワードの作成に失敗しました", - "addSuccess": "センシティブワードが正常に作成されました", - "cacheStats": "キャッシュ統計:部分一致({containsCount}) 完全一致({exactCount}) 正規表現({regexCount})", - "confirmDelete": "センシティブワード「{word}」を削除してもよろしいですか?", - "delete": "センシティブワードを削除", - "deleteFailed": "削除に失敗しました", - "deleteSuccess": "センシティブワードが正常に削除されました", - "description": "センシティブコンテンツをブロックするためにセンシティブワードフィルタリングルールを設定します。", - "dialog": { - "addDescription": "センシティブワードフィルタリングルールを設定します。マッチしたリクエストはアップストリームに転送されません。", - "addTitle": "センシティブワードを追加", - "creating": "作成中...", - "descriptionLabel": "説明", - "descriptionPlaceholder": "オプション:説明を追加...", - "editDescription": "センシティブワード設定を変更します。変更後、自動的にキャッシュがリフレッシュされます。", - "editTitle": "センシティブワードを編集", - "matchTypeContains": "部分一致 - テキストにこの単語が含まれている場合ブロック", - "matchTypeExact": "完全一致 - 完全に一致する場合のみブロック", - "matchTypeLabel": "マッチタイプ *", - "matchTypeRegex": "正規表現 - 複雑なパターンマッチングをサポート", - "saving": "保存中...", - "wordLabel": "センシティブワード *", - "wordPlaceholder": "センシティブワードを入力...", - "wordRequired": "センシティブワードを入力してください" - }, - "disable": "センシティブワードが無効になりました", - "edit": "センシティブワードを編集", - "editFailed": "センシティブワードの更新に失敗しました", - "editSuccess": "センシティブワードが正常に更新されました", - "emptyState": "センシティブワードがありません。右上の「センシティブワードを追加」をクリックして設定を開始してください。", - "enable": "センシティブワードが有効になりました", - "refreshCache": "キャッシュを更新", - "refreshCacheFailed": "キャッシュのリフレッシュに失敗しました", - "refreshCacheSuccess": "キャッシュのリフレッシュに成功しました。{count}個のセンシティブワードを読み込みました", - "section": { - "description": "センシティブワードによってブロックされたリクエストはアップストリームに転送されず、課金もされません。部分一致、完全一致、正規表現の3つのモードをサポートしています。", - "title": "センシティブワードリスト" - }, - "table": { - "actions": "操作", - "createdAt": "作成日時", - "description": "説明", - "matchType": "マッチタイプ", - "matchTypeContains": "部分一致", - "matchTypeExact": "完全一致", - "matchTypeRegex": "正規表現", - "status": "ステータス", - "word": "センシティブワード" - }, - "title": "センシティブワード管理", - "toggleFailed": "トグルに失敗しました", - "toggleFailedError": "トグルに失敗しました:" - }, - "requestFilters": { - "nav": "リクエストフィルター", - "title": "リクエストフィルター", - "description": "上流に送る前にヘッダー削除/上書きやボディ置換を行い、リクエストをサニタイズします。", - "add": "フィルター追加", - "addSuccess": "作成しました", - "addFailed": "作成に失敗しました", - "edit": "フィルター編集", - "editSuccess": "更新しました", - "editFailed": "更新に失敗しました", - "delete": "フィルター削除", - "deleteSuccess": "削除しました", - "deleteFailed": "削除に失敗しました", - "enable": "有効", - "disable": "無効", - "confirmDelete": "フィルター \"{name}\" を削除しますか?", - "empty": "フィルターがありません。追加してください。", - "refreshCache": "キャッシュ更新", - "refreshSuccess": "{count} 件読み込みました", - "refreshFailed": "更新に失敗しました", - "dialog": { - "createTitle": "フィルター追加", - "editTitle": "フィルター編集", - "name": "名称", - "scope": "スコープ", - "action": "アクション", - "target": "対象フィールド/パス", - "replacement": "置換値 (任意)", - "description": "説明 (任意)", - "priority": "優先度", - "matchType": "マッチタイプ", - "matchTypeContains": "含む", - "matchTypeExact": "完全一致", - "matchTypeRegex": "正規表現", - "jsonPathPlaceholder": "例: messages.0.content / data.items[0].token", - "targetPlaceholder": "ヘッダー名またはテキスト/パス", - "replacementPlaceholder": "文字列またはJSON。空でクリア", - "save": "保存", - "saving": "保存中...", - "validation": { - "fieldRequired": "名称と対象は必須です" - }, - "bindingType": "適用先", - "bindingGlobal": "全プロバイダー(グローバル)", - "bindingProviders": "特定のプロバイダー", - "bindingGroups": "プロバイダーグループ", - "selectProviders": "プロバイダーを選択...", - "selectGroups": "グループを選択...", - "searchProviders": "プロバイダー検索...", - "searchGroups": "グループ検索...", - "noProvidersFound": "プロバイダーが見つかりません", - "noGroupsFound": "グループが見つかりません", - "providersSelected": "{count}件のプロバイダーを選択", - "groupsSelected": "{count}件のグループを選択", - "loading": "読み込み中...", - "clear": "クリア", - "selectAll": "すべて選択" - }, - "table": { - "name": "名称", - "scope": "スコープ", - "action": "アクション", - "target": "対象", - "replacement": "置換値", - "priority": "優先度", - "apply": "適用", - "status": "状態", - "createdAt": "作成日時", - "actions": "操作" - }, - "scopeLabel": { - "header": "Header", - "body": "Body" - }, - "actionLabel": { - "remove": "ヘッダー削除", - "set": "ヘッダー設定", - "json_path": "JSONパス置換", - "text_replace": "テキスト置換" - }, - "applyToAll": "すべてのリクエストに適用", - "providers": "プロバイダー", - "groups": "グループ" - }, - "errorRules": { - "nav": "エラールール", - "title": "エラールール管理", - "description": "自動再試行を行わないクライアントエラールールを管理します。設定後、ルールに一致するエラーは再試行せずユーザーに直接返され、プロバイダーのサーキットブレーカーにもカウントされません。", - "section": { - "title": "エラールールリスト" - }, - "tester": { - "title": "エラールールテスト", - "description": "エラーメッセージを入力して、設定済みルールに一致するかと最終的な返却内容を確認します。", - "inputLabel": "テストするエラーメッセージ", - "inputPlaceholder": "検証したいエラーメッセージを入力...", - "testButton": "テストを実行", - "testing": "テスト中...", - "matched": "エラールールに一致しました", - "notMatched": "一致するルールなし", - "finalResponse": "オーバーライドレスポンス", - "ruleInfo": "一致したルール", - "noRule": "一致したルールはありません", - "category": "カテゴリ", - "pattern": "パターン", - "matchType": "マッチタイプ", - "overrideStatusCode": "オーバーライドステータスコード", - "testFailed": "テストに失敗しました。再度お試しください", - "messageRequired": "テストするエラーメッセージを入力してください", - "warnings": "設定の警告", - "statusCodeOnlyOverride": "ステータスコードのみオーバーライドされ、レスポンスボディはアップストリームのエラーが使用されます" - }, - "add": "エラールールを追加", - "addSuccess": "エラールールが正常に作成されました", - "addFailed": "エラールールの作成に失敗しました", - "edit": "エラールールを編集", - "editSuccess": "エラールールが正常に更新されました", - "editFailed": "エラールールの更新に失敗しました", - "delete": "エラールールを削除", - "deleteSuccess": "エラールールが正常に削除されました", - "deleteFailed": "削除に失敗しました", - "enable": "エラールールが有効になりました", - "disable": "エラールールが無効になりました", - "toggleFailed": "切り替えに失敗しました", - "toggleFailedError": "切り替えに失敗しました:", - "refreshCache": "ルールを同期", - "refreshCacheSuccess": "ルールが正常に同期され、{count} 個のエラールールがロードされました", - "refreshCacheFailed": "ルールの同期に失敗しました", - "cacheStats": "キャッシュ: {totalCount}件のルール", - "emptyState": "エラールールがまだありません。右上の「エラールールを追加」をクリックして設定を開始してください。", - "confirmDelete": "エラールール \"{pattern}\" を削除してもよろしいですか?", - "dialog": { - "addTitle": "エラールールを追加", - "addDescription": "エラーメッセージの正規表現パターンを設定します。マッチしたエラーはリトライ不可能なクライアントエラーとして識別されます。", - "editTitle": "エラールールを編集", - "editDescription": "エラールール設定を変更します。変更後、キャッシュが自動的に更新されます。", - "patternLabel": "正規表現パターン *", - "patternPlaceholder": "正規表現を入力...", - "patternRequired": "正規表現パターンを入力してください", - "patternHint": "JavaScript正規表現構文をサポート、例:prompt is too long|invalid.*request", - "categoryLabel": "ルールカテゴリ *", - "categoryPlaceholder": "ルールカテゴリを選択", - "categoryRequired": "ルールカテゴリを選択してください", - "categoryHint": "分類管理と統計のためのエラーカテゴリを選択", - "descriptionLabel": "説明", - "descriptionPlaceholder": "オプション: 説明を追加...", - "invalidRegex": "正規表現の構文エラー", - "regexTester": "正規表現テスター", - "testMessageLabel": "テストメッセージ", - "testMessagePlaceholder": "テストするエラーメッセージを入力...", - "matchSuccess": "マッチ成功", - "matchFailed": "マッチなし", - "invalidPattern": "無効な正規表現", - "matchedText": "マッチしたテキスト", - "defaultRuleHint": "デフォルトルールのパターンは変更できません", - "enableOverride": "エラーオーバーライドを有効にする", - "enableOverrideHint": "有効にすると、クライアントに返すエラーレスポンスとステータスコードをカスタマイズできます。元のエラーはデータベースに記録されます。現在、Claude APIエラー形式のみサポートしています。", - "overrideResponseLabel": "オーバーライドレスポンス(JSON形式)", - "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"カスタムメッセージ\"\n }\n}", - "overrideResponseHint": "空白のままにするとステータスコードのみオーバーライドします。", - "overrideStatusCodeLabel": "オーバーライドステータスコード(オプション)", - "overrideStatusCodePlaceholder": "例: 400", - "overrideStatusCodeHint": "空白のままにするとアップストリームのステータスコードを使用します。範囲: 400-599。", - "useTemplate": "Claude Error テンプレート", - "useTemplateConfirm": "入力済みの内容をテンプレートで上書きしますか?", - "validJson": "JSON 形式は有効です", - "invalidJson": "JSON形式が無効です", - "invalidStatusCode": "ステータスコードは400-599の範囲内でなければなりません", - "creating": "作成中...", - "saving": "保存中..." - }, - "table": { - "pattern": "正規表現パターン", - "category": "ルールカテゴリ", - "description": "説明", - "status": "ステータス", - "default": "デフォルト", - "isEnabled": "有効状態", - "isDefault": "デフォルトルール", - "createdAt": "作成日時", - "actions": "操作" - }, - "form": { - "fields": { - "pattern": "正規表現パターン", - "category": "ルールカテゴリ", - "description": "説明" - }, - "placeholders": { - "pattern": "例: prompt is too long", - "category": "カテゴリを選択", - "description": "オプション: 説明を追加..." - }, - "labels": { - "pattern": "正規表現パターン *", - "category": "ルールカテゴリ *", - "description": "説明", - "isEnabled": "有効状態" - } - }, - "actions": { - "add": "追加", - "edit": "編集", - "delete": "削除", - "refresh": "更新", - "test": "テスト", - "messages": { - "success": "操作が成功しました", - "error": "操作が失敗しました" - } - }, - "validation": { - "patternRequired": "正規表現パターンを入力してください", - "categoryRequired": "ルールカテゴリを選択してください", - "patternInvalid": "正規表現の構文エラー", - "redosRisk": "正規表現に ReDoS リスクがあります。パターンを簡略化してください", - "patternTooComplex": "正規表現が複雑すぎます" - }, - "categories": { - "prompt_limit": "プロンプト長制限", - "content_filter": "コンテンツフィルター", - "pdf_limit": "PDF ページ制限", - "thinking_error": "Thinking フォーマットエラー", - "parameter_error": "パラメータ検証失敗", - "invalid_request": "無効なリクエスト", - "cache_limit": "キャッシュ制御制限" - }, - "regexTester": { - "title": "正規表現テスター", - "testMessage": "テストメッセージ", - "testMessagePlaceholder": "テストするエラーメッセージを入力...", - "matchResult": "マッチ結果", - "matched": "マッチしました", - "notMatched": "マッチしませんでした", - "test": "テスト" - }, - "defaultRules": { - "cannotDelete": "デフォルトルールは削除できません", - "cannotDisable": "デフォルトルールは有効のままにすることをお勧めします" - } - }, - "mcpPassthroughConfig": "MCP パススルー設定", - "mcpPassthroughConfigNone": "無効", - "mcpPassthroughConfigMinimax": "Minimax", - "mcpPassthroughConfigGlm": "GLM", - "mcpPassthroughConfigCustom": "カスタム (予約)", - "mcpPassthroughDesc": "有効にすると、MCP ツール呼び出しを指定された AI プロバイダにパススルーします(例:minimax の画像認識、Web 検索)", - "mcpPassthroughSelect": "パススルータイプ", - "mcpPassthroughNoneLabel": "無効", - "mcpPassthroughNoneDesc": "MCP パススルーを有効にしません(デフォルト)", - "mcpPassthroughMinimaxLabel": "Minimax", - "mcpPassthroughMinimaxDesc": "minimax MCP サービスにパススルー(画像認識、Web 検索などをサポート)", - "mcpPassthroughGlmLabel": "GLM", - "mcpPassthroughGlmDesc": "GLM MCP サービスにパススルー(画像分析、動画分析などをサポート)", - "mcpPassthroughCustomLabel": "カスタム", - "mcpPassthroughCustomDesc": "カスタム MCP サービスにパススルー(予約、未実装)", - "mcpPassthroughHint": "ヒント: MCP パススルーにより、Claude Code クライアントは第三者の AI プロバイダ提供的ツール機能(画像認識、Web 検索など)を使用できます", - "mcpPassthroughUrlLabel": "MCP パススルー URL", - "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com", - "mcpPassthroughUrlDesc": "MCP サービスベース URL。空のままにすると、プロバイダ URL から自動的に抽出されます", - "mcpPassthroughUrlAuto": "自動抽出: {url}" -} diff --git a/messages/ja/settings/clientVersions.json b/messages/ja/settings/clientVersions.json new file mode 100644 index 000000000..65cafc17b --- /dev/null +++ b/messages/ja/settings/clientVersions.json @@ -0,0 +1,51 @@ +{ + "description": "クライアントバージョン要件を管理し、ユーザーが最新の安定版を使用していることを確認します。VSCodeとCLIは個別に管理されます。", + "empty": { + "description": "過去7日間に認識可能なクライアントを使用したアクティブユーザーがいません", + "title": "クライアントデータなし" + }, + "features": { + "activeWindow": "アクティブウィンドウ:", + "activeWindowDesc": "過去7日間にリクエストがあったユーザーのみを集計", + "autoDetect": "システムは各クライアントの最新安定版(GAバージョン)を自動検出します", + "blockOldVersion": "旧バージョンを使用するユーザーはHTTP 400エラーを受信し、サービスを継続使用できません", + "errorMessage": "エラーメッセージには現在のバージョンとアップグレード必要バージョン番号が含まれます", + "gaRule": "判定ルール:", + "gaRuleDesc": "1人以上のユーザーが使用しているバージョンをGAバージョンとみなします", + "recommendation": "推奨方法:", + "recommendationDesc": "まず下記のバージョン分布を確認し、新バージョンが安定していることを確認してから有効にしてください。", + "title": "機能説明", + "whatHappens": "有効化後の動作:" + }, + "section": { + "distribution": { + "description": "過去7日間のアクティブユーザーのクライアントバージョン情報を表示します。各クライアントタイプごとにGAバージョンを独立して集計します。", + "title": "クライアントバージョン分布" + }, + "settings": { + "description": "有効にすると、システムはクライアントバージョンを自動的に検出し、旧バージョンユーザーのリクエストをブロックします。", + "title": "アップグレードリマインダー設定" + } + }, + "table": { + "currentGA": "現在のGAバージョン:", + "internalType": "内部タイプ:", + "lastActive": "最終アクティブ時間", + "latest": "最新版", + "needsUpgrade": "アップグレード必要", + "noUsers": "ユーザーデータなし", + "status": "ステータス", + "unknown": "不明", + "user": "ユーザー", + "usersCount": "{count}名のユーザー", + "version": "現在のバージョン" + }, + "title": "クライアント更新リマインダー", + "toggle": { + "description": "有効にすると、システムはクライアントバージョンを自動的に検出し、古いバージョンをブロックします。", + "disableSuccess": "クライアントバージョンチェックが無効になりました", + "enable": "クライアントバージョンチェックを有効にする", + "enableSuccess": "クライアントバージョンチェックが有効になりました", + "toggleFailed": "トグルに失敗しました" + } +} diff --git a/messages/ja/settings/common.json b/messages/ja/settings/common.json new file mode 100644 index 000000000..ff93e78b3 --- /dev/null +++ b/messages/ja/settings/common.json @@ -0,0 +1,31 @@ +{ + "cancel": "キャンセル", + "completed": "完了", + "confirm": "確認", + "copied": "キーをクリップボードにコピーしました", + "copy": "コピー", + "copyFailed": "コピーに失敗しました", + "create": "作成", + "creating": "作成中...", + "delete": "削除", + "disabled": "無効", + "edit": "編集", + "empty": "結果が見つかりません", + "enabled": "有効", + "error": "不明なエラー", + "failed": "失敗", + "loading": "読み込み中...", + "none": "なし(このバージョンを使用しているユーザーなし)", + "refresh": "更新", + "reset": "リセット", + "save": "保存する", + "saving": "保存しています...", + "submit": "送信", + "success": "成功しました", + "test": "テスト", + "testing": "テスト中...", + "unlimited": "無制限", + "unlimited_desc": "無制限", + "update": "更新する", + "updating": "更新しています..." +} diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json new file mode 100644 index 000000000..88d3e69b2 --- /dev/null +++ b/messages/ja/settings/config.json @@ -0,0 +1,89 @@ +{ + "autoCleanup": "ログ自動クリーンアップ", + "autoCleanupDesc": "スケジュールに従って履歴ログを自動的にクリーンアップし、データベース容量を解放します。", + "description": "システムの基本パラメータを管理し、サイト表示と統計動作に影響します。", + "form": { + "allowGlobalView": "グローバル使用量表示を許可", + "allowGlobalViewDesc": "無効にすると、一般ユーザーはダッシュボードで自分のキーの使用統計のみを表示できます。", + "autoCleanupSaved": "自動クリーンアップ設定が保存されました", + "billingModelSource": "課金モデルソース", + "billingModelSourceDesc": "モデルリダイレクト時に課金に使用するモデルを設定します。「リダイレクト前」はユーザーがリクエストした元のモデル、「リダイレクト後」は実際に呼び出されたモデルを使用します。", + "billingModelSourceOptions": { + "original": "リダイレクト前(元のモデル)", + "redirected": "リダイレクト後(実際のモデル)" + }, + "billingModelSourcePlaceholder": "課金モデルソースを選択", + "cleanupBatchSize": "バッチサイズ", + "cleanupBatchSizeDesc": "バッチごとに削除するレコード数(範囲:1000-100000、推奨10000)", + "cleanupBatchSizePlaceholder": "10000", + "cleanupBatchSizeRequired": "バッチサイズ *", + "cleanupRetentionDays": "保持日数", + "cleanupRetentionDaysDesc": "この日数を超えるログは自動的にクリーンアップされます(範囲:1-365日)", + "cleanupRetentionDaysPlaceholder": "30", + "cleanupRetentionDaysRequired": "保持日数 *", + "cleanupSchedule": "クリーンアップスケジュール", + "cleanupScheduleCronDesc": "Cron式、デフォルト:0 2 * * *(毎日午前2時)", + "cleanupScheduleCronExample": "例:0 3 * * 0(毎週日曜日午前3時)", + "cleanupScheduleDesc": "自動クリーンアップの実行スケジュールを選択します", + "cleanupScheduleLabel": "実行時間(Cron)", + "cleanupSchedulePlaceholder": "0 2 * * *", + "cleanupScheduleRequired": "実行時間(Cron)*", + "configUpdated": "システム設定が更新されました。ページが更新され、通貨表示の変更が適用されます。", + "currencies": { + "CNY": "¥ 人民元 (CNY)", + "EUR": "€ ユーロ (EUR)", + "GBP": "£ 英ポンド (GBP)", + "HKD": "HK$ 香港ドル (HKD)", + "JPY": "¥ 日本円 (JPY)", + "KRW": "₩ 韓国ウォン (KRW)", + "SGD": "S$ シンガポールドル (SGD)", + "TWD": "NT$ 新台湾ドル (TWD)", + "USD": "$ 米ドル (USD)" + }, + "currencyDisplay": "通貨表示単位", + "currencyDisplayDesc": "変更後、システムのすべてのページとAPIインターフェースは対応する通貨記号を使用します(記号のみ、為替レート変換なし)。", + "currencyDisplayPlaceholder": "通貨単位を選択", + "enableAutoCleanup": "自動クリーンアップを有効にする", + "enableAutoCleanupDesc": "スケジュールに従って履歴ログを自動的にクリーンアップします", + "enableHttp2": "HTTP/2 を有効にする", + "enableHttp2Desc": "有効にすると、プロキシ要求は優先的に HTTP/2 を使用します。HTTP/2 が失敗した場合は自動的に HTTP/1.1 にフォールバックします。", + "enableResponseFixer": "レスポンス整流を有効化", + "enableResponseFixerDesc": "上流応答の一般的な形式問題(エンコーディング、SSE、途切れた JSON)を自動修復します(既定で有効)。", + "enableThinkingSignatureRectifier": "thinking 署名整流を有効化", + "enableThinkingSignatureRectifierDesc": "Anthropic プロバイダーで thinking 署名の不整合や不正なリクエストエラーが発生した場合、thinking 関連ブロックを削除して同一プロバイダーへ1回だけ再試行します(既定で有効)。", + "interceptAnthropicWarmupRequests": "Warmup リクエストを遮断(Anthropic)", + "interceptAnthropicWarmupRequestsDesc": "有効にすると、Claude Code の Warmup プローブ要求は CCH が直接短い応答を返し、上流プロバイダーへのリクエストを回避します。ログには残りますが、課金/レート制限/統計には含まれません。", + "keepDays": "保持日数", + "keepDaysDesc": "指定した日数より古いログをクリーンアップします", + "responseFixerFixEncoding": "エンコーディングを修復", + "responseFixerFixEncodingDesc": "BOM/NULL バイトを除去し、無効な UTF-8 を正規化します。", + "responseFixerFixSseFormat": "SSE 形式を修復", + "responseFixerFixSseFormatDesc": "不足している data: 前置きを補い、改行を正規化し、よくあるフィールド形式を修正します。", + "responseFixerFixTruncatedJson": "途切れた JSON を修復", + "responseFixerFixTruncatedJsonDesc": "未閉じの括弧/引用符を補い、末尾カンマを除去し、必要に応じて null を補完します。", + "saveConfig": "設定を保存", + "saveError": "保存に失敗しました", + "saveFailed": "保存に失敗しました", + "saveSettings": "設定を保存", + "saveSuccess": "正常に保存されました", + "siteTitle": "サイトタイトル", + "siteTitleDesc": "ブラウザタブのタイトルとシステムのデフォルト表示名を設定するために使用されます。", + "siteTitlePlaceholder": "例:Claude Code Hub", + "siteTitleRequired": "サイトタイトルは空にできません", + "verboseProviderError": "詳細なプロバイダーエラー", + "verboseProviderErrorDesc": "有効にすると、すべてのプロバイダーが利用不可の場合に詳細なエラーメッセージ(プロバイダー数、レート制限の理由など)を返します。無効の場合は簡潔なエラーコードのみを返します。" + }, + "section": { + "autoCleanup": { + "description": "スケジュールに従って履歴ログを自動的にクリーンアップし、データベース容量を解放します。", + "title": "ログ自動クリーンアップ" + }, + "siteParams": { + "description": "サイトタイトル、通貨表示単位、ダッシュボード統計表示ポリシーを設定します。", + "title": "サイトパラメータ" + } + }, + "siteSettings": "サイトパラメータ", + "siteSettingsDesc": "サイトタイトル、通貨表示単位、ダッシュボード統計表示方針を設定します。", + "title": "基本設定" +} diff --git a/messages/ja/settings/data.json b/messages/ja/settings/data.json new file mode 100644 index 000000000..de55c4a7a --- /dev/null +++ b/messages/ja/settings/data.json @@ -0,0 +1,133 @@ +{ + "cleanup": { + "backupRecommendation": "推奨:クリーンアップ前にデータベースバックアップをエクスポートして、データ復元が必要な場合に備えてください。", + "button": "ログをクリーンアップ", + "cancel": "キャンセル", + "cleaning": "クリーンアップ中...", + "confirm": "クリーンアップを確認", + "confirmTitle": "ログクリーンアップの確認", + "confirmWarning": "この操作は{range}のすべてのログレコードを完全に削除し、復元できません。", + "descriptionWarning": "履歴ログデータをクリーンアップしてデータベースストレージを解放します。注:統計データは保持されますが、ログ詳細は完全に削除されます。", + "error": "ログのクリーンアップに失敗しました", + "failed": "クリーンアップ失敗", + "logsDeleted": "✗ ログ詳細は削除されます(リクエスト/レスポンス内容、エラー情報など)", + "previewCount": "{count}件のログレコードを削除します", + "previewError": "プレビュー情報を取得できません", + "previewLoading": "集計中...", + "range": { + "180days": "6ヶ月前のログ(180日)", + "30days": "1ヶ月前のログ(30日)", + "7days": "1週間前のログ(7日)", + "90days": "3ヶ月前のログ(90日)" + }, + "rangeDescription": { + "180days": "6ヶ月前", + "30days": "1ヶ月前", + "7days": "1週間前", + "90days": "3ヶ月前", + "default": "{days}日前" + }, + "rangeLabel": "クリーンアップ範囲", + "statisticsRetained": "✓ 統計データは保持されます(トレンド分析用)", + "successMessage": "{count}件のログレコードをクリーンアップしました({batches}バッチ、所要時間{duration}秒)", + "willClean": "{range}のすべてのログレコードをクリーンアップします" + }, + "description": "データベースのバックアップと復元を管理し、完全なインポート/エクスポートとログクリーンアップをサポートします。", + "export": { + "button": "データベースをエクスポート", + "descriptionFull": "完全なデータベースバックアップファイル(.dump形式)をエクスポートし、データ移行または復旧に使用できます。バックアップはPostgreSQL custom formatを使用し、自動圧縮され、異なるデータベースバージョンと互換性があります。", + "error": "データベースのエクスポートに失敗しました", + "exporting": "エクスポート中...", + "failed": "エクスポート失敗", + "successMessage": "データベースのエクスポートに成功しました!" + }, + "guide": { + "items": { + "cleanup": { + "description": "履歴ログを物理的に削除します(取り消せません)。統計テーブルは保持されます。クリーンアップ前にデータベースバックアップをエクスポートすることをお勧めします。", + "title": "ログクリーンアップ" + }, + "environment": { + "description": "Docker Composeデプロイメントが必要です。ローカル開発環境ではサポートされない可能性があります。", + "title": "環境要件" + }, + "format": { + "description": "PostgreSQL custom format(.dump)を使用し、自動圧縮で異なるデータベースバージョンと互換性があります。", + "title": "バックアップ形式" + }, + "merge": { + "description": "既存データを保持し、バックアップからのデータを挿入しようとします。主キーの競合がインポート失敗を引き起こす可能性があります。", + "title": "統合モード" + }, + "overwrite": { + "description": "インポート前にすべての既存データを削除します。完全復元に最適です。", + "title": "上書きモード" + }, + "safety": { + "description": "インポート前に現在のデータベースをバックアップとしてエクスポートすることをお勧めします。", + "title": "セキュリティ推奨" + } + }, + "title": "使用説明と注意事項" + }, + "import": { + "backupFile": "バックアップファイル:", + "backupRecommendation": "この操作を実行する前に、現在のデータベースをバックアップとしてエクスポートすることをお勧めします。", + "button": "データベースをインポート", + "cancel": "キャンセル", + "cleanFirstDescription": "インポート前にすべての既存データを削除し、データベースがバックアップと完全に一致するようにします。チェックしない場合、データのマージを試みますが、主キーの競合により失敗する可能性があります。", + "cleanFirstLabel": "既存データをクリア(上書きモード)", + "confirm": "インポートを確認", + "confirmMerge": "「マージモード」を選択しました。これにより、既存データを保持しながらバックアップのインポートを試みます。", + "confirmOverwrite": "「上書きモード」を選択しました。これにより、すべての既存データが削除された後、バックアップがインポートされます。", + "confirmTitle": "データベースインポートの確認", + "descriptionFull": "バックアップファイルからデータベースを復元します。PostgreSQL custom format(.dump)形式のバックアップファイルをサポートします。", + "error": "データベースのインポートに失敗しました", + "errorUnknown": "不明なエラー", + "failedMessage": "データインポート失敗、詳細ログを確認してください", + "fileError": ".dump形式のバックアップファイルを選択してください", + "fileSelected": "選択済み:{name}({size} MB)", + "importing": "インポート中...", + "noFileSelected": "最初にバックアップファイルを選択してください", + "parseError": "応答データの解析に失敗しました", + "progressTitle": "インポート進行状況", + "selectFileLabel": "バックアップファイルを選択", + "streamError": "レスポンスストリームを読み取れません", + "streamInterrupted": "データストリームが予期せず中断されました", + "streamInterruptedDesc": "インポートの進行が正常に完了しませんでした。ログを確認してデータ整合性を検証してください。問題がある場合は再度インポートしてください。", + "successCleanModeDesc": "すべてのデータが正常に復元されました。ページ表示が異常な場合はブラウザを更新してください。", + "successMergeModeDesc": "データのインポートとマージが正常に完了しました。ページ表示が異常な場合はブラウザを更新してください。", + "successMessage": "データインポート完了!", + "successWithWarnings": "データのインポートが完了しました(警告あり)", + "successWithWarningsDesc": "データのインポートは成功しましたが、既に存在する一部のオブジェクトはスキップされました。ページ表示が異常な場合はブラウザを更新するか、アプリを再起動してください。", + "warningMerge": "注意:主キーの競合が存在する場合、インポートが失敗する可能性があります。", + "warningOverwrite": "警告:この操作は元に戻せません。すべての現在のデータが完全に削除されます!" + }, + "section": { + "cleanup": { + "description": "履歴ログデータをクリーンアップしてデータベースストレージを解放します。注:統計データは保持されますが、ログ詳細は完全に削除されます。", + "title": "ログクリーンアップ" + }, + "export": { + "description": "完全なデータベースバックアップファイル(.dump形式)をエクスポートし、データ移行または復旧に使用できます。", + "title": "データエクスポート" + }, + "import": { + "description": "バックアップファイルからデータベースを復元します。PostgreSQL custom format(.dump)形式のバックアップファイルをサポートします。", + "title": "データインポート" + }, + "status": { + "description": "現在のデータベース接続状態と基本情報を表示します。", + "title": "データベースステータス" + } + }, + "status": { + "connected": "データベース接続正常", + "error": "データベースステータスの取得に失敗しました", + "loading": "読み込み中...", + "retry": "再試行", + "tables": "{count} テーブル", + "unavailable": "データベース利用不可" + }, + "title": "データ管理" +} diff --git a/messages/ja/settings/errorRules.json b/messages/ja/settings/errorRules.json new file mode 100644 index 000000000..5f680ef97 --- /dev/null +++ b/messages/ja/settings/errorRules.json @@ -0,0 +1,157 @@ +{ + "actions": { + "add": "追加", + "delete": "削除", + "edit": "編集", + "messages": { + "error": "操作が失敗しました", + "success": "操作が成功しました" + }, + "refresh": "更新", + "test": "テスト" + }, + "add": "エラールールを追加", + "addFailed": "エラールールの作成に失敗しました", + "addSuccess": "エラールールが正常に作成されました", + "cacheStats": "キャッシュ: {totalCount}件のルール", + "categories": { + "cache_limit": "キャッシュ制御制限", + "content_filter": "コンテンツフィルター", + "invalid_request": "無効なリクエスト", + "parameter_error": "パラメータ検証失敗", + "pdf_limit": "PDF ページ制限", + "prompt_limit": "プロンプト長制限", + "thinking_error": "Thinking フォーマットエラー" + }, + "confirmDelete": "エラールール \"{pattern}\" を削除してもよろしいですか?", + "defaultRules": { + "cannotDelete": "デフォルトルールは削除できません", + "cannotDisable": "デフォルトルールは有効のままにすることをお勧めします" + }, + "delete": "エラールールを削除", + "deleteFailed": "削除に失敗しました", + "deleteSuccess": "エラールールが正常に削除されました", + "description": "自動再試行を行わないクライアントエラールールを管理します。設定後、ルールに一致するエラーは再試行せずユーザーに直接返され、プロバイダーのサーキットブレーカーにもカウントされません。", + "dialog": { + "addDescription": "エラーメッセージの正規表現パターンを設定します。マッチしたエラーはリトライ不可能なクライアントエラーとして識別されます。", + "addTitle": "エラールールを追加", + "categoryHint": "分類管理と統計のためのエラーカテゴリを選択", + "categoryLabel": "ルールカテゴリ *", + "categoryPlaceholder": "ルールカテゴリを選択", + "categoryRequired": "ルールカテゴリを選択してください", + "creating": "作成中...", + "defaultRuleHint": "デフォルトルールのパターンは変更できません", + "descriptionLabel": "説明", + "descriptionPlaceholder": "オプション: 説明を追加...", + "editDescription": "エラールール設定を変更します。変更後、キャッシュが自動的に更新されます。", + "editTitle": "エラールールを編集", + "enableOverride": "エラーオーバーライドを有効にする", + "enableOverrideHint": "有効にすると、クライアントに返すエラーレスポンスとステータスコードをカスタマイズできます。元のエラーはデータベースに記録されます。現在、Claude APIエラー形式のみサポートしています。", + "invalidJson": "JSON形式が無効です", + "invalidPattern": "無効な正規表現", + "invalidRegex": "正規表現の構文エラー", + "invalidStatusCode": "ステータスコードは400-599の範囲内でなければなりません", + "matchFailed": "マッチなし", + "matchSuccess": "マッチ成功", + "matchedText": "マッチしたテキスト", + "overrideResponseHint": "空白のままにするとステータスコードのみオーバーライドします。", + "overrideResponseLabel": "オーバーライドレスポンス(JSON形式)", + "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"カスタムメッセージ\"\n }\n}", + "overrideStatusCodeHint": "空白のままにするとアップストリームのステータスコードを使用します。範囲: 400-599。", + "overrideStatusCodeLabel": "オーバーライドステータスコード(オプション)", + "overrideStatusCodePlaceholder": "例: 400", + "patternHint": "JavaScript正規表現構文をサポート、例:prompt is too long|invalid.*request", + "patternLabel": "正規表現パターン *", + "patternPlaceholder": "正規表現を入力...", + "patternRequired": "正規表現パターンを入力してください", + "regexTester": "正規表現テスター", + "saving": "保存しています...", + "testMessageLabel": "テストメッセージ", + "testMessagePlaceholder": "テストするエラーメッセージを入力...", + "useTemplate": "Claude Error テンプレート", + "useTemplateConfirm": "入力済みの内容をテンプレートで上書きしますか?", + "validJson": "JSON 形式は有効です" + }, + "disable": "エラールールが無効になりました", + "edit": "エラールールを編集", + "editFailed": "エラールールの更新に失敗しました", + "editSuccess": "エラールールが正常に更新されました", + "emptyState": "エラールールがまだありません。右上の「エラールールを追加」をクリックして設定を開始してください。", + "enable": "エラールールが有効になりました", + "form": { + "fields": { + "category": "ルールカテゴリ", + "description": "説明", + "pattern": "正規表現パターン" + }, + "labels": { + "category": "ルールカテゴリ *", + "description": "説明", + "isEnabled": "有効状態", + "pattern": "正規表現パターン *" + }, + "placeholders": { + "category": "カテゴリを選択", + "description": "オプション: 説明を追加...", + "pattern": "例: prompt is too long" + } + }, + "nav": "エラールール", + "refreshCache": "ルールを同期", + "refreshCacheFailed": "ルールの同期に失敗しました", + "refreshCacheSuccess": "ルールが正常に同期され、{count} 個のエラールールがロードされました", + "regexTester": { + "matchResult": "マッチ結果", + "matched": "マッチしました", + "notMatched": "マッチしませんでした", + "test": "テスト", + "testMessage": "テストメッセージ", + "testMessagePlaceholder": "テストするエラーメッセージを入力...", + "title": "正規表現テスター" + }, + "section": { + "title": "エラールールリスト" + }, + "table": { + "actions": "アクション", + "category": "ルールカテゴリ", + "createdAt": "作成日時", + "default": "デフォルト", + "description": "説明", + "isDefault": "デフォルトルール", + "isEnabled": "有効状態", + "pattern": "正規表現パターン", + "status": "ステータス" + }, + "tester": { + "category": "カテゴリ", + "description": "エラーメッセージを入力して、設定済みルールに一致するかと最終的な返却内容を確認します。", + "finalResponse": "オーバーライドレスポンス", + "inputLabel": "テストするエラーメッセージ", + "inputPlaceholder": "検証したいエラーメッセージを入力...", + "matchType": "マッチタイプ", + "matched": "エラールールに一致しました", + "messageRequired": "テストするエラーメッセージを入力してください", + "noRule": "一致したルールはありません", + "notMatched": "一致するルールなし", + "overrideStatusCode": "オーバーライドステータスコード", + "pattern": "パターン", + "ruleInfo": "一致したルール", + "statusCodeOnlyOverride": "ステータスコードのみオーバーライドされ、レスポンスボディはアップストリームのエラーが使用されます", + "testButton": "テストを実行", + "testFailed": "テストに失敗しました。再度お試しください", + "testing": "テスト中...", + "title": "エラールールテスト", + "warnings": "設定の警告" + }, + "title": "エラールール管理", + "toggleFailed": "切り替えに失敗しました", + "toggleFailedError": "切り替えに失敗しました:", + "validation": { + "categoryRequired": "ルールカテゴリを選択してください", + "patternInvalid": "正規表現の構文エラー", + "patternRequired": "正規表現パターンを入力してください", + "patternTooComplex": "正規表現が複雑すぎます", + "redosRisk": "正規表現に ReDoS リスクがあります。パターンを簡略化してください" + } +} diff --git a/messages/ja/settings/errors.json b/messages/ja/settings/errors.json new file mode 100644 index 000000000..dd4d1e751 --- /dev/null +++ b/messages/ja/settings/errors.json @@ -0,0 +1,17 @@ +{ + "addFailed": "プロバイダーの追加に失敗しました", + "addSuccess": "追加に成功しました", + "deleteFailed": "プロバイダーの削除に失敗しました", + "deleteSuccess": "削除に成功しました", + "editFailed": "プロバイダーの更新に失敗しました", + "editSuccess": "更新に成功しました", + "loadFailed": "通知設定の読み込みに失敗しました", + "saveFailed": "保存に失敗しました", + "saveFailed_error": "設定の保存に失敗しました", + "saveSuccess": "保存に成功しました", + "syncFailed": "同期に失敗しました", + "syncSuccess": "同期に成功しました", + "testFailed": "テストに失敗しました", + "testFailedRetry": "テストに失敗しました。再試行してください", + "unknownError": "操作中に例外が発生しました" +} diff --git a/messages/ja/settings/index.ts b/messages/ja/settings/index.ts new file mode 100644 index 000000000..414b5daa4 --- /dev/null +++ b/messages/ja/settings/index.ts @@ -0,0 +1,101 @@ +import clientVersions from "./clientVersions.json"; +import common from "./common.json"; +import config from "./config.json"; +import data from "./data.json"; +import errorRules from "./errorRules.json"; +import errors from "./errors.json"; +import logs from "./logs.json"; +import nav from "./nav.json"; +import notifications from "./notifications.json"; +import prices from "./prices.json"; +import requestFilters from "./requestFilters.json"; +import sensitiveWords from "./sensitiveWords.json"; +import strings from "./strings.json"; + +import providersAutoSort from "./providers/autoSort.json"; +import providersFilter from "./providers/filter.json"; +import providersGuide from "./providers/guide.json"; +import providersInlineEdit from "./providers/inlineEdit.json"; +import providersList from "./providers/list.json"; +import providersSchedulingDialog from "./providers/schedulingDialog.json"; +import providersSearch from "./providers/search.json"; +import providersSection from "./providers/section.json"; +import providersSort from "./providers/sort.json"; +import providersStrings from "./providers/strings.json"; +import providersTypes from "./providers/types.json"; + +import providersFormApiTest from "./providers/form/apiTest.json"; +import providersFormButtons from "./providers/form/buttons.json"; +import providersFormCommon from "./providers/form/common.json"; +import providersFormDeleteDialog from "./providers/form/deleteDialog.json"; +import providersFormErrors from "./providers/form/errors.json"; +import providersFormFailureThresholdConfirmDialog from "./providers/form/failureThresholdConfirmDialog.json"; +import providersFormKey from "./providers/form/key.json"; +import providersFormMaxRetryAttempts from "./providers/form/maxRetryAttempts.json"; +import providersFormModelRedirect from "./providers/form/modelRedirect.json"; +import providersFormModelSelect from "./providers/form/modelSelect.json"; +import providersFormName from "./providers/form/name.json"; +import providersFormProviderTypes from "./providers/form/providerTypes.json"; +import providersFormProxyTest from "./providers/form/proxyTest.json"; +import providersFormSections from "./providers/form/sections.json"; +import providersFormStrings from "./providers/form/strings.json"; +import providersFormSuccess from "./providers/form/success.json"; +import providersFormTitle from "./providers/form/title.json"; +import providersFormUrl from "./providers/form/url.json"; +import providersFormUrlPreview from "./providers/form/urlPreview.json"; +import providersFormWebsiteUrl from "./providers/form/websiteUrl.json"; + +const providersForm = { + ...providersFormStrings, + apiTest: providersFormApiTest, + buttons: providersFormButtons, + common: providersFormCommon, + deleteDialog: providersFormDeleteDialog, + errors: providersFormErrors, + failureThresholdConfirmDialog: providersFormFailureThresholdConfirmDialog, + key: providersFormKey, + maxRetryAttempts: providersFormMaxRetryAttempts, + modelRedirect: providersFormModelRedirect, + modelSelect: providersFormModelSelect, + name: providersFormName, + providerTypes: providersFormProviderTypes, + proxyTest: providersFormProxyTest, + sections: providersFormSections, + success: providersFormSuccess, + title: providersFormTitle, + url: providersFormUrl, + urlPreview: providersFormUrlPreview, + websiteUrl: providersFormWebsiteUrl, +}; + +const providers = { + ...providersStrings, + autoSort: providersAutoSort, + filter: providersFilter, + form: providersForm, + guide: providersGuide, + inlineEdit: providersInlineEdit, + list: providersList, + schedulingDialog: providersSchedulingDialog, + search: providersSearch, + section: providersSection, + sort: providersSort, + types: providersTypes, +}; + +export default { + nav, + common, + config, + providers, + prices, + sensitiveWords, + requestFilters, + logs, + data, + clientVersions, + notifications, + errors, + errorRules, + ...strings, +}; diff --git a/messages/ja/settings/logs.json b/messages/ja/settings/logs.json new file mode 100644 index 000000000..4392c209c --- /dev/null +++ b/messages/ja/settings/logs.json @@ -0,0 +1,54 @@ +{ + "description": "システムログレベルを動的に調整してロギング詳細度をリアルタイムで制御します。", + "form": { + "changeNotice": "現在のレベルは {current} です、保存後 {selected} に切り替わります", + "currentLevel": "現在のログレベル", + "effectiveImmediately": "ログレベルの変更はすぐに有効になります、サービス再起動不要。", + "failed": "設定に失敗しました", + "failedError": "ログレベルの設定に失敗しました", + "fetchFailed": "ログレベルの取得に失敗しました", + "levelGuideDebug": "Debug(開発環境推奨): 詳細なデバッグ情報を含む、トラブルシューティングに適しています", + "levelGuideFatal": "Fatal/Error: エラーのみ表示、最小限のログ、高負荷本番環境に適しています", + "levelGuideInfo": "Info(本番環境推奨): 主要なビジネスイベント(プロバイダー選択、セッション再利用、価格同期)+ 警告 + エラーを表示", + "levelGuideTitle": "ログレベルガイド", + "levelGuideTrace": "Trace: 非常に詳細なトレース情報、すべての詳細を含む", + "levelGuideWarn": "Warn: 警告(レート制限、サーキットブレーカー開放など)+ エラーを含む", + "save": "設定を保存", + "saving": "保存しています...", + "selectLevel": "ログレベルを選択", + "success": "ログレベルを設定しました: {level}" + }, + "levels": { + "debug": { + "description": "デバッグ情報 + 全レベル(開発環境推奨)", + "label": "Debug" + }, + "error": { + "description": "エラーメッセージ", + "label": "Error" + }, + "fatal": { + "description": "致命的エラーのみ", + "label": "Fatal" + }, + "info": { + "description": "主要なビジネスイベント + 警告 + エラー(本番環境推奨)", + "label": "Info" + }, + "trace": { + "description": "非常に詳細なトレース + 全レベル", + "label": "Trace" + }, + "warn": { + "description": "警告 + エラー", + "label": "Warn" + } + }, + "section": { + "description": "変更はすぐに有効になります、サービス再起動不要。", + "title": "ログレベルコントロール" + }, + "subtitle": "ログレベルコントロール", + "subtitleDesc": "変更はすぐに有効になります、再起動不要。本番環境でのトラブルシューティングに便利です。", + "title": "ログ管理" +} diff --git a/messages/ja/settings/nav.json b/messages/ja/settings/nav.json new file mode 100644 index 000000000..4ee6a154d --- /dev/null +++ b/messages/ja/settings/nav.json @@ -0,0 +1,15 @@ +{ + "apiDocs": "API文書", + "clientVersions": "更新通知", + "config": "設定", + "data": "データ", + "docs": "ドキュメント", + "errorRules": "エラー", + "feedback": "報告", + "logs": "ログ", + "notifications": "通知", + "prices": "価格表", + "providers": "供給元", + "requestFilters": "リクエスト", + "sensitiveWords": "フィルター" +} diff --git a/messages/ja/settings/notifications.json b/messages/ja/settings/notifications.json new file mode 100644 index 000000000..3827a52e7 --- /dev/null +++ b/messages/ja/settings/notifications.json @@ -0,0 +1,146 @@ +{ + "bindings": { + "advanced": "詳細", + "bindTarget": "プッシュ先をバインド", + "boundCount": "バインド: {count}", + "editTemplateOverride": "上書きを編集", + "enable": "有効", + "enableType": "この通知を有効化", + "enabledCount": "有効: {count}", + "noTargets": "プッシュ先がありません", + "scheduleCron": "Cron", + "scheduleCronPlaceholder": "例: 0 9 * * *", + "scheduleTimezone": "タイムゾーン", + "templateOverride": "テンプレート上書き", + "templateOverrideTitle": "テンプレート上書きを編集", + "title": "バインド" + }, + "circuitBreaker": { + "description": "プロバイダーが完全に遮断された時に即座にアラートを送信", + "enable": "サーキットブレーカーアラートを有効にする", + "test": "接続テスト", + "title": "サーキットブレーカーアラート", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." + }, + "costAlert": { + "description": "ユーザー/プロバイダーの消費がクォータしきい値を超えた時にアラートをトリガー", + "enable": "コストアラートを有効にする", + "interval": "チェック間隔(分)", + "test": "接続テスト", + "threshold": "アラートしきい値", + "thresholdHelp": "消費がクォータの{percent}%に達した時にアラート", + "thresholdLabel": "アラートしきい値: {percent}%", + "title": "コストアラート", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeFeishu": "Feishu", + "webhookTypeUnknown": "不明なプラットフォームです。WeComまたはFeishuのWebhook URLを使用してください", + "webhookTypeWeCom": "WeCom" + }, + "dailyLeaderboard": { + "description": "毎日定時でユーザー消費トップNランキングを送信", + "enable": "日次ランキングを有効にする", + "test": "接続テスト", + "time": "送信時刻", + "timeError": "時刻形式エラー、HH:mm形式である必要があります", + "timePlaceholder": "09:00", + "title": "日次ユーザー消費ランキング", + "topN": "トップN件表示", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." + }, + "description": "Webhook プッシュ通知を設定", + "form": { + "loadError": "通知設定の読み込みに失敗しました", + "loading": "読み込み中...", + "save": "設定を保存", + "saveError": "設定の保存に失敗しました", + "saveFailed": "保存に失敗しました", + "saving": "保存しています...", + "success": "通知設定を保存し、タスクを再スケジュールしました", + "testError": "接続テストに失敗しました", + "testFailed": "テストに失敗しました", + "testFailedRetry": "テストに失敗しました。再試行してください", + "testNoResult": "テストは成功しましたが、結果が返されませんでした", + "testSuccess": "テストメッセージを送信しました", + "webhookRequired": "まずWebhook URLを入力してください" + }, + "global": { + "description": "すべてのプッシュ通知機能を有効または無効にする", + "enable": "プッシュ通知を有効にする", + "legacyModeDescription": "現在は旧来の単一URL通知設定を使用しています。プッシュ先を作成するとマルチターゲットモードに切り替わります。", + "legacyModeTitle": "互換モード", + "title": "通知マスタースイッチ" + }, + "targetDialog": { + "createTitle": "プッシュ先を追加", + "customHeaders": "カスタムヘッダー(JSON)", + "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", + "dingtalkSecret": "DingTalk シークレット", + "dingtalkSecretPlaceholder": "任意(署名用)", + "editTitle": "プッシュ先を編集", + "enable": "有効化", + "errors": { + "headersInvalidJson": "Headers は有効な JSON である必要があります", + "headersMustBeObject": "Headers は JSON オブジェクトである必要があります", + "headersValueMustBeString": "Headers の値は文字列である必要があります" + }, + "name": "名前", + "namePlaceholder": "例: Ops グループ", + "proxy": { + "fallbackToDirect": "プロキシ失敗時に直結へフォールバック", + "title": "プロキシ", + "toggle": "プロキシ設定を切り替え", + "url": "プロキシURL", + "urlPlaceholder": "http://127.0.0.1:7890" + }, + "selectType": "プラットフォームを選択", + "telegramBotToken": "Telegram Bot Token", + "telegramBotTokenPlaceholder": "例: 123456:ABCDEF...", + "telegramChatId": "Telegram Chat ID", + "telegramChatIdPlaceholder": "例: -1001234567890", + "type": "プラットフォーム", + "types": { + "custom": "カスタムWebhook", + "dingtalk": "DingTalk", + "feishu": "Feishu", + "telegram": "Telegram", + "wechat": "WeCom" + }, + "webhookUrl": "Webhook URL", + "webhookUrlPlaceholder": "https://example.com/webhook" + }, + "targets": { + "add": "追加", + "bindingsSaved": "バインドを保存しました", + "created": "プッシュ先を作成しました", + "delete": "削除", + "deleteConfirm": "このプッシュ先を削除しますか?関連するバインドも削除されます。", + "deleteConfirmTitle": "プッシュ先を削除", + "deleted": "プッシュ先を削除しました", + "description": "プッシュ先を管理します。WeCom、Feishu、DingTalk、Telegram、カスタムWebhookに対応。", + "edit": "編集", + "emptyHint": "プッシュ先がありません。「追加」で作成してください。", + "enable": "有効化", + "lastTestAt": "最終テスト", + "lastTestFailed": "テスト失敗", + "lastTestNever": "未テスト", + "lastTestSuccess": "テスト成功", + "statusDisabled": "無効", + "statusEnabled": "有効", + "test": "テスト", + "testSelectType": "テスト種類を選択", + "title": "プッシュ先", + "update": "保存", + "updated": "プッシュ先を更新しました" + }, + "templateEditor": { + "insert": "挿入", + "jsonInvalid": "JSON が不正です", + "placeholder": "JSON テンプレートを入力...", + "placeholders": "プレースホルダー", + "title": "テンプレート(JSON)" + }, + "title": "プッシュ通知" +} diff --git a/messages/ja/settings/prices.json b/messages/ja/settings/prices.json new file mode 100644 index 000000000..4a86b16c9 --- /dev/null +++ b/messages/ja/settings/prices.json @@ -0,0 +1,191 @@ +{ + "title": "価格表", + "description": "プラットフォーム基本設定とモデル価格を管理します", + "section": { + "title": "モデル価格", + "description": "AIモデルの価格設定を管理します" + }, + "searchPlaceholder": "モデル名を検索...", + "filters": { + "all": "すべて", + "local": "ローカル", + "anthropic": "Anthropic", + "openai": "OpenAI", + "vertex": "Vertex" + }, + "badges": { + "local": "ローカル" + }, + "capabilities": { + "assistantPrefill": "アシスタント事前入力", + "computerUse": "コンピューター利用", + "functionCalling": "関数呼び出し", + "pdfInput": "PDF入力", + "promptCaching": "プロンプトキャッシュ", + "reasoning": "推論", + "responseSchema": "レスポンススキーマ", + "toolChoice": "ツール選択", + "vision": "ビジョン", + "statusSupported": "対応", + "statusUnsupported": "未対応", + "tooltip": "{label}: {status}" + }, + "sync": { + "button": "クラウド価格表を同期", + "syncing": "同期中...", + "checking": "競合を確認中...", + "successWithChanges": "価格表を更新: {added}件追加、{updated}件更新、{unchanged}件変化なし", + "successNoChanges": "価格表は最新です。更新の必要はありません", + "failed": "同期に失敗しました", + "failedError": "同期に失敗しました: {error}", + "failedNoResult": "価格表は更新されましたが結果が返されていません", + "noModels": "モデル価格が見つかりません", + "partialFailure": "一部更新が成功しましたが、{failed}件のモデルが失敗しました", + "failedModels": "失敗モデル: {models}", + "skippedConflicts": "{count}件の手動モデルをスキップしました" + }, + "conflict": { + "title": "上書きする項目を選択", + "description": "以下のモデルには手動で設定された価格があります。チェックした項目はLiteLLM価格で上書きされ、チェックしない項目は現在のままです", + "searchPlaceholder": "モデルを検索...", + "table": { + "modelName": "モデル", + "manualPrice": "手動価格", + "litellmPrice": "LiteLLM価格", + "action": "操作内容" + }, + "viewDiff": "差異を表示", + "diffTitle": "価格差異", + "diff": { + "field": "フィールド", + "manual": "手動", + "litellm": "LiteLLM", + "inputPrice": "入力価格", + "outputPrice": "出力価格", + "imagePrice": "画像価格", + "provider": "プロバイダー", + "mode": "タイプ" + }, + "pagination": { + "showing": "{from}〜{to}件を表示 (全{total}件)" + }, + "selectedCount": "{count}/{total}件のモデルを選択", + "noMatch": "一致するモデルが見つかりません", + "noConflicts": "競合なし", + "applyOverwrite": "上書きを適用", + "applying": "適用中..." + }, + "table": { + "modelName": "モデル名", + "provider": "プロバイダー", + "capabilities": "機能", + "price": "価格", + "inputPrice": "入力価格 ($/M)", + "outputPrice": "出力価格 ($/M)", + "priceInput": "入力", + "priceOutput": "出力", + "pricePerRequest": "回", + "cacheReadPrice": "キャッシュ読み取り ($/M)", + "cacheCreationPrice": "キャッシュ作成 ($/M)", + "cache5m": "5m", + "cache1h": "1h+", + "copyModelId": "モデルIDをコピー", + "updatedAt": "更新日時", + "actions": "操作内容", + "typeChat": "チャット", + "typeImage": "画像生成", + "typeCompletion": "補完", + "typeUnknown": "不明", + "loading": "読み込み中...", + "noMatch": "一致するモデルが見つかりません", + "noDataTitle": "価格データがありません", + "noDataHint": "システムは組み込み価格表を持っています。上のボタンを使用して同期または更新してください。" + }, + "pagination": { + "showing": "{from}〜{to}件を表示 (全{total}件)", + "previous": "前へ", + "next": "次へ", + "perPageLabel": "1ページあたり", + "perPage": "1ページあたり{size}件" + }, + "stats": { + "totalModels": "合計{count}個のモデル", + "searchResults": "{count}件の検索結果", + "lastUpdated": "最終更新: {time}" + }, + "dialog": { + "title": "モデル価格表を更新", + "description": "モデル価格データを含むJSONまたはTOMLファイルを選択してアップロード", + "selectFile": "JSON/TOMLファイルをクリックして選択、またはドラッグしてください", + "fileSizeLimit": "ファイルサイズは10MBを超えることはできません", + "fileSizeLimitSmall": "ファイルサイズは10MB以下です", + "invalidFileType": "JSONまたはTOML形式のファイルを選択してください", + "fileTooLarge": "ファイルサイズが10MBを超えています", + "upload": "アップロードして更新", + "uploading": "アップロード中...", + "updatePriceTable": "価格表を更新", + "updating": "更新しています...", + "selectJson": "ファイルを選択", + "updateSuccess": "価格表が正常に更新されました。{count}個のモデルを更新しました", + "updateFailed": "更新に失敗しました", + "systemHasBuiltIn": "システムは組み込み価格表を持っています", + "manualDownload": "手動でダウンロードすることもできます", + "latestPriceTable": "クラウド価格表", + "andUploadViaButton": "、上のボタンでアップロードしてください", + "cloudModelCountLoading": "クラウドモデル数を読み込み中...", + "cloudModelCountFailed": "クラウドモデル数の読み込みに失敗しました", + "supportedModels": "現在{count}個のモデルをサポート", + "results": { + "title": "更新結果", + "total": "合計: {total}個のモデル", + "success": "成功: {success}件", + "failed": "失敗: {failed}", + "skipped": "スキップ: {skipped}", + "more": " (+{count})", + "details": "詳細", + "viewDetails": "詳細ログを表示" + } + }, + "addModel": "モデルを追加", + "editModel": "モデルを編集", + "deleteModel": "モデルを削除", + "addModelDescription": "新しいモデル価格設定を手動で追加します", + "editModelDescription": "モデルの価格設定を編集します", + "deleteConfirm": "モデル {name} を削除してもよろしいですか?この操作は元に戻せません。", + "form": { + "modelName": "モデルID", + "modelNamePlaceholder": "例: gpt-5.2-codex", + "modelNameRequired": "モデルIDは必須です", + "displayName": "表示名 (任意)", + "displayNamePlaceholder": "例: GPT-5.2 Codex", + "type": "タイプ", + "provider": "プロバイダー", + "providerPlaceholder": "例: openai", + "requestPrice": "呼び出し単価 ($/request)", + "inputPrice": "入力価格 ($/M tokens)", + "outputPrice": "出力価格 ($/M tokens)", + "outputPriceImage": "出力価格 ($/image)", + "cacheReadPrice": "キャッシュ読み取り価格 ($/M tokens)", + "cacheCreationPrice5m": "キャッシュ作成価格 (5m,$/M tokens)", + "cacheCreationPrice1h": "キャッシュ作成価格 (1h+,$/M tokens)" + }, + "drawer": { + "prefillLabel": "既存モデルを検索してプリフィル", + "prefillEmpty": "一致するモデルが見つかりません", + "prefillFailed": "検索に失敗しました", + "promptCachingHint": "モデルがキャッシュに対応している場合のみ有効化し、下のキャッシュ価格を設定してください", + "cachePricingTitle": "キャッシュ価格" + }, + "actions": { + "edit": "編集", + "more": "その他の操作", + "delete": "削除" + }, + "toast": { + "createSuccess": "モデルを追加しました", + "updateSuccess": "モデルを更新しました", + "deleteSuccess": "モデルを削除しました", + "saveFailed": "保存に失敗しました", + "deleteFailed": "削除に失敗しました" + } +} diff --git a/messages/ja/settings/providers/autoSort.json b/messages/ja/settings/providers/autoSort.json new file mode 100644 index 000000000..c2b962f2f --- /dev/null +++ b/messages/ja/settings/providers/autoSort.json @@ -0,0 +1,16 @@ +{ + "button": "優先度を自動ソート", + "changeCount": "{count} 件のプロバイダーが更新されます", + "changesTitle": "変更詳細", + "confirm": "変更を適用", + "costMultiplierHeader": "コスト倍率", + "dialogDescription": "コスト倍率に基づいて優先度を自動割り当て(低コスト = 高優先度)", + "dialogTitle": "プロバイダー優先度の自動ソート", + "error": "優先度の更新に失敗しました", + "noChanges": "変更不要(ソート済み)", + "priorityChangeHeader": "優先度変更", + "priorityHeader": "優先度", + "providerHeader": "プロバイダー", + "providersHeader": "プロバイダー", + "success": "{count} 件のプロバイダーの優先度を更新しました" +} diff --git a/messages/ja/settings/providers/filter.json b/messages/ja/settings/providers/filter.json new file mode 100644 index 000000000..0e99db282 --- /dev/null +++ b/messages/ja/settings/providers/filter.json @@ -0,0 +1,13 @@ +{ + "circuitBroken": "サーキットブレーカー", + "groups": { + "all": "すべて", + "default": "default", + "label": "グループ:" + }, + "status": { + "active": "有効", + "all": "すべてのステータス", + "inactive": "無効" + } +} diff --git a/messages/ja/settings/providers/form/apiTest.json b/messages/ja/settings/providers/form/apiTest.json new file mode 100644 index 000000000..b31f0b463 --- /dev/null +++ b/messages/ja/settings/providers/form/apiTest.json @@ -0,0 +1,157 @@ +{ + "apiFormat": "プロバイダータイプ", + "apiFormatDesc": "手動で変更しない限り、ルーティング設定のプロバイダータイプと同期", + "chunksCount": "{count} チャンク受信 ({format})", + "chunksReceived": "受信したチャンク", + "close": "閉じる", + "copyFailed": "コピー失敗", + "copyFormat": { + "errorDetails": "エラー詳細", + "message": "メッセージ", + "testResult": "テスト結果" + }, + "copyResult": "結果をコピー", + "copySuccess": "クリップボードにコピーしました", + "customConfig": "カスタム", + "customPayloadDesc": "カスタムJSONペイロードを入力してデフォルトのリクエストボディを上書き", + "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", + "disclaimer": { + "confirmConfig": "プロバイダーURL、APIキー、モデル設定を確認してください", + "realRequest": "テストはプロバイダーに実際のリクエストを送信し、少量のクォータを消費する可能性があります", + "resultReference": "プロバイダーによって結果が異なる場合があり、参考用です", + "title": "注意事項" + }, + "error": "エラーメッセージ", + "failed": "失敗", + "fillKeyFirst": "まずAPIキーを入力してください", + "fillUrlFirst": "まずプロバイダーURLを入力してください", + "formatAnthropicMessages": "Claude (Anthropic Messages API)", + "formatOpenAIChat": "OpenAI Compatible", + "formatOpenAIResponses": "Codex (Response API)", + "geminiAuthFallback": { + "desc": "実際のプロキシ転送はヘッダー認証のみを使用するため、リクエストが失敗する可能性があります", + "warning": "ヘッダー認証に失敗し、URLパラメータ認証を使用しました" + }, + "invalidUrl": "プロバイダーURLが無効です(http/httpsのみ対応)", + "model": "モデル", + "noResult": "テスト成功ですが結果が返されませんでした", + "presetConfig": "プリセット", + "presetDesc": "プリセットテンプレートには、リレーサービス検証用の本物のCLIリクエストパターンが含まれています", + "requestConfig": "リクエスト設定", + "response": "応答内容", + "responseModel": "応答モデル", + "responseTime": "応答時間", + "resultCard": { + "copyText": { + "contentCheck": "コンテンツ検証", + "error": "エラー", + "httpCheck": "HTTPチェック", + "httpStatus": "HTTPステータス", + "inputOutput": "入力 {input} / 出力 {output} トークン", + "latency": "レイテンシ", + "latencyCheck": "レイテンシチェック", + "message": "メッセージ", + "model": "モデル", + "response": "応答", + "status": "ステータス", + "testedAt": "テスト日時", + "usage": "使用量", + "validationDetails": "検証詳細" + }, + "dialogTitle": "プロバイダーテスト詳細", + "errorDetails": { + "title": "エラー詳細", + "type": "エラータイプ" + }, + "judgment": "判定結果", + "labels": { + "content": "コンテンツ", + "error": "エラー", + "firstByte": "最初のバイト", + "http": "HTTP", + "latency": "レイテンシ", + "model": "モデル", + "responsePreview": "応答プレビュー", + "totalLatency": "合計レイテンシ" + }, + "rawResponse": { + "hint": "ここに生のレスポンス内容が表示されます。キーワードがレスポンスに含まれているか確認できます。", + "title": "生のレスポンスボディ" + }, + "status": { + "green": "利用可能", + "red": "利用不可", + "yellow": "不安定" + }, + "streamInfo": { + "chunksCount": "チャンク数", + "isStreaming": "ストリーミング", + "no": "いいえ", + "title": "ストリーム応答情報", + "yes": "はい" + }, + "timing": { + "firstByte": "最初のバイト", + "testedAt": "テスト日時", + "title": "タイミング情報", + "totalLatency": "合計レイテンシ" + }, + "tokenUsage": { + "cacheCreation": "キャッシュ作成", + "cacheRead": "キャッシュ読取", + "input": "入力", + "output": "出力", + "title": "トークン使用量" + }, + "validation": { + "content": { + "failed": "ターゲットが見つかりません", + "passed": "ターゲット文字列を含む", + "target": "ターゲット", + "title": "Tier 3: コンテンツ検証" + }, + "failed": "失敗", + "http": { + "failed": "4xx/5xx 失敗", + "passed": "2xx/3xx OK", + "statusCode": "ステータスコード", + "title": "Tier 1: HTTPステータス" + }, + "latency": { + "actual": "実際のレイテンシ", + "failed": "閾値超過", + "passed": "閾値内", + "title": "Tier 2: レイテンシ閾値" + }, + "passed": "合格", + "timeout": "タイムアウト", + "title": "三層検証詳細" + } + }, + "selectApiFormat": "テストするプロバイダータイプを選択", + "selectPreset": "プリセットテンプレートを選択", + "streamFormat": "ストリーム形式", + "streamInfo": "ストリーム応答情報", + "streamResponse": "ストリーム応答", + "success": "成功しました", + "successContains": "成功検出キーワード", + "successContainsDesc": "成功と見なすには、レスポンスにこのキーワードが含まれている必要があります", + "successContainsPlaceholder": "pong", + "testApi": "プロバイダーモデルテスト", + "testFailed": "テスト失敗", + "testFailedRetry": "テスト失敗、再試行してください", + "testModel": "テストモデル", + "testModelDesc": "空欄の場合はデフォルトモデルを使用、手動入力も可能", + "testSuccess": "モデルテスト成功", + "testing": "テスト中...", + "timeout": { + "desc": "テストリクエストの最大待機時間(5〜120秒)", + "geminiHint": "、Gemini Thinkingモデルは60秒以上を推奨", + "label": "タイムアウト(秒)" + }, + "truncatedBrief": "先頭 {length} 文字を表示、全文は「詳細を見る」をクリック", + "truncatedPreview": "先頭 {length} 文字を表示、全文はコピーして確認", + "unknown": "不明", + "usage": "トークン使用量", + "viewDetails": "詳細を見る" +} diff --git a/messages/ja/settings/providers/form/buttons.json b/messages/ja/settings/providers/form/buttons.json new file mode 100644 index 000000000..7d0a15888 --- /dev/null +++ b/messages/ja/settings/providers/form/buttons.json @@ -0,0 +1,9 @@ +{ + "collapseAll": "高度な設定をすべて折りたたむ", + "delete": "削除", + "expandAll": "高度な設定をすべて展開", + "submit": "追加を確定", + "submitting": "追加中...", + "update": "更新を確定", + "updating": "更新しています..." +} diff --git a/messages/ja/settings/providers/form/common.json b/messages/ja/settings/providers/form/common.json new file mode 100644 index 000000000..0eefe2718 --- /dev/null +++ b/messages/ja/settings/providers/form/common.json @@ -0,0 +1,3 @@ +{ + "core": "コア" +} diff --git a/messages/ja/settings/providers/form/deleteDialog.json b/messages/ja/settings/providers/form/deleteDialog.json new file mode 100644 index 000000000..b33efe318 --- /dev/null +++ b/messages/ja/settings/providers/form/deleteDialog.json @@ -0,0 +1,6 @@ +{ + "cancel": "キャンセル", + "confirm": "削除を確定", + "description": "プロバイダー「{name}」を削除しますか?この操作は元に戻せません。", + "title": "プロバイダーを削除" +} diff --git a/messages/ja/settings/providers/form/errors.json b/messages/ja/settings/providers/form/errors.json new file mode 100644 index 000000000..50c85bcfe --- /dev/null +++ b/messages/ja/settings/providers/form/errors.json @@ -0,0 +1,8 @@ +{ + "addFailed": "プロバイダーの追加に失敗しました", + "deleteFailed": "プロバイダーの削除に失敗しました", + "groupTagTooLong": "プロバイダーグループが長すぎます(合計{max}文字まで)", + "invalidUrl": "有効な API アドレスを入力してください", + "invalidWebsiteUrl": "有効な公式サイト URL を入力してください", + "updateFailed": "プロバイダーの更新に失敗しました" +} diff --git a/messages/ja/settings/providers/form/failureThresholdConfirmDialog.json b/messages/ja/settings/providers/form/failureThresholdConfirmDialog.json new file mode 100644 index 000000000..83a7bf901 --- /dev/null +++ b/messages/ja/settings/providers/form/failureThresholdConfirmDialog.json @@ -0,0 +1,13 @@ +{ + "cancel": "キャンセル", + "confirm": "保存を確定", + "confirmQuestion": "この設定を保存してもよろしいですか?", + "descriptionDisabledAction": "サーキットブレーカーを無効化", + "descriptionDisabledMiddle": "に設定しています。これは", + "descriptionDisabledPrefix": "サーキットブレーカーの失敗閾値を", + "descriptionDisabledSuffix": "することを意味し、プロバイダーは連続した失敗によって遮断されません。", + "descriptionDisabledValue": "0", + "descriptionHighValuePrefix": "サーキットブレーカーの失敗閾値を", + "descriptionHighValueSuffix": "に設定しています。これは高い値であり、プロバイダーが多数の失敗の後にのみ遮断される可能性があります。", + "title": "特別な設定を確認" +} diff --git a/messages/ja/settings/providers/form/key.json b/messages/ja/settings/providers/form/key.json new file mode 100644 index 000000000..1ad44a060 --- /dev/null +++ b/messages/ja/settings/providers/form/key.json @@ -0,0 +1,7 @@ +{ + "currentKey": "現在のキー: {key}", + "label": "API キー", + "leaveEmpty": "(空欄のままにすると変更しません)", + "leaveEmptyDesc": "空欄のままにすると既存のキーを保持します", + "placeholder": "API キーを入力" +} diff --git a/messages/ja/settings/providers/form/maxRetryAttempts.json b/messages/ja/settings/providers/form/maxRetryAttempts.json new file mode 100644 index 000000000..53cf0cd90 --- /dev/null +++ b/messages/ja/settings/providers/form/maxRetryAttempts.json @@ -0,0 +1,5 @@ +{ + "desc": "初回呼び出しを含め、同一プロバイダーで最大何回まで試行してから切り替えるか。空欄の場合はシステム既定値を使用します。", + "label": "単一プロバイダーの最大試行回数", + "placeholder": "2" +} diff --git a/messages/ja/settings/providers/form/modelRedirect.json b/messages/ja/settings/providers/form/modelRedirect.json new file mode 100644 index 000000000..cd55a47ec --- /dev/null +++ b/messages/ja/settings/providers/form/modelRedirect.json @@ -0,0 +1,14 @@ +{ + "add": "追加", + "addNewRule": "新規ルールを追加", + "alreadyExists": "モデル \"{model}\" のリダイレクトルールは既に存在します", + "currentRules": "現在のルール ({count})", + "description": "Claude Code クライアントがリクエストするモデル(例:claude-sonnet-4.5)を、上流プロバイダーが実際にサポートするモデル(例:glm-4.6、gemini-pro)にリダイレクトします。コスト最適化やサードパーティAIサービスへの接続に使用します。", + "emptyState": "リダイレクトルールがありません。ルールを追加すると、システムは自動的にリクエスト内のモデル名を書き換えます。", + "sourceEmpty": "ソースモデル名を入力してください", + "sourceModel": "ユーザーがリクエストするモデル", + "sourcePlaceholder": "例:claude-sonnet-4-5-20250929", + "targetEmpty": "ターゲットモデル名を入力してください", + "targetModel": "実際に転送されるモデル", + "targetPlaceholder": "例:glm-4.6" +} diff --git a/messages/ja/settings/providers/form/modelSelect.json b/messages/ja/settings/providers/form/modelSelect.json new file mode 100644 index 000000000..501307f22 --- /dev/null +++ b/messages/ja/settings/providers/form/modelSelect.json @@ -0,0 +1,20 @@ +{ + "allowAllModels": "すべての {type} モデルを許可", + "claude": "Claude", + "clear": "クリア", + "gemini": "Gemini", + "loading": "読み込み中...", + "manualAdd": "手動でモデルを追加", + "manualDesc": "任意のモデル名を追加できます(価格表のモデルに限定されません)", + "manualPlaceholder": "モデル名を入力(例:gpt-5-turbo)", + "notFound": "モデルが見つかりません", + "openai": "OpenAI", + "refresh": "モデルリストを更新", + "searchPlaceholder": "モデル名を検索...", + "selectAll": "すべて選択 ({count})", + "selectedCount": "{count} 個のモデルを選択済み", + "sourceFallback": "ローカル", + "sourceFallbackDesc": "ローカル価格表のモデルリストを使用(上流が利用不可または未対応)", + "sourceUpstream": "上流", + "sourceUpstreamDesc": "モデルリストは上流プロバイダーAPIから取得" +} diff --git a/messages/ja/settings/providers/form/name.json b/messages/ja/settings/providers/form/name.json new file mode 100644 index 000000000..3e98216b7 --- /dev/null +++ b/messages/ja/settings/providers/form/name.json @@ -0,0 +1,4 @@ +{ + "label": "プロバイダー名 *", + "placeholder": "例: Zhipu" +} diff --git a/messages/ja/settings/providers/form/providerTypes.json b/messages/ja/settings/providers/form/providerTypes.json new file mode 100644 index 000000000..4f890b749 --- /dev/null +++ b/messages/ja/settings/providers/form/providerTypes.json @@ -0,0 +1,10 @@ +{ + "claude": "Claude (Anthropic Messages API)", + "claudeAuth": "Claude (Anthropic Auth Token)", + "codex": "Codex (Response API)", + "gemini": "Gemini (Google Gemini API)", + "geminiCli": "Gemini CLI", + "geminiCliDisabled": "", + "openaiCompatible": "OpenAI Compatible", + "openaiCompatibleDisabled": " - 開発中" +} diff --git a/messages/ja/settings/providers/form/proxyTest.json b/messages/ja/settings/providers/form/proxyTest.json new file mode 100644 index 000000000..a55e7459d --- /dev/null +++ b/messages/ja/settings/providers/form/proxyTest.json @@ -0,0 +1,21 @@ +{ + "connectionFailed": "接続失敗", + "connectionMethod": "接続方式:", + "connectionSuccess": "接続成功", + "direct": "直接接続", + "errorType": "エラータイプ:", + "fillUrlFirst": "まずプロバイダーURLを入力してください", + "networkError": "ネットワークエラー:", + "noResult": "テスト成功ですが結果が返されませんでした", + "proxy": "プロキシ", + "proxyError": "プロキシエラー:", + "responseTime": "応答時間:", + "statusCode": "ステータスコード:", + "testConnection": "接続テスト", + "testFailed": "テスト失敗", + "testFailedRetry": "テストに失敗しました。再試行してください", + "testing": "テスト中...", + "timeoutError": "接続タイムアウト(5秒)。以下を確認してください:\n1. プロキシサーバーにアクセスできるか\n2. プロキシアドレスとポートが正しいか\n3. プロキシ認証情報が正しいか", + "viaDirect": "(直接接続)", + "viaProxy": "(プロキシ経由)" +} diff --git a/messages/ja/settings/providers/form/sections.json b/messages/ja/settings/providers/form/sections.json new file mode 100644 index 000000000..40edf78d4 --- /dev/null +++ b/messages/ja/settings/providers/form/sections.json @@ -0,0 +1,313 @@ +{ + "apiTest": { + "desc": "プロバイダーのモデルが利用可能かをテストします。既定ではルーティング設定で選択したプロバイダー種別に従います。", + "summary": "プロバイダーとモデルの接続性を確認", + "testLabel": "プロバイダーモデルをテスト", + "title": "プロバイダーモデルテスト" + }, + "circuitBreaker": { + "desc": "連続失敗時に自動的にブレークして全体の品質を保護します", + "failureThreshold": { + "desc": "何回連続失敗でブレークするか", + "label": "失敗しきい値(回)", + "placeholder": "5" + }, + "maxRetryAttempts": { + "desc": "初回呼び出しを含め、同一プロバイダーで試行する上限。超えると他のプロバイダーへ切り替えます。未入力の場合はデフォルト値を使用。", + "label": "プロバイダーごとの最大試行回数", + "placeholder": "2" + }, + "openDuration": { + "desc": "ブレーク後、半開に移行するまでの時間", + "label": "ブレーク時間(分)", + "placeholder": "30" + }, + "successThreshold": { + "desc": "半開状態で何回成功したら完全回復するか", + "label": "回復しきい値(回)", + "placeholder": "2" + }, + "summary": "{failureThreshold} 回失敗 / {openDuration} 分間ブレーク / {successThreshold} 回成功で回復 / 各プロバイダー最大 {maxRetryAttempts} 回試行", + "title": "サーキットブレーカー設定" + }, + "codexStrategy": { + "desc": "Codex リクエストの instructions フィールドの扱いを制御します。上流ゲートウェイとの互換性に影響します。", + "hint": "ヒント: 88code や foxcode など一部の厳格な Codex 中継では公式 instructions が必要です。「自動」または「公式を強制」を選択してください。", + "select": { + "auto": { + "desc": "クライアントの instructions を透過し、400 エラー時は公式プロンプトで自動再試行", + "label": "自動(推奨)" + }, + "force": { + "desc": "常に公式の Codex CLI instructions を使用(約 4000+ 文字)", + "label": "公式を強制" + }, + "keep": { + "desc": "常にクライアントの instructions を透過し、自動再試行しない(緩い中継向け)", + "label": "そのまま透過" + }, + "label": "ポリシー選択", + "placeholder": "戦略を選択" + }, + "summary": { + "auto": "自動(推奨)", + "force": "公式を強制", + "keep": "そのまま透過" + }, + "title": "Codex Instructions ポリシー" + }, + "mcpPassthrough": { + "desc": "有効にすると、MCP ツール呼び出しを指定された AI プロバイダにパススルーします(例:minimax の画像認識、Web 検索)", + "hint": "ヒント: MCP パススルーにより、Claude Code クライアントは第三者の AI プロバイダー提供のツール機能(画像認識、Web 検索など)を使用できます", + "select": { + "custom": { + "desc": "カスタム MCP サービスにパススルー(予約、未実装)", + "label": "カスタム" + }, + "glm": { + "desc": "GLM MCP サービスにパススルー(画像分析、動画分析などをサポート)", + "label": "GLM" + }, + "label": "パススルータイプ", + "minimax": { + "desc": "minimax MCP サービスにパススルー(画像認識、Web 検索などをサポート)", + "label": "Minimax" + }, + "none": { + "desc": "MCP パススルーを有効にしません(デフォルト)", + "label": "無効" + }, + "placeholder": "パススルータイプを選択" + }, + "summary": { + "custom": "カスタム (予約)", + "glm": "GLM", + "minimax": "Minimax", + "none": "無効" + }, + "title": "MCP パススルー設定", + "urlAuto": "自動抽出: {url}", + "urlDesc": "MCP サービスベース URL。空のままにすると、プロバイダー URL から自動的に抽出されます", + "urlLabel": "MCP パススルー URL", + "urlPlaceholder": "https://api.minimaxi.com" + }, + "proxy": { + "desc": "プロキシを設定して接続性を改善します(HTTP、HTTPS、SOCKS4、SOCKS5)", + "fallback": { + "desc": "有効にすると、プロキシ接続が失敗した場合に直接接続を試行します", + "label": "プロキシ失敗時は直接接続にフォールバック" + }, + "summary": { + "configured": "プロキシ設定済み", + "fallback": "(フォールバック有効)", + "none": "未設定" + }, + "test": { + "desc": "設定したプロキシ経由でプロバイダー URL への接続をテストします(HEAD リクエスト、課金なし)", + "label": "接続テスト" + }, + "title": "プロキシ設定", + "url": { + "formats": "対応フォーマット:", + "label": "プロキシ URL", + "optional": "(任意)", + "placeholder": "例: http://proxy.example.com:8080 または socks5://127.0.0.1:1080" + } + }, + "rateLimit": { + "dailyResetMode": { + "desc": { + "fixed": "毎日決まった時刻にクォータをリセット", + "rolling": "初回呼び出しから24時間後にリセット" + }, + "label": "日次リセットモード", + "options": { + "fixed": "固定時刻リセット", + "rolling": "ローリング(24時間)" + } + }, + "dailyResetTime": { + "label": "日次リセット時刻 (HH:mm)" + }, + "limit5h": { + "label": "5時間の上限 (USD)", + "placeholder": "空欄で無制限" + }, + "limitConcurrent": { + "label": "同時セッション上限", + "placeholder": "0 は無制限" + }, + "limitDaily": { + "label": "1日の支出上限 (USD)", + "placeholder": "空欄は無制限" + }, + "limitMonthly": { + "label": "月の上限 (USD)", + "placeholder": "空欄で無制限" + }, + "limitTotal": { + "label": "総消費上限 (USD)", + "placeholder": "空欄で無制限" + }, + "limitWeekly": { + "label": "週の上限 (USD)", + "placeholder": "空欄で無制限" + }, + "summary": { + "concurrent": "同時: {count}", + "daily": "日: {amount}({resetTime}にリセット)", + "fiveHour": "5h: ${amount}", + "monthly": "月: ${amount}", + "none": "無制限", + "total": "総: ${amount}", + "weekly": "週: ${amount}" + }, + "title": "レート制限" + }, + "routing": { + "cacheTtl": { + "desc": "プロンプトキャッシュのTTLを強制設定。cache_controlを含むリクエストにのみ適用されます。", + "label": "キャッシュTTLオーバーライド", + "options": { + "1h": "1時間", + "5m": "5分", + "inherit": "オーバーライドしない(クライアントに従う)" + } + }, + "codexOverrides": { + "parallelToolCalls": { + "help": "並列の tool calls を許可するかどうかを制御します。「クライアントに従う」は parallel_tool_calls を変更しません。無効化すると並列度が下がる可能性があります。", + "label": "並列ツール呼び出しオーバーライド", + "options": { + "false": "強制無効", + "inherit": "オーバーライドしない(クライアントに従う)", + "true": "強制有効" + } + }, + "reasoningEffort": { + "help": "回答前にモデルが使う推論の強度(推論トークン量)を制御します。「クライアントに従う」はリクエストを変更しません。その他の値は reasoning.effort を強制します。注意: none は GPT-5.1 のみ、xhigh は GPT-5.1-Codex-Max のみ対応で、未対応モデルではエラーになります。", + "label": "推論強度オーバーライド", + "options": { + "high": "high", + "inherit": "オーバーライドしない(クライアントに従う)", + "low": "low", + "medium": "medium(デフォルト)", + "minimal": "minimal", + "none": "none(GPT-5.1 のみ)", + "xhigh": "xhigh(GPT-5.1-Codex-Max のみ)" + } + }, + "reasoningSummary": { + "help": "推論サマリーを返すかどうかを制御します。auto は簡潔、detailed は詳細です。「クライアントに従う」は reasoning.summary を変更しません。", + "label": "推論サマリーオーバーライド", + "options": { + "auto": "auto", + "detailed": "detailed", + "inherit": "オーバーライドしない(クライアントに従う)" + } + }, + "textVerbosity": { + "help": "モデル出力の冗長さを制御します。low は簡潔、high は詳細です。「クライアントに従う」は text.verbosity を変更しません。", + "label": "出力の詳細度オーバーライド", + "options": { + "high": "high", + "inherit": "オーバーライドしない(クライアントに従う)", + "low": "low", + "medium": "medium(デフォルト)" + } + } + }, + "context1m": { + "desc": "1M コンテキストウィンドウのサポートを設定します。Sonnet モデル(claude-sonnet-4-5、claude-sonnet-4)にのみ適用されます。有効時は段階的料金が適用されます。", + "label": "1M コンテキストウィンドウ", + "options": { + "disabled": "無効", + "forceEnable": "強制有効化", + "inherit": "継承(クライアントに従う)" + } + }, + "joinClaudePool": { + "desc": "有効にすると、Claude 系のプロバイダーと共に負荷分散に参加します", + "help": "claude-* へのリダイレクトがある場合のみ利用できます。ユーザーが claude-* モデルを要求した際に本プロバイダーも選択対象になります。", + "label": "Claude ルーティングプールに参加" + }, + "modelRedirects": { + "label": "モデルリダイレクト設定", + "optional": "(任意)" + }, + "modelWhitelist": { + "allowAll": "✓ すべてのモデルを許可(推奨)", + "desc": "このプロバイダーが処理できるモデルを制限します。既定では同タイプのすべてのモデルを処理できます。", + "label": "許可するモデル", + "moreModels": "+{count} 件 さらに表示", + "optional": "(任意)", + "selectedOnly": "選択した {count} 件のモデルのみ許可します。他のモデルはこのプロバイダーにルーティングされません。", + "title": "モデル許可リスト" + }, + "preserveClientIp": { + "desc": "x-forwarded-for / x-real-ip を上流に渡します(実際の IP が露出する可能性)", + "help": "プライバシー保護のためデフォルトはオフ。上流側で端末 IP が必要な場合のみ有効化してください。", + "label": "クライアント IP を転送" + }, + "providerType": { + "desc": "(スケジューリングに影響)", + "label": "プロバイダー種別", + "placeholder": "プロバイダー種別を選択" + }, + "providerTypeDesc": "プロバイダーの API 形式を選択します。", + "providerTypeDisabledNote": "注: OpenAI Compatible は開発中のため、現在は使用できません", + "scheduleParams": { + "costMultiplier": { + "desc": "コスト計算の倍率。公式=1.0、20% 安い=0.8、20% 高い=1.2(小数4桁まで)", + "label": "コスト倍率", + "placeholder": "1.0" + }, + "group": { + "desc": "グループタグ。ユーザーの providerGroup が一致する場合のみ利用可能。例: \"premium\" に設定すると providerGroup=\"premium\" のユーザーのみ対象", + "label": "プロバイダーグループ", + "placeholder": "例: premium, economy" + }, + "priority": { + "desc": "値が小さいほど優先度が高くなります(0 が最も高い)。システムは最も高い優先度のプロバイダーのみから選択します。推奨: メイン=0、予備=1、緊急=2", + "label": "優先度", + "placeholder": "0" + }, + "title": "スケジューリング設定", + "weight": { + "desc": "重み付きランダム。同一優先度内では重みが高いほど選ばれる確率が上がります。例 1:2:3 ≈ 16%:33%:50%", + "label": "重み", + "placeholder": "1" + } + }, + "summary": { + "models": "許可モデル {count} 件", + "none": "未設定", + "redirects": "リダイレクト {count} 件" + }, + "title": "ルーティング" + }, + "timeout": { + "desc": "リクエストのタイムアウト時間を設定します。0 は無効を意味します", + "disableHint": "0に設定するとタイムアウトを無効にします(カナリアロールバックシナリオのみ、非推奨)", + "nonStreamingTotal": { + "core": "true", + "desc": "非ストリーミングリクエストの総タイムアウト、範囲60~1200秒、デフォルト600秒(10分)", + "label": "非ストリーミング総タイムアウト(秒)", + "placeholder": "600" + }, + "streamingFirstByte": { + "core": "true", + "desc": "ストリーミングリクエストの初バイトタイムアウト、範囲1~120秒、デフォルト30秒", + "label": "ストリーミング初バイトタイムアウト(秒)", + "placeholder": "30" + }, + "streamingIdle": { + "core": "true", + "desc": "ストリーミングリクエストのアイドルタイムアウト、範囲60~600秒、0で無効化(途中停止防止)", + "label": "ストリーミングアイドルタイムアウト(秒)", + "placeholder": "60" + }, + "summary": "初回バイト: {streaming}s | ストリーム間隔: {idle}s | 非ストリーミング: {nonStreaming}s", + "title": "タイムアウト設定" + } +} diff --git a/messages/ja/settings/providers/form/strings.json b/messages/ja/settings/providers/form/strings.json new file mode 100644 index 000000000..0fdd8f983 --- /dev/null +++ b/messages/ja/settings/providers/form/strings.json @@ -0,0 +1,204 @@ +{ + "addRedirect": "リダイレクトを追加", + "allowAllModels": "✓ すべてのモデルを許可 (推奨)", + "apiAddress": "API アドレス", + "apiAddressPlaceholder": "例: https://open.bigmodel.cn/api/anthropic", + "apiAddressRequired": "API アドレス *", + "apiKey": "APIキー", + "apiKeyCurrent": "現在のキー:", + "apiKeyLeaveEmpty": "(空欄のまま変更しない)", + "apiKeyLeaveEmptyDesc": "空欄の場合はキーを変更しません", + "apiKeyOptional": "現在のキーを保持する場合は空のままにしてください", + "apiKeyPlaceholder": "APIキーを入力", + "apiKeyRequired": "API キー *", + "baseUrl": "ベースURL", + "baseUrlPlaceholder": "例:https://open.bigmodel.cn/api/anthropic", + "baseUrlRequired": "プロバイダーURLを入力してください", + "circuitBreakerConfig": "サーキットブレーカー設定", + "circuitBreakerConfigSummary": "{failureThreshold} 回失敗 / {openDuration} 分間ブレーク / {successThreshold} 回成功で回復 / 各プロバイダー最大 {maxRetryAttempts} 回試行", + "circuitBreakerDesc": "連続失敗時に自動的にブレークして全体の品質を保護します", + "clearSearch": "検索をクリア", + "codexInstructions": "Codexインストラクション方針", + "codexInstructionsAuto": "自動 (推奨)", + "codexInstructionsDesc": "(スケジューリング方針を決定)", + "codexInstructionsForce": "公式を強制", + "codexInstructionsKeep": "元の値を保持", + "codexStrategyAutoDesc": "クライアントの instructions を透過し、400 エラー時は公式プロンプトで自動リトライします", + "codexStrategyAutoLabel": "自動 (推奨)", + "codexStrategyConfig": "Codex Instructions 戦略", + "codexStrategyConfigAuto": "自動 (推奨)", + "codexStrategyConfigForce": "公式を強制", + "codexStrategyConfigKeep": "元の値を保持", + "codexStrategyDesc": "Codex リクエストの instructions フィールドの扱いを制御します。上流ゲートウェイとの互換性に影響します", + "codexStrategyForceDesc": "公式の Codex CLI instructions を常に使用します (約 4000+ 文字)", + "codexStrategyForceLabel": "公式を強制", + "codexStrategyHint": "ヒント: 一部の厳格な Codex ゲートウェイ (例: 88code, foxcode) では公式 instructions が必要です。\"自動\" または \"公式を強制\" を選択してください", + "codexStrategyKeepDesc": "クライアントの instructions を常に透過し、自動リトライしません (柔軟なゲートウェイ向け)", + "codexStrategyKeepLabel": "元の値を保持", + "codexStrategySelect": "戦略の選択", + "collapseAll": "高度な設定をすべて折りたたむ", + "confirmAdd": "追加を確認", + "confirmAddPending": "追加中...", + "confirmUpdate": "更新を確認", + "confirmUpdatePending": "更新しています...", + "costMultiplier": "コスト乗数", + "costMultiplierDesc": "例: A (コスト 1.0x)、C (コスト 0.8x)", + "costMultiplierLabel": "コスト乗数", + "costMultiplierPlaceholder": "1.0", + "deleteButton": "削除", + "dialogDescription": "プロバイダーの詳細と高度な設定を構成します。", + "enabled": "有効", + "expandAll": "高度な設定をすべて展開", + "failureThreshold": "失敗閾値 (回)", + "failureThresholdDesc": "連続失敗が何回でブレークを発動するか", + "failureThresholdPlaceholder": "5", + "filterAllProviders": "すべてのプロバイダー", + "filterByType": "タイプでフィルタ", + "filterProvider": "プロバイダータイプでフィルタ", + "group": "グループ", + "groupPlaceholder": "例:premium, economy", + "joinClaudePool": "Claude スケジューリングプールに参加", + "joinClaudePoolDesc": "有効にすると、このプロバイダーは Claude タイプのプロバイダーとともに負荷分散スケジューリングに参加します", + "joinClaudePoolHelp": "モデルリダイレクト設定に claude-* モデルへのマッピングが含まれる場合にのみ利用可能です。有効にすると、ユーザーが claude-* モデルを要求した際にも、このプロバイダーがスケジューリング対象になります。", + "leaveEmpty": "無制限の場合は空のままにしてください", + "limit0Means": "0は無制限を意味します", + "limit5hLabel": "5時間支出上限 (USD)", + "limitAmount5h": "5時間支出上限 (USD)", + "limitAmount5hDesc": "例:プロバイダーBが$10制限、$9.8消費済み", + "limitAmountMonthly": "月間支出上限 (USD)", + "limitAmountWeekly": "週間支出上限 (USD)", + "limitConcurrent": "同時セッション数制限", + "limitConcurrentDesc": "例:プロバイダーC制限2、現在アクティブセッション数:2", + "limitConcurrentLabel": "同時セッション上限", + "limitMonthlyLabel": "月間支出上限 (USD)", + "limitPlaceholder0": "0 は無制限", + "limitPlaceholderUnlimited": "空欄は無制限", + "limitWeeklyLabel": "週間支出上限 (USD)", + "modelRedirects": "モデルリダイレクト", + "modelRedirectsAddNew": "新しいルールを追加", + "modelRedirectsCurrentRules": "現在のルール ({count})", + "modelRedirectsDesc": "Claudeモデルリクエストを他のサポートされるモデルにリダイレクトします", + "modelRedirectsEmpty": "リダイレクトルールがまだありません。ルールを追加すると、システムがリクエスト内のモデル名を自動で書き換えます。", + "modelRedirectsExists": "モデル \"{model}\" には既にリダイレクトルールがあります", + "modelRedirectsLabel": "モデルリダイレクト設定", + "modelRedirectsOptional": "(任意)", + "modelRedirectsSourceModel": "ユーザーが要求したモデル", + "modelRedirectsSourcePlaceholder": "例: claude-sonnet-4-5-20250929", + "modelRedirectsSourceRequired": "ソースモデル名は空にできません", + "modelRedirectsTargetModel": "実際に転送されるモデル", + "modelRedirectsTargetPlaceholder": "例: glm-4.6", + "modelRedirectsTargetRequired": "ターゲットモデル名は空にできません", + "modelWhitelist": "モデルホワイトリスト", + "modelWhitelistAllowAll": "すべての {type} モデルを許可", + "modelWhitelistAllowAllClause": "すべての Claude モデルを許可", + "modelWhitelistAllowAllOpenAI": "すべての OpenAI モデルを許可", + "modelWhitelistClear": "クリア", + "modelWhitelistDesc": "このプロバイダーが処理できるモデルを制限します。既定では、同タイプのすべてのモデルを処理できます。", + "modelWhitelistLabel": "許可するモデル", + "modelWhitelistLoading": "読み込み中...", + "modelWhitelistManualAdd": "モデルを手動追加", + "modelWhitelistManualDesc": "価格表に限定せず、任意のモデル名を追加できます", + "modelWhitelistManualPlaceholder": "モデル名を入力 (例: gpt-5-turbo)", + "modelWhitelistNotFound": "モデルが見つかりません", + "modelWhitelistSearchPlaceholder": "モデル名を検索...", + "modelWhitelistSelectAll": "すべて選択 ({count})", + "modelWhitelistSelected": "{count} 件選択済み", + "modelWhitelistSelectedOnly": "選択した {count} 件のモデルのみ許可します。他のモデルへのリクエストはこのプロバイダーにルーティングされません。", + "namePlaceholder": "プロバイダー名を入力", + "openDuration": "ブレーク時間 (分)", + "openDurationDesc": "どれくらいで自動的に半開状態に移行するか", + "openDurationPlaceholder": "30", + "priority": "優先度", + "priorityDesc": "同じ優先度内では、コスト乗数の低い順でソートされます", + "priorityLabel": "優先度", + "priorityPlaceholder": "0", + "providerGroupDesc": "プロバイダーグループタグ。ユーザーの providerGroup が一致する場合のみこのプロバイダーを利用できます。例: \"premium\" に設定すると providerGroup=\"premium\" のユーザーのみ利用可能です", + "providerGroupLabel": "プロバイダーグループ", + "providerGroupPlaceholder": "例: premium, economy", + "providerName": "プロバイダー名", + "providerNamePlaceholder": "例: 智譜", + "providerNameRequired": "プロバイダー名 *", + "providerType": "プロバイダータイプ", + "providerTypeDesc": "プロバイダーの API フォーマット種別を選択します。", + "providerTypeDisabledNote": "注: Gemini CLI と OpenAI Compatible タイプは開発中のため、現在は利用できません", + "proxy": "プロキシ", + "proxyAddressFormats": "対応形式:", + "proxyAddressLabel": "プロキシアドレス", + "proxyAddressOptional": "(任意)", + "proxyAddressPlaceholder": "例: http://proxy.example.com:8080 または socks5://127.0.0.1:1080", + "proxyConfig": "プロキシ設定", + "proxyConfigDesc": "プロバイダーへの接続性を向上させるためにプロキシサーバーを設定します (HTTP、HTTPS、SOCKS4、SOCKS5 に対応)", + "proxyConfigNone": "未設定", + "proxyConfigSummary": "プロキシ設定済み", + "proxyConfigSummaryFallback": " (フォールバック有効)", + "proxyConfigured": "プロキシが設定されています", + "proxyFallback": "プロキシ失敗時のフォールバック", + "proxyFallbackDesc": "プロキシ失敗時に直接接続にフォールバックします", + "proxyFallbackLabel": "プロキシ失敗時に直接接続へフォールバック", + "proxyNotConfigured": "未設定", + "proxyTestButton": "接続をテスト", + "proxyTestDesc": "設定したプロキシ経由でプロバイダー URL へのアクセスをテストします (HEAD リクエストを使用し、クォータは消費しません)", + "proxyTestFailed": "接続失敗", + "proxyTestFillUrl": "先にプロバイダー URL を入力してください", + "proxyTestLabel": "接続テスト", + "proxyTestNetworkError": "ネットワークエラー: {error}", + "proxyTestProxyError": "プロキシエラー: {error}", + "proxyTestResponseTime": "応答時間: {time}", + "proxyTestResultConnectionMethod": "接続方法: {via}", + "proxyTestResultConnectionMethodDirect": "直接", + "proxyTestResultConnectionMethodProxy": "プロキシ", + "proxyTestResultErrorType": "エラー種別: {type}", + "proxyTestResultFailed": "接続失敗", + "proxyTestResultMessage": "{message}", + "proxyTestResultResponseTime": "応答時間: {time}ms", + "proxyTestResultStatusCode": "ステータスコード: {code}", + "proxyTestResultSuccess": "接続成功 {via}", + "proxyTestStatusCode": "| ステータスコード: {code}", + "proxyTestSuccess": "接続成功", + "proxyTestTesting": "テスト中...", + "proxyTestTimeout": "接続がタイムアウトしました (5秒)。次を確認してください:\n1. プロキシサーバーに接続できるか\n2. プロキシのアドレスとポートが正しいか\n3. プロキシの認証情報が正しいか", + "proxyTestViaDirect": "(直接)", + "proxyTestViaProxy": "(プロキシ経由)", + "proxyUrl": "プロキシアドレス", + "proxyUrlPlaceholder": "例:http://proxy.example.com:8080 または socks5://127.0.0.1:1080", + "rateLimitConfig": "レート制限設定", + "rateLimitConfigNone": "無制限", + "rateLimitConfigSummary": "5h: ${fiveHour}, 週: ${weekly}, 月: ${monthly}, 同時: {concurrent}", + "remark": "備考", + "remarkPlaceholder": "オプション:説明を追加...", + "removeRedirect": "リダイレクトを削除", + "routingConfig": "ルーティング設定", + "routingConfigNone": "未設定", + "routingConfigSummary": "モデル許可リスト {models} 件、リダイレクト {redirects} 件", + "scheduleParams": "スケジューリングパラメータ", + "searchClear": "検索をクリア", + "searchPlaceholder": "プロバイダー名、URL、備考で検索...", + "selectProviderType": "プロバイダータイプを選択", + "sort": "プロバイダーをソート", + "sortByCost": "コスト順", + "sortByCreated": "作成日順 (新-旧)", + "sortByName": "名前順 (A-Z)", + "sortByPriority": "優先度順 (高-低)", + "sortByWeight": "重み順 (高-低)", + "sourceModel": "ソースモデル名", + "sourceModelPlaceholder": "例:claude-sonnet-4-5-20250929", + "sourceModelRequired": "ソースモデル名は空にできません", + "successThreshold": "回復閾値 (回)", + "successThresholdDesc": "半開状態で何回成功したら完全に回復するか", + "successThresholdPlaceholder": "2", + "targetModel": "ターゲットモデル名", + "targetModelPlaceholder": "例:glm-4.6", + "targetModelRequired": "ターゲットモデル名は空にできません", + "testProxy": "接続をテスト", + "testProxyFailed": "プロキシテストに失敗しました", + "testProxyFailedError": "接続テスト失敗:", + "testProxySuccess": "プロキシ接続成功", + "validUrlRequired": "有効な API アドレスを入力してください", + "websiteUrlDesc": "プロバイダー公式サイト URL (管理画面へのクイックリンク)", + "websiteUrlInvalid": "有効なプロバイダー公式サイト URL を入力してください", + "websiteUrlPlaceholder": "https://example.com", + "weight": "重み", + "weightDesc": "加重ランダム確率。同じ優先度内では、重みが高いほど選択される確率が高くなります。", + "weightLabel": "重み", + "weightPlaceholder": "1" +} diff --git a/messages/ja/settings/providers/form/success.json b/messages/ja/settings/providers/form/success.json new file mode 100644 index 000000000..0fa668b07 --- /dev/null +++ b/messages/ja/settings/providers/form/success.json @@ -0,0 +1,4 @@ +{ + "created": "プロバイダーを追加しました", + "createdDesc": "「{name}」を追加しました" +} diff --git a/messages/ja/settings/providers/form/title.json b/messages/ja/settings/providers/form/title.json new file mode 100644 index 000000000..410c09410 --- /dev/null +++ b/messages/ja/settings/providers/form/title.json @@ -0,0 +1,4 @@ +{ + "create": "プロバイダーを追加", + "edit": "プロバイダーを編集" +} diff --git a/messages/ja/settings/providers/form/url.json b/messages/ja/settings/providers/form/url.json new file mode 100644 index 000000000..7285ae182 --- /dev/null +++ b/messages/ja/settings/providers/form/url.json @@ -0,0 +1,4 @@ +{ + "label": "API アドレス *", + "placeholder": "例: https://open.bigmodel.cn/api/anthropic" +} diff --git a/messages/ja/settings/providers/form/urlPreview.json b/messages/ja/settings/providers/form/urlPreview.json new file mode 100644 index 000000000..9efeab434 --- /dev/null +++ b/messages/ja/settings/providers/form/urlPreview.json @@ -0,0 +1,9 @@ +{ + "copy": "コピー", + "copyFailed": "コピーに失敗しました", + "copySuccess": "{name} をクリップボードにコピーしました", + "duplicatePath": "重複パス検出", + "invalidUrl": "無効なURL形式", + "invalidUrlDesc": "有効なHTTP/HTTPSアドレスを入力してください", + "title": "URL結合プレビュー" +} diff --git a/messages/ja/settings/providers/form/websiteUrl.json b/messages/ja/settings/providers/form/websiteUrl.json new file mode 100644 index 000000000..79ffa0068 --- /dev/null +++ b/messages/ja/settings/providers/form/websiteUrl.json @@ -0,0 +1,5 @@ +{ + "desc": "管理ページへのクイックアクセス用", + "label": "プロバイダー公式サイト", + "placeholder": "https://example.com" +} diff --git a/messages/ja/settings/providers/guide.json b/messages/ja/settings/providers/guide.json new file mode 100644 index 000000000..6d94f615b --- /dev/null +++ b/messages/ja/settings/providers/guide.json @@ -0,0 +1,120 @@ +{ + "after": "フィルタ後:", + "before": "フィルタ前:", + "bestPracticesConcurrent": "• 同時実行制御: プロバイダー API 制限に合わせてセッションの同時数を設定", + "bestPracticesCost": "• コスト乗数: 公式=1.0、自前ホストは 0.8-1.2 を設定可能", + "bestPracticesLimit": "• 上限設定: 予算に合わせて 5h / 7d / 30d の上限を設定", + "bestPracticesPriority": "• 優先度設定: メインのプロバイダー=0、バックアップ=1-3", + "bestPracticesTitle": "ベストプラクティス", + "bestPracticesWeight": "• 重み設定: 容量に応じて重みを設定 (容量が大きいほど重みも大きい)", + "circuitBreaker": "サーキットブレーカーチェック", + "circuitBreakerOpen": "Aはフィルタされました、残り:B、C、D", + "circuitBreakerRecovery": "Aは60秒後に半開状態に自動復帰します", + "circuitBreakerRecovery5h": "5時間スライディングウィンドウ後に自動復帰", + "costOptimize": "2. コスト最適化: 同一優先度内では、コスト乗数が低いほど選択確率が高くなります", + "costSort": "コストベースのソートフォールバック", + "costSortExample": "すべてのプロバイダー: A (default)、B (premium)、C (premium)、D (economy)", + "costSortProb": "より安いCがより高い選択確率を持ちます", + "costSortResult": "ソート後: C (0.8x)、A (1.0x)", + "decision": "判断:", + "group": "ユーザーグループフィルタリング", + "groupDesc": "ユーザーがプロバイダーグループを指定した場合、システムはそのグループから優先的に選択します", + "groupDowngrade": "警告をログに記録し、グローバルプロバイダープールから選択", + "groupExample": "ユーザーは providerGroup = 'premium' を設定しました", + "groupFallback": "グループに利用可能なプロバイダーがない場合は、すべてのプロバイダーにフォールバック", + "groupFiltered": "AとCのみから選択、BとDはフィルタされます", + "groupUnavailable": "ユーザーグループ'vip'のすべてのプロバイダーが無効または制限超過です", + "health": "健全性フィルタリング (サーキットブレーカー + レート制限)", + "healthCheck": "Bが有効で健全かをチェック", + "healthCheckAmountLimit": "支出上限をチェック (5h, 7d, 30d)", + "healthCheckAmountLimitExample": "プロバイダー B の 5h 上限 $10、消費 $9.8", + "healthCheckCircuit": "プロバイダー A 5回失敗、サーキットブレーカー: オープン", + "healthCheckConcurrent": "現在のアクティブセッション数が上限を超えていないかをチェック", + "healthCheckConcurrentExample": "プロバイダー C 同時制限 2、現在アクティブ 2 セッション", + "healthFilter": "3. ヘルスフィルタ: サーキットブレーカー中または上限超過のプロバイダーを自動でスキップします", + "healthFiltered": "B を除外 (上限に接近)、残り: C、D", + "healthFiltered2": "C を除外 (満杯)、残り: D", + "history": "リクエスト履歴をチェック", + "historyDesc": "この API キーが過去 10 秒間に使用したプロバイダーをクエリします", + "priority": "優先度階層化", + "priorityExample": "異なる優先度を持つ4つのプロバイダーが有効です", + "priorityFirst": "1. 優先度優先: 最優先 (数値が最小) のプロバイダーからのみ選択します", + "priorityResult": "最優先 (0) のプロバイダーに絞り込み: A、C", + "priorityStep": "システムは優先度でフィルタし、最高優先度のプロバイダーのみを選択します", + "randomResult": "最終的に C がランダムに選択されました", + "randomSelect": "加重ランダム", + "reset": "サーキットブレーカーを手動リセット", + "resetSuccess": "サーキットブレーカーがリセットされました", + "scenario1Desc": "システムはまず優先度でフィルタし、最優先プロバイダーのみから選択します", + "scenario1Step1": "初期状態", + "scenario1Step1After": "最優先 (0) のプロバイダーに絞り込み: A、C", + "scenario1Step1Before": "プロバイダー A (優先度 0)、B (優先度 1)、C (優先度 0)、D (優先度 2)", + "scenario1Step1Decision": "A と C のみから選択し、B と D は除外されます", + "scenario1Step1Desc": "優先度が異なる4つのプロバイダーが有効です", + "scenario1Step2": "コストソート", + "scenario1Step2After": "ソート後: C (0.8x)、A (1.0x)", + "scenario1Step2Before": "A (コスト 1.0x)、C (コスト 0.8x)", + "scenario1Step2Decision": "より安い C の方が選択確率が高くなります", + "scenario1Step2Desc": "同一優先度内では、コスト乗数の低い順にソートします", + "scenario1Step3": "加重ランダム", + "scenario1Step3After": "C の確率 75%、A の確率 25%", + "scenario1Step3Before": "C (重み 3)、A (重み 1)", + "scenario1Step3Decision": "最終的に C がランダムに選択されました", + "scenario1Step3Desc": "重みでランダム選択し、重みが大きいほど確率が高くなります", + "scenario1Title": "優先度階層化", + "scenario2Desc": "ユーザーがプロバイダーグループを指定した場合、システムはそのグループから優先的に選択します", + "scenario2Step1": "ユーザーグループを確認", + "scenario2Step1After": "'premium' グループに絞り込み: B、C", + "scenario2Step1Before": "すべてのプロバイダー: A (default)、B (premium)、C (premium)、D (economy)", + "scenario2Step1Decision": "B と C のみから選択します", + "scenario2Step1Desc": "ユーザーは providerGroup = 'premium' を設定しました", + "scenario2Step2": "グループフォールバック", + "scenario2Step2After": "有効なすべてのプロバイダーにフォールバック: A、B、C、D", + "scenario2Step2Before": "ユーザーグループ'vip'のすべてのプロバイダーが無効または制限超過です", + "scenario2Step2Decision": "警告をログに記録し、グローバルプロバイダープールから選択します", + "scenario2Step2Desc": "グループに利用可能なプロバイダーがない場合は、すべてのプロバイダーにフォールバック", + "scenario2Title": "ユーザーグループフィルタリング", + "scenario3Desc": "システムはサーキットブレーカー中または上限超過のプロバイダーを自動で除外します", + "scenario3Step1": "サーキットブレーカーチェック", + "scenario3Step1After": "A を除外、残り: B、C、D", + "scenario3Step1Before": "プロバイダー A 5回失敗、サーキットブレーカー: オープン", + "scenario3Step1Decision": "A は 60 秒後に半開状態へ自動復帰します", + "scenario3Step1Desc": "5回連続で失敗するとサーキットブレーカーがオープンし、60秒間利用できません", + "scenario3Step2": "金額レート制限", + "scenario3Step2After": "B を除外 (上限に接近)、残り: C、D", + "scenario3Step2Before": "プロバイダー B の 5h 上限 $10、消費 $9.8", + "scenario3Step2Decision": "5時間スライディングウィンドウ後に自動復帰", + "scenario3Step2Desc": "支出が上限 (5h, 7d, 30d) を超えていないか確認します", + "scenario3Step3": "同時セッション制限", + "scenario3Step3After": "C を除外 (満杯)、残り: D", + "scenario3Step3Before": "プロバイダー C 同時制限 2、現在アクティブ 2 セッション", + "scenario3Step3Decision": "セッション期限切れ後に自動解放 (5分)", + "scenario3Step3Desc": "現在のアクティブセッション数が設定された同時上限を超えていないか確認します", + "scenario3Title": "ヘルスフィルタ (サーキットブレーカー + レート制限)", + "scenario4Desc": "連続したチャットでは Claude のコンテキストキャッシュを活用し、同じプロバイダーを優先して再利用します", + "scenario4Step1": "リクエスト履歴を確認", + "scenario4Step1After": "B が有効で健全かをチェック", + "scenario4Step1Before": "最後のリクエストはプロバイダー B を使用しました", + "scenario4Step1Decision": "B は利用可能なら直接再利用し、ランダム選択をスキップします", + "scenario4Step1Desc": "この API キーが過去 10 秒間に使用したプロバイダーをクエリします", + "scenario4Step2": "再利用の無効化", + "scenario4Step2After": "通常の選択フローに戻ります", + "scenario4Step2Before": "前回使用したプロバイダー B は無効またはサーキットオープン状態です", + "scenario4Step2Decision": "他の利用可能なプロバイダーから選択", + "scenario4Step2Desc": "前回使用したプロバイダーが利用できない場合は再選択します", + "scenario4Title": "セッション再利用メカニズム", + "scenariosTitle": "インタラクティブなシナリオデモ", + "session": "セッション再利用メカニズム", + "sessionDesc": "前回使用したプロバイダーが利用できない場合は再選択します", + "sessionExample": "最後のリクエストはプロバイダー B を使用しました", + "sessionExpired": "セッションは期限切れ (5分) 後に自動的に解放されます", + "sessionFallback": "他の利用可能なプロバイダーから選択", + "sessionLastUsed": "B は利用可能、直接再利用、ランダム選択をスキップ", + "sessionReuse": "4. セッション再利用: 連続したチャットでは同じプロバイダーを再利用し、コンテキストコストを節約します", + "sessionUnavailable": "前回使用したプロバイダー B は無効またはサーキットオープン状態です", + "step": "ステップ", + "title": "基本原則", + "weight": "重みに基づく加重ランダム選択", + "weightCalc": "Cは75%の選択確率、Aは25%", + "weightExample": "C (重み 3)、A (重み 1)" +} diff --git a/messages/ja/settings/providers/inlineEdit.json b/messages/ja/settings/providers/inlineEdit.json new file mode 100644 index 000000000..6e835c01b --- /dev/null +++ b/messages/ja/settings/providers/inlineEdit.json @@ -0,0 +1,12 @@ +{ + "cancel": "キャンセル", + "costMultiplierInvalid": "0以上の数値を入力してください", + "costMultiplierLabel": "コスト倍率", + "priorityInvalid": "0 以上の整数を入力してください", + "priorityLabel": "優先度", + "save": "保存する", + "saveFailed": "保存に失敗しました", + "saveSuccess": "保存に成功しました", + "weightInvalid": "1〜100 の整数を入力してください", + "weightLabel": "重み" +} diff --git a/messages/ja/settings/providers/list.json b/messages/ja/settings/providers/list.json new file mode 100644 index 000000000..32793f63b --- /dev/null +++ b/messages/ja/settings/providers/list.json @@ -0,0 +1,37 @@ +{ + "cancelButton": "キャンセル", + "circuitBroken": "遮断中", + "clipboardUnavailable": "この環境ではクリップボードを使用できません。手動でコピーしてください。", + "confirmDeleteMessage": "プロバイダー \"{name}\" を削除してもよろしいですか?この操作は元に戻せません。", + "confirmDeleteTitle": "プロバイダーの削除を確認しますか?", + "copyFailed": "コピーに失敗しました", + "costMultiplier": "コスト倍率", + "deleteButton": "削除", + "deleteError": "操作中にエラーが発生しました", + "deleteFailed": "削除に失敗しました", + "deleteSuccess": "削除に成功しました", + "deleteSuccessDesc": "プロバイダー \"{name}\" が削除されました", + "getKeyFailed": "キーの取得に失敗しました", + "keyCopied": "キーがクリップボードにコピーされました", + "keyLoading": "読み込み中...", + "officialWebsite": "公式", + "priority": "優先度", + "resetCircuitFailed": "サーキットブレーカーのリセットに失敗しました", + "resetCircuitSuccess": "サーキットブレーカーがリセットされました", + "resetCircuitSuccessDesc": "プロバイダー \"{name}\" のサーキットブレーカーステータスがクリアされました", + "resetUsageFailed": "総用量のリセットに失敗しました", + "resetUsageSuccess": "総用量をリセットしました", + "resetUsageSuccessDesc": "プロバイダー \"{name}\" の総用量をリセットしました", + "resetUsageTitle": "総用量をリセット", + "statusDisabled": "無効", + "statusEnabled": "有効", + "todayUsageCount": "{count} 回", + "todayUsageLabel": "本日の使用量", + "toggleFailed": "切り替えに失敗しました", + "toggleSuccess": "プロバイダーが{status}になりました", + "toggleSuccessDesc": "プロバイダー \"{name}\" のステータスが更新されました", + "unknownError": "不明なエラー", + "viewFullKey": "完全な API キーを表示", + "viewFullKeyDesc": "安全に保管し、他人と共有しないでください", + "weight": "重み" +} diff --git a/messages/ja/settings/providers/schedulingDialog.json b/messages/ja/settings/providers/schedulingDialog.json new file mode 100644 index 000000000..0144beabb --- /dev/null +++ b/messages/ja/settings/providers/schedulingDialog.json @@ -0,0 +1,9 @@ +{ + "after": "後:", + "before": "前:", + "decision": "決定:", + "description": "システムが高可用性とコスト最適化のために上流プロバイダーをインテリジェントに選択する方法を理解する", + "step": "ステップ", + "title": "プロバイダースケジューリングルール", + "triggerButton": "スケジューリングルール" +} diff --git a/messages/ja/settings/providers/search.json b/messages/ja/settings/providers/search.json new file mode 100644 index 000000000..131fe3b36 --- /dev/null +++ b/messages/ja/settings/providers/search.json @@ -0,0 +1,7 @@ +{ + "clear": "検索をクリア", + "found": "{count}件のプロバイダーが見つかりました", + "notFound": "一致するプロバイダーが見つかりません", + "placeholder": "プロバイダー名、URL、メモを検索...", + "showing": "{filtered} / {total} プロバイダーを表示" +} diff --git a/messages/ja/settings/providers/section.json b/messages/ja/settings/providers/section.json new file mode 100644 index 000000000..43b9d3dce --- /dev/null +++ b/messages/ja/settings/providers/section.json @@ -0,0 +1,5 @@ +{ + "description": "上流プロバイダーの支出制限とセッション並行制限を設定します。空のままにすると無制限です。", + "leaderboard": "プロバイダーランキング", + "title": "プロバイダー管理" +} diff --git a/messages/ja/settings/providers/sort.json b/messages/ja/settings/providers/sort.json new file mode 100644 index 000000000..93aebfc84 --- /dev/null +++ b/messages/ja/settings/providers/sort.json @@ -0,0 +1,8 @@ +{ + "byActualPriority": "実際の選択優先順", + "byCreatedAt": "作成日時順 (新-旧)", + "byName": "名前順 (A-Z)", + "byPriority": "優先度順 (高-低)", + "byWeight": "重み順 (高-低)", + "placeholder": "プロバイダーを並べ替え" +} diff --git a/messages/ja/settings/providers/strings.json b/messages/ja/settings/providers/strings.json new file mode 100644 index 000000000..a7f870e86 --- /dev/null +++ b/messages/ja/settings/providers/strings.json @@ -0,0 +1,47 @@ +{ + "add": "プロバイダーを追加", + "addFailed": "プロバイダーの追加に失敗しました", + "addProvider": "プロバイダーを追加", + "addSuccess": "プロバイダーが正常に追加されました", + "circuitBroken": "サーキットブレーカー作動中", + "clone": "プロバイダーを複製", + "cloneFailed": "コピーに失敗しました", + "confirmDelete": "このプロバイダーを削除してもよろしいですか?", + "confirmDeleteDesc": "プロバイダー「{name}」を削除してもよろしいですか?この操作は元に戻せません。", + "confirmDeleteProvider": "プロバイダーの削除を確認しますか?", + "confirmDeleteProviderDesc": "サービスプロバイダー「{name}」を削除してもよろしいですか?この操作は復元できません。", + "createProvider": "サービスプロバイダーを追加", + "delete": "プロバイダーを削除", + "deleteFailed": "プロバイダーの削除に失敗しました", + "deleteSuccess": "削除しました", + "description": "APIサービスプロバイダーを設定し、可用性ステータスを維持します。", + "disabledStatus": "無効", + "displayCount": "{filtered} / {total}個のプロバイダーを表示", + "edit": "プロバイダーを編集", + "editFailed": "プロバイダーの更新に失敗しました", + "editProvider": "サービスプロバイダーを編集", + "enabledStatus": "有効", + "keyLoading": "読み込み中...", + "noProviders": "プロバイダーの設定がありません", + "noProvidersDesc": "最初の API プロバイダーを追加してください", + "notFound": "プロバイダーが見つかりません", + "official": "公式サイト", + "resetCircuit": "サーキットブレーカーをリセットしました", + "resetCircuitDesc": "プロバイダー「{name}」のサーキットブレーカー状態を解除しました", + "resetCircuitFailed": "サーキットブレーカーのリセットに失敗しました", + "scheduling": "スケジューリング戦略の詳細説明", + "schedulingDesc": "プロバイダー選択の仕組み:優先度階層化、セッション再利用、ロードバランシング、フェイルオーバーを理解します", + "searchNoResults": "一致するプロバイダーが見つかりません", + "searchResults": "{count} 件のプロバイダーが見つかりました", + "subtitle": "プロバイダー管理", + "subtitleDesc": "上流プロバイダーの支出制限とセッション並行制限を設定します。空のままにすると無制限です。", + "title": "プロバイダー管理", + "todayUsage": "今日の使用量", + "todayUsageCount": "{count} 回", + "toggleFailed": "状態の切り替えに失敗しました", + "toggleSuccess": "プロバイダーを{status}にしました", + "toggleSuccessDesc": "プロバイダー「{name}」の状態を更新しました", + "updateFailed": "プロバイダーの更新に失敗しました", + "viewKey": "完全な API キーを表示", + "viewKeyDesc": "安全に保管し、他人に共有しないでください" +} diff --git a/messages/ja/settings/providers/types.json b/messages/ja/settings/providers/types.json new file mode 100644 index 000000000..920eed164 --- /dev/null +++ b/messages/ja/settings/providers/types.json @@ -0,0 +1,26 @@ +{ + "claude": { + "description": "Anthropic 公式 API", + "label": "Claude" + }, + "claudeAuth": { + "description": "Claude リレーサービス", + "label": "Claude Auth" + }, + "codex": { + "description": "Codex CLI API", + "label": "Codex" + }, + "gemini": { + "description": "Google Gemini API", + "label": "Gemini" + }, + "geminiCli": { + "description": "Gemini CLI API", + "label": "Gemini CLI" + }, + "openaiCompatible": { + "description": "OpenAI 互換 API", + "label": "OpenAI Compatible" + } +} diff --git a/messages/ja/settings/requestFilters.json b/messages/ja/settings/requestFilters.json new file mode 100644 index 000000000..a654dfda7 --- /dev/null +++ b/messages/ja/settings/requestFilters.json @@ -0,0 +1,84 @@ +{ + "actionLabel": { + "json_path": "JSONパス置換", + "remove": "ヘッダー削除", + "set": "ヘッダー設定", + "text_replace": "テキスト置換" + }, + "add": "フィルター追加", + "addFailed": "作成に失敗しました", + "addSuccess": "作成しました", + "applyToAll": "すべてのリクエストに適用", + "confirmDelete": "フィルター \"{name}\" を削除しますか?", + "delete": "フィルター削除", + "deleteFailed": "削除に失敗しました", + "deleteSuccess": "削除しました", + "description": "上流に送る前にヘッダー削除/上書きやボディ置換を行い、リクエストをサニタイズします。", + "dialog": { + "action": "アクション", + "bindingGlobal": "全プロバイダー(グローバル)", + "bindingGroups": "プロバイダーグループ", + "bindingProviders": "特定のプロバイダー", + "bindingType": "適用先", + "clear": "クリア", + "createTitle": "フィルター追加", + "description": "説明 (任意)", + "editTitle": "フィルター編集", + "groupsSelected": "{count}件のグループを選択", + "jsonPathPlaceholder": "例: messages.0.content / data.items[0].token", + "loading": "読み込み中...", + "matchType": "マッチタイプ", + "matchTypeContains": "含む", + "matchTypeExact": "完全一致", + "matchTypeRegex": "正規表現", + "name": "名前", + "noGroupsFound": "グループが見つかりません", + "noProvidersFound": "プロバイダーが見つかりません", + "priority": "優先度", + "providersSelected": "{count}件のプロバイダーを選択", + "replacement": "置換値 (任意)", + "replacementPlaceholder": "文字列またはJSON。空でクリア", + "save": "保存する", + "saving": "保存しています...", + "scope": "スコープ", + "searchGroups": "グループ検索...", + "searchProviders": "プロバイダー検索...", + "selectAll": "すべて選択", + "selectGroups": "グループを選択...", + "selectProviders": "プロバイダーを選択...", + "target": "対象フィールド/パス", + "targetPlaceholder": "ヘッダー名またはテキスト/パス", + "validation": { + "fieldRequired": "名称と対象は必須です" + } + }, + "disable": "無効", + "edit": "フィルター編集", + "editFailed": "更新に失敗しました", + "editSuccess": "更新しました", + "empty": "フィルターがありません。追加してください。", + "enable": "有効", + "groups": "グループ", + "nav": "リクエストフィルター", + "providers": "プロバイダー", + "refreshCache": "キャッシュ更新", + "refreshFailed": "更新に失敗しました", + "refreshSuccess": "{count} 件読み込みました", + "scopeLabel": { + "body": "Body", + "header": "Header" + }, + "table": { + "action": "アクション", + "actions": "アクション", + "apply": "適用", + "createdAt": "作成日時", + "name": "名前", + "priority": "優先度", + "replacement": "置換値", + "scope": "スコープ", + "status": "状態", + "target": "対象" + }, + "title": "リクエストフィルター" +} diff --git a/messages/ja/settings/sensitiveWords.json b/messages/ja/settings/sensitiveWords.json new file mode 100644 index 000000000..32fbea832 --- /dev/null +++ b/messages/ja/settings/sensitiveWords.json @@ -0,0 +1,55 @@ +{ + "add": "センシティブワードを追加", + "addFailed": "センシティブワードの作成に失敗しました", + "addSuccess": "センシティブワードが正常に作成されました", + "cacheStats": "キャッシュ統計:部分一致({containsCount}) 完全一致({exactCount}) 正規表現({regexCount})", + "confirmDelete": "センシティブワード「{word}」を削除してもよろしいですか?", + "delete": "センシティブワードを削除", + "deleteFailed": "削除に失敗しました", + "deleteSuccess": "センシティブワードが正常に削除されました", + "description": "センシティブコンテンツをブロックするためにセンシティブワードフィルタリングルールを設定します。", + "dialog": { + "addDescription": "センシティブワードフィルタリングルールを設定します。マッチしたリクエストはアップストリームに転送されません。", + "addTitle": "センシティブワードを追加", + "creating": "作成中...", + "descriptionLabel": "説明", + "descriptionPlaceholder": "オプション:説明を追加...", + "editDescription": "センシティブワード設定を変更します。変更後、自動的にキャッシュがリフレッシュされます。", + "editTitle": "センシティブワードを編集", + "matchTypeContains": "部分一致 - テキストにこの単語が含まれている場合ブロック", + "matchTypeExact": "完全一致 - 完全に一致する場合のみブロック", + "matchTypeLabel": "マッチタイプ *", + "matchTypeRegex": "正規表現 - 複雑なパターンマッチングをサポート", + "saving": "保存しています...", + "wordLabel": "センシティブワード *", + "wordPlaceholder": "センシティブワードを入力...", + "wordRequired": "センシティブワードを入力してください" + }, + "disable": "センシティブワードが無効になりました", + "edit": "センシティブワードを編集", + "editFailed": "センシティブワードの更新に失敗しました", + "editSuccess": "センシティブワードが正常に更新されました", + "emptyState": "センシティブワードがありません。右上の「センシティブワードを追加」をクリックして設定を開始してください。", + "enable": "センシティブワードが有効になりました", + "refreshCache": "キャッシュを更新", + "refreshCacheFailed": "キャッシュのリフレッシュに失敗しました", + "refreshCacheSuccess": "キャッシュのリフレッシュに成功しました。{count}個のセンシティブワードを読み込みました", + "section": { + "description": "センシティブワードによってブロックされたリクエストはアップストリームに転送されず、課金もされません。部分一致、完全一致、正規表現の3つのモードをサポートしています。", + "title": "センシティブワードリスト" + }, + "table": { + "actions": "アクション", + "createdAt": "作成日時", + "description": "説明", + "matchType": "マッチタイプ", + "matchTypeContains": "部分一致", + "matchTypeExact": "完全一致", + "matchTypeRegex": "正規表現", + "status": "ステータス", + "word": "センシティブワード" + }, + "title": "センシティブワード管理", + "toggleFailed": "トグルに失敗しました", + "toggleFailedError": "トグルに失敗しました:" +} diff --git a/messages/ja/settings/strings.json b/messages/ja/settings/strings.json new file mode 100644 index 000000000..3f02d5560 --- /dev/null +++ b/messages/ja/settings/strings.json @@ -0,0 +1,22 @@ +{ + "mcpPassthroughConfig": "MCP パススルー設定", + "mcpPassthroughConfigCustom": "カスタム (予約)", + "mcpPassthroughConfigGlm": "GLM", + "mcpPassthroughConfigMinimax": "Minimax", + "mcpPassthroughConfigNone": "無効", + "mcpPassthroughCustomDesc": "カスタム MCP サービスにパススルー(予約、未実装)", + "mcpPassthroughCustomLabel": "カスタム", + "mcpPassthroughDesc": "有効にすると、MCP ツール呼び出しを指定された AI プロバイダにパススルーします(例:minimax の画像認識、Web 検索)", + "mcpPassthroughGlmDesc": "GLM MCP サービスにパススルー(画像分析、動画分析などをサポート)", + "mcpPassthroughGlmLabel": "GLM", + "mcpPassthroughHint": "ヒント: MCP パススルーにより、Claude Code クライアントは第三者の AI プロバイダ提供的ツール機能(画像認識、Web 検索など)を使用できます", + "mcpPassthroughMinimaxDesc": "minimax MCP サービスにパススルー(画像認識、Web 検索などをサポート)", + "mcpPassthroughMinimaxLabel": "Minimax", + "mcpPassthroughNoneDesc": "MCP パススルーを有効にしません(デフォルト)", + "mcpPassthroughNoneLabel": "無効", + "mcpPassthroughSelect": "パススルータイプ", + "mcpPassthroughUrlAuto": "自動抽出: {url}", + "mcpPassthroughUrlDesc": "MCP サービスベース URL。空のままにすると、プロバイダ URL から自動的に抽出されます", + "mcpPassthroughUrlLabel": "MCP パススルー URL", + "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com" +} diff --git a/messages/ru/index.ts b/messages/ru/index.ts index d5bad5de2..70f0e4ca7 100644 --- a/messages/ru/index.ts +++ b/messages/ru/index.ts @@ -11,7 +11,7 @@ import notifications from "./notifications.json"; import providerChain from "./provider-chain.json"; import providers from "./providers.json"; import quota from "./quota.json"; -import settings from "./settings.json"; +import settings from "./settings"; import ui from "./ui.json"; import usage from "./usage.json"; import users from "./users.json"; diff --git a/messages/ru/provider-chain.json b/messages/ru/provider-chain.json index b44a317c9..70315cc17 100644 --- a/messages/ru/provider-chain.json +++ b/messages/ru/provider-chain.json @@ -86,7 +86,7 @@ "remaining": "Осталось попыток: {count}", "status": "Состояние", "alreadyBroken": "Уже сработал", - "circuitTriggered": "⚠️ Автомат защиты сработал", + "circuitTriggered": "Предупреждение: Автомат защиты сработал", "errorDetails": "Детали ошибки", "systemError": "Системная ошибка", "systemErrorFailed": "Системная ошибка (Попытка {attempt})", @@ -97,7 +97,7 @@ "errorMeaning": "Значение: {meaning}", "meaning": "Значение", "notCountedInCircuit": "Эта ошибка не учитывается в автомате защиты провайдера", - "systemErrorNote": "ℹ️ Эта ошибка не учитывается в автомате защиты провайдера", + "systemErrorNote": "Примечание: Эта ошибка не учитывается в автомате защиты провайдера", "reselection": "Повторный выбор провайдера", "reselect": "Повторный выбор провайдера", "excluded": "Исключено: {providers}", diff --git a/messages/ru/settings.json b/messages/ru/settings.json deleted file mode 100644 index 97daad508..000000000 --- a/messages/ru/settings.json +++ /dev/null @@ -1,2126 +0,0 @@ -{ - "clientVersions": { - "description": "Управление требованиями версии клиента для обеспечения использования последней стабильной версии. VSCode и CLI управляются отдельно.", - "empty": { - "description": "За последние 7 дней не было активных пользователей с распознаваемыми клиентами", - "title": "Нет данных о клиентах" - }, - "features": { - "activeWindow": "Активное окно:", - "activeWindowDesc": "Учитываются только пользователи с запросами за последние 7 дней", - "autoDetect": "Система автоматически определяет последнюю стабильную версию (GA версию) каждого типа клиента", - "blockOldVersion": "Пользователи старых версий будут получать HTTP 400 ошибку и не смогут продолжить использование сервиса", - "errorMessage": "Сообщение об ошибке будет содержать текущую версию и требуемую версию для обновления", - "gaRule": "Правила определения:", - "gaRuleDesc": "Версия считается GA версией, когда её используют более 1 пользователя", - "recommendation": "Рекомендуемый подход:", - "recommendationDesc": "Сначала изучите распределение версий ниже, убедитесь в стабильности новой версии перед включением.", - "title": "Описание функции", - "whatHappens": "Что произойдет после включения:" - }, - "section": { - "distribution": { - "description": "Показывает информацию о версиях клиентов активных пользователей за последние 7 дней. GA версия рассчитывается независимо для каждого типа клиента.", - "title": "Распределение версий клиентов" - }, - "settings": { - "description": "После включения система будет автоматически проверять версию клиента и блокировать запросы от пользователей со старыми версиями.", - "title": "Настройки напоминания об обновлении" - } - }, - "table": { - "currentGA": "Текущая GA версия:", - "internalType": "Внутренний тип:", - "lastActive": "Последняя активность", - "latest": "Последняя", - "needsUpgrade": "Требуется обновление", - "noUsers": "Нет данных о пользователях", - "status": "Статус", - "unknown": "Неизвестно", - "user": "Пользователь", - "usersCount": "{count} пользователей", - "version": "Текущая версия" - }, - "title": "Напоминание об обновлении клиента", - "toggle": { - "description": "При включении система автоматически проверяет версию и блокирует старые версии.", - "disableSuccess": "Проверка версии клиента отключена", - "enable": "Включить проверку версии клиента", - "enableSuccess": "Проверка версии клиента включена", - "toggleFailed": "Ошибка переключения" - } - }, - "common": { - "cancel": "Отменить", - "completed": "Завершено", - "confirm": "Подтвердить", - "copied": "Ключ скопирован в буфер обмена", - "copy": "Копировать", - "copyFailed": "Ошибка копирования", - "create": "Создать", - "creating": "Создание...", - "delete": "Удалить", - "disabled": "Отключено", - "edit": "Редактировать", - "empty": "Результаты не найдены", - "enabled": "Включено", - "error": "Неизвестная ошибка", - "failed": "Ошибка", - "loading": "Загрузка...", - "none": "Нет (нет пользователей с этой версией)", - "refresh": "Обновить", - "reset": "Сброс", - "save": "Сохранить", - "saving": "Сохранение...", - "submit": "Отправить", - "success": "Успех", - "test": "Тестировать", - "testing": "Тестирование...", - "unlimited": "Без ограничений", - "unlimited_desc": "Без ограничений", - "update": "Обновить", - "updating": "Обновление..." - }, - "config": { - "autoCleanup": "Автоматическая очистка логов", - "autoCleanupDesc": "Автоматически очищать исторические логи по расписанию для освобождения места в БД.", - "description": "Управление основными параметрами системы, влияющими на отображение и поведение статистики.", - "section": { - "siteParams": { - "title": "Параметры сайта", - "description": "Настройка заголовка сайта, валюты отображения и политики отображения статистики на панели." - }, - "autoCleanup": { - "title": "Автоматическая очистка логов", - "description": "Автоматически очищать исторические логи по расписанию для освобождения места в БД." - } - }, - "form": { - "allowGlobalView": "Разрешить просмотр глобального использования", - "allowGlobalViewDesc": "При отключении обычные пользователи могут видеть только статистику использования своих ключей на панели.", - "verboseProviderError": "Подробные ошибки провайдеров", - "verboseProviderErrorDesc": "При включении возвращает подробные сообщения об ошибках при недоступности всех провайдеров (количество провайдеров, причины ограничений и т.д.); при отключении возвращает только простой код ошибки.", - "interceptAnthropicWarmupRequests": "Перехватывать Warmup-запросы (Anthropic)", - "interceptAnthropicWarmupRequestsDesc": "Если включено, Warmup-пробные запросы Claude Code будут отвечены самим CCH без обращения к провайдерам; запрос сохраняется в логах, но не тарифицируется, не учитывается в лимитах и исключается из статистики.", - "enableThinkingSignatureRectifier": "Включить исправление thinking-signature", - "enableThinkingSignatureRectifierDesc": "Если Anthropic-провайдер возвращает ошибку несовместимой подписи thinking или некорректного запроса, автоматически удаляет несовместимые thinking-блоки и повторяет запрос один раз к тому же провайдеру (включено по умолчанию).", - "enableResponseFixer": "Включить исправление ответов", - "enableResponseFixerDesc": "Автоматически исправляет распространённые проблемы ответа у провайдеров (кодировка, SSE, обрезанный JSON). Включено по умолчанию.", - "responseFixerFixEncoding": "Исправлять кодировку", - "responseFixerFixEncodingDesc": "Удаляет BOM/нулевые байты и нормализует невалидный UTF-8.", - "responseFixerFixSseFormat": "Исправлять формат SSE", - "responseFixerFixSseFormatDesc": "Добавляет отсутствующий префикс data:, нормализует переводы строк и исправляет распространённые поля.", - "responseFixerFixTruncatedJson": "Исправлять обрезанный JSON", - "responseFixerFixTruncatedJsonDesc": "Закрывает незакрытые скобки/кавычки, удаляет завершающие запятые и при необходимости дополняет null.", - "cleanupSchedule": "График очистки", - "cleanupScheduleDesc": "Выбрать расписание автоматической очистки", - "configUpdated": "Параметры системы обновлены. Страница обновится для применения изменений валюты.", - "currencyDisplay": "Валюта", - "currencyDisplayPlaceholder": "Выберите валюту", - "currencyDisplayDesc": "После изменения все страницы и API будут использовать соответствующий символ валюты (только символ, без конвертации).", - "keepDays": "Хранить дней", - "keepDaysDesc": "Очищать логи старше указанного количества дней", - "saveFailed": "Ошибка сохранения", - "saveSuccess": "Сохранено успешно", - "saveError": "Ошибка сохранения", - "saveSettings": "Сохранить настройки", - "siteTitle": "Название сайта", - "siteTitlePlaceholder": "например: Claude Code Hub", - "siteTitleRequired": "Название сайта не может быть пустым", - "siteTitleDesc": "Используется для установки заголовка вкладки браузера и имени системы по умолчанию.", - "enableAutoCleanup": "Включить автоочистку", - "enableAutoCleanupDesc": "Автоматически очищать исторические логи по расписанию", - "cleanupRetentionDays": "Хранить дней", - "cleanupRetentionDaysRequired": "Хранить дней *", - "cleanupRetentionDaysPlaceholder": "30", - "cleanupRetentionDaysDesc": "Логи старше этого количества дней будут автоматически очищены (диапазон: 1-365 дней)", - "cleanupScheduleLabel": "Время выполнения (Cron)", - "cleanupScheduleRequired": "Время выполнения (Cron) *", - "cleanupSchedulePlaceholder": "0 2 * * *", - "cleanupScheduleCronDesc": "Cron-выражение, по умолчанию: 0 2 * * * (2 часа ночи ежедневно)", - "cleanupScheduleCronExample": "Пример: 0 3 * * 0 (3 часа ночи каждое воскресенье)", - "cleanupBatchSize": "Размер пакета", - "cleanupBatchSizeRequired": "Размер пакета *", - "cleanupBatchSizePlaceholder": "10000", - "cleanupBatchSizeDesc": "Количество записей для удаления за раз (диапазон: 1000-100000, рекомендуется 10000)", - "saveConfig": "Сохранить конфигурацию", - "autoCleanupSaved": "Конфигурация автоочистки сохранена", - "currencies": { - "USD": "$ Доллар США (USD)", - "CNY": "¥ Китайский юань (CNY)", - "EUR": "€ Евро (EUR)", - "JPY": "¥ Японская иена (JPY)", - "GBP": "£ Фунт стерлингов (GBP)", - "HKD": "HK$ Гонконгский доллар (HKD)", - "TWD": "NT$ Новый тайваньский доллар (TWD)", - "KRW": "₩ Южнокорейская вона (KRW)", - "SGD": "S$ Сингапурский доллар (SGD)" - }, - "billingModelSource": "Источник модели для тарификации", - "billingModelSourcePlaceholder": "Выберите источник модели для тарификации", - "billingModelSourceDesc": "Настройте, какую модель использовать для тарификации при перенаправлении модели. «До перенаправления» использует исходную модель, запрошенную пользователем, «После перенаправления» использует фактически вызванную модель.", - "billingModelSourceOptions": { - "original": "До перенаправления (исходная модель)", - "redirected": "После перенаправления (фактическая модель)" - } - }, - "siteSettings": "Параметры сайта", - "siteSettingsDesc": "Настройка названия сайта, валюты и политики отображения статистики.", - "title": "Конфигурация" - }, - "data": { - "cleanup": { - "rangeLabel": "Диапазон очистки", - "range": { - "7days": "Логи старше 1 недели (7 дней)", - "30days": "Логи старше 1 месяца (30 дней)", - "90days": "Логи старше 3 месяцев (90 дней)", - "180days": "Логи старше 6 месяцев (180 дней)" - }, - "rangeDescription": { - "7days": "1 неделю назад", - "30days": "1 месяц назад", - "90days": "3 месяца назад", - "180days": "6 месяцев назад", - "default": "{days} дней назад" - }, - "willClean": "Будут очищены все записи логов с {range}", - "button": "Очистить логи", - "confirmTitle": "Подтверждение очистки логов", - "confirmWarning": "Эта операция навсегда удалит все записи логов с {range} и не может быть отменена.", - "previewLoading": "Подсчет...", - "previewCount": "Будет удалено {count} записей логов", - "previewError": "Не удается получить предварительный просмотр", - "statisticsRetained": "✓ Статистические данные будут сохранены (для анализа трендов)", - "logsDeleted": "✗ Детали логов будут удалены (содержимое запроса/ответа, информация об ошибках и т.д.)", - "backupRecommendation": "Рекомендация: Экспортируйте резервную копию базы данных перед очисткой на случай, если потребуется восстановление.", - "cancel": "Отмена", - "confirm": "Подтвердить очистку", - "cleaning": "Очистка...", - "successMessage": "Успешно очищено {count} записей логов ({batches} пакетов, заняло {duration}с)", - "failed": "Очистка не удалась", - "error": "Не удалось очистить логи", - "descriptionWarning": "Очистка исторических данных логов для освобождения дискового пространства базы данных. Примечание: Статистические данные будут сохранены, но детали логов будут удалены навсегда." - }, - "description": "Управление резервной копией и восстановлением БД с полным импортом/экспортом и очисткой логов.", - "export": { - "button": "Экспортировать базу данных", - "exporting": "Экспорт...", - "successMessage": "База данных успешно экспортирована!", - "failed": "Экспорт не удался", - "error": "Не удалось экспортировать базу данных", - "descriptionFull": "Экспорт полного файла резервной копии базы данных (формат .dump) для миграции или восстановления данных. Резервная копия использует формат PostgreSQL custom format, автоматически сжимается и совместима с разными версиями базы данных." - }, - "guide": { - "title": "Инструкции и меры предосторожности", - "items": { - "cleanup": { - "title": "Очистка логов", - "description": "Физически удаляет исторические логи (необратимо). Таблица статистики будет сохранена. Рекомендуется сначала экспортировать резервную копию." - }, - "format": { - "title": "Формат резервной копии", - "description": "Использует PostgreSQL custom format (.dump), автоматически сжимается и совместим с разными версиями БД." - }, - "overwrite": { - "title": "Режим перезаписи", - "description": "Удаляет все существующие данные перед импортом. Лучше всего для полного восстановления." - }, - "merge": { - "title": "Режим объединения", - "description": "Сохраняет существующие данные и пытается вставить из резервной копии. Конфликты первичного ключа могут привести к ошибкам." - }, - "safety": { - "title": "Рекомендация безопасности", - "description": "Перед импортом сначала экспортируйте текущую БД как резервную копию." - }, - "environment": { - "title": "Требования окружения", - "description": "Требует развертывания Docker Compose. Локальная разработка может не поддерживаться." - } - } - }, - "import": { - "selectFileLabel": "Выбрать файл резервной копии", - "fileSelected": "Выбрано: {name} ({size} МБ)", - "fileError": "Пожалуйста, выберите файл резервной копии в формате .dump", - "noFileSelected": "Сначала выберите файл резервной копии", - "cleanFirstLabel": "Очистить существующие данные (режим перезаписи)", - "cleanFirstDescription": "Удалить все существующие данные перед импортом, чтобы база данных точно соответствовала резервной копии. Если не отмечено, будет предпринята попытка объединения данных, но это может не удаться из-за конфликтов первичных ключей.", - "button": "Импортировать базу данных", - "importing": "Импорт...", - "progressTitle": "Прогресс импорта", - "confirmTitle": "Подтверждение импорта базы данных", - "confirmOverwrite": "Вы выбрали 'Режим перезаписи', который удалит все существующие данные перед импортом резервной копии.", - "confirmMerge": "Вы выбрали 'Режим объединения', который попытается импортировать резервную копию, сохраняя существующие данные.", - "warningOverwrite": "⚠️ Предупреждение: Это действие необратимо, все текущие данные будут навсегда удалены!", - "warningMerge": "⚠️ Примечание: Импорт может не удаться при наличии конфликтов первичных ключей.", - "backupFile": "Файл резервной копии:", - "backupRecommendation": "Рекомендуется экспортировать текущую базу данных в качестве резервной копии перед продолжением.", - "cancel": "Отмена", - "confirm": "Подтвердить импорт", - "successMessage": "Импорт данных завершен!", - "failedMessage": "Импорт данных не удался, проверьте подробные логи", - "error": "Не удалось импортировать базу данных", - "streamError": "Не удается прочитать поток ответа", - "errorUnknown": "Неизвестная ошибка", - "descriptionFull": "Восстановление базы данных из файла резервной копии. Поддерживает файлы резервных копий в формате PostgreSQL custom format (.dump)." - }, - "status": { - "loading": "Загрузка...", - "error": "Не удалось получить статус базы данных", - "retry": "Повторить", - "connected": "База данных подключена", - "unavailable": "База данных недоступна", - "tables": "{count} таблиц" - }, - "title": "Управление данными", - "section": { - "status": { - "title": "Статус базы данных", - "description": "Просмотр текущего статуса подключения к базе данных и основной информации." - }, - "cleanup": { - "title": "Очистка логов", - "description": "Очистка исторических данных логов для освобождения дискового пространства базы данных. Примечание: Статистические данные будут сохранены, но детали логов будут удалены навсегда." - }, - "export": { - "title": "Экспорт данных", - "description": "Экспорт полного файла резервной копии базы данных (формат .dump) для миграции или восстановления данных." - }, - "import": { - "title": "Импорт данных", - "description": "Восстановление базы данных из файла резервной копии. Поддерживает файлы резервных копий в формате PostgreSQL custom format (.dump)." - } - } - }, - "errors": { - "saveSuccess": "Сохранение успешно", - "saveFailed": "Ошибка сохранения", - "saveFailed_error": "Не удалось сохранить настройки", - "addSuccess": "Добавление успешно", - "addFailed": "Не удалось добавить провайдера", - "editSuccess": "Обновление успешно", - "editFailed": "Не удалось обновить провайдера", - "deleteSuccess": "Удаление успешно", - "deleteFailed": "Не удалось удалить провайдера", - "syncSuccess": "Синхронизация успешна", - "syncFailed": "Ошибка синхронизации", - "testFailed": "Тест не пройден", - "testFailedRetry": "Тест не пройден, попробуйте снова", - "loadFailed": "Не удалось загрузить настройки уведомлений", - "unknownError": "Во время операции произошло исключение" - }, - "logs": { - "description": "Динамическая регулировка уровня логирования для контроля подробности.", - "subtitle": "Контроль уровня логирования", - "subtitleDesc": "Изменения вступают в силу немедленно без перезагрузки. Полезно для отладки в производстве.", - "section": { - "title": "Контроль уровня логирования", - "description": "Изменения вступают в силу немедленно без перезапуска сервиса." - }, - "levels": { - "fatal": { - "label": "Fatal", - "description": "Только критические ошибки" - }, - "error": { - "label": "Error", - "description": "Сообщения об ошибках" - }, - "warn": { - "label": "Warn", - "description": "Предупреждения + Ошибки" - }, - "info": { - "label": "Info", - "description": "Ключевые бизнес-события + Предупреждения + Ошибки (Рекомендуется для продакшена)" - }, - "debug": { - "label": "Debug", - "description": "Отладочная информация + Все уровни (Рекомендуется для разработки)" - }, - "trace": { - "label": "Trace", - "description": "Очень подробная трассировка + Все уровни" - } - }, - "form": { - "currentLevel": "Текущий уровень логирования", - "selectLevel": "Выбрать уровень логирования", - "save": "Сохранить параметры", - "saving": "Сохранение...", - "success": "Уровень логирования установлен: {level}", - "failed": "Ошибка установки", - "failedError": "Ошибка установки уровня логирования", - "fetchFailed": "Ошибка получения уровня логирования", - "effectiveImmediately": "Изменения уровня логирования вступают в силу немедленно без перезапуска сервиса.", - "levelGuideTitle": "Руководство по уровням логирования", - "levelGuideFatal": "Fatal/Error: Показывать только ошибки, минимальное логирование, подходит для высоконагруженного продакшена", - "levelGuideWarn": "Warn: Включает предупреждения (ограничение скорости, открытие автомата защиты и т.д.) + Ошибки", - "levelGuideInfo": "Info (Рекомендуется для продакшена): Показывает ключевые бизнес-события (выбор поставщика, повторное использование сессии, синхронизация цен) + Предупреждения + Ошибки", - "levelGuideDebug": "Debug (Рекомендуется для разработки): Включает подробную отладочную информацию, подходит для устранения проблем", - "levelGuideTrace": "Trace: Очень подробная трассировочная информация, включает все детали", - "changeNotice": "Текущий уровень {current}, после сохранения переключится на {selected}" - }, - "title": "Управление логами" - }, - "nav": { - "apiDocs": "API док.", - "clientVersions": "Обновления", - "config": "Конфиг", - "data": "Данные", - "errorRules": "Ошибки", - "feedback": "Отзывы", - "docs": "Документация", - "logs": "Логи", - "notifications": "Уведомления", - "prices": "Цены", - "providers": "Поставщики", - "sensitiveWords": "Фильтры", - "requestFilters": "Запросы" - }, - "notifications": { - "title": "Push-уведомления", - "description": "Настройка push-уведомлений Webhook", - "global": { - "title": "Главный переключатель уведомлений", - "description": "Включить или отключить все функции push-уведомлений", - "enable": "Включить push-уведомления", - "legacyModeTitle": "Режим совместимости", - "legacyModeDescription": "Сейчас используется устаревшая схема уведомлений с одним URL. Создайте цель отправки, чтобы перейти на режим с несколькими целями." - }, - "targets": { - "title": "Цели отправки", - "description": "Управление целями отправки. Поддерживает WeCom, Feishu, DingTalk, Telegram и пользовательский Webhook.", - "add": "Добавить цель", - "update": "Сохранить цель", - "edit": "Редактировать", - "delete": "Удалить", - "deleteConfirmTitle": "Удалить цель", - "deleteConfirm": "Удалить эту цель? Связанные привязки также будут удалены.", - "enable": "Включить цель", - "statusEnabled": "Включено", - "statusDisabled": "Отключено", - "lastTestAt": "Последний тест", - "lastTestNever": "Тестов не было", - "lastTestSuccess": "Тест OK", - "lastTestFailed": "Тест не пройден", - "test": "Тест", - "testSelectType": "Выберите тип теста", - "emptyHint": "Целей нет. Нажмите «Добавить цель», чтобы создать.", - "created": "Цель создана", - "updated": "Цель обновлена", - "deleted": "Цель удалена", - "bindingsSaved": "Привязки сохранены" - }, - "targetDialog": { - "createTitle": "Добавить цель", - "editTitle": "Редактировать цель", - "name": "Название", - "namePlaceholder": "например, Ops Group", - "type": "Платформа", - "selectType": "Выберите платформу", - "enable": "Включить", - "webhookUrl": "Webhook URL", - "webhookUrlPlaceholder": "https://example.com/webhook", - "telegramBotToken": "Telegram Bot Token", - "telegramBotTokenPlaceholder": "например, 123456:ABCDEF...", - "telegramChatId": "Telegram Chat ID", - "telegramChatIdPlaceholder": "например, -1001234567890", - "dingtalkSecret": "Секрет DingTalk", - "dingtalkSecretPlaceholder": "Необязательно, для подписи", - "customHeaders": "Пользовательские заголовки (JSON)", - "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", - "errors": { - "headersInvalidJson": "Заголовки должны быть корректным JSON", - "headersMustBeObject": "Заголовки должны быть JSON-объектом", - "headersValueMustBeString": "Значения заголовков должны быть строками" - }, - "types": { - "wechat": "WeCom", - "feishu": "Feishu", - "dingtalk": "DingTalk", - "telegram": "Telegram", - "custom": "Custom Webhook" - }, - "proxy": { - "title": "Прокси", - "toggle": "Показать/скрыть настройки прокси", - "url": "URL прокси", - "urlPlaceholder": "http://127.0.0.1:7890", - "fallbackToDirect": "При ошибке прокси — прямое подключение" - } - }, - "bindings": { - "title": "Привязки", - "noTargets": "Нет доступных целей отправки.", - "bindTarget": "Привязать цель", - "enable": "Включить", - "enableType": "Включить это уведомление", - "advanced": "Дополнительно", - "scheduleCron": "Cron", - "scheduleCronPlaceholder": "например, 0 9 * * *", - "scheduleTimezone": "Часовой пояс", - "templateOverride": "Переопределение шаблона", - "editTemplateOverride": "Редактировать", - "templateOverrideTitle": "Редактировать переопределение шаблона", - "boundCount": "Привязано: {count}", - "enabledCount": "Включено: {count}" - }, - "templateEditor": { - "title": "Шаблон (JSON)", - "placeholder": "Введите JSON-шаблон...", - "jsonInvalid": "Некорректный JSON", - "placeholders": "Плейсхолдеры", - "insert": "Вставить" - }, - "circuitBreaker": { - "title": "Оповещение о размыкателе цепи", - "description": "Отправить оповещение немедленно при полном размыкании провайдера", - "enable": "Включить оповещение о размыкателе цепи", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "test": "Тест подключения" - }, - "dailyLeaderboard": { - "title": "Ежедневный рейтинг потребления пользователей", - "description": "Ежедневная отправка рейтинга топ N пользователей по потреблению", - "enable": "Включить ежедневный рейтинг", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "time": "Время отправки", - "timePlaceholder": "09:00", - "timeError": "Ошибка формата времени, должен быть HH:mm", - "topN": "Показать топ N", - "test": "Тест подключения" - }, - "costAlert": { - "title": "Оповещение о расходах", - "description": "Триггер оповещения при превышении порога квоты потребления пользователя/провайдера", - "enable": "Включить оповещение о расходах", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "webhookTypeWeCom": "WeCom", - "webhookTypeFeishu": "Feishu", - "webhookTypeUnknown": "Неизвестная платформа. Используйте URL вебхука WeCom или Feishu", - "threshold": "Порог оповещения", - "thresholdLabel": "Порог оповещения: {percent}%", - "thresholdHelp": "Оповещение при достижении {percent}% квоты", - "interval": "Интервал проверки (минуты)", - "test": "Тест подключения" - }, - "form": { - "save": "Сохранить настройки", - "saving": "Сохранение...", - "loading": "Загрузка...", - "success": "Настройки уведомлений сохранены и задачи перепланированы", - "saveFailed": "Ошибка сохранения", - "saveError": "Не удалось сохранить настройки", - "loadError": "Не удалось загрузить настройки уведомлений", - "webhookRequired": "Сначала заполните Webhook URL", - "testSuccess": "Тестовое сообщение отправлено", - "testFailed": "Тест не пройден", - "testFailedRetry": "Тест не пройден, попробуйте снова", - "testError": "Ошибка тестирования подключения", - "testNoResult": "Тест пройден, но результат не возвращен" - } - }, - "prices": { - "title": "Прайс-лист", - "description": "Управление конфигурацией платформы и ценами моделей", - "section": { - "title": "Цены моделей", - "description": "Управление ценами AI моделей" - }, - "searchPlaceholder": "Поиск по названию модели...", - "filters": { - "all": "Все", - "local": "Локальные", - "anthropic": "Anthropic", - "openai": "OpenAI", - "vertex": "Vertex" - }, - "badges": { - "local": "Локальная" - }, - "capabilities": { - "assistantPrefill": "Предзаполнение ассистента", - "computerUse": "Использование компьютера", - "functionCalling": "Вызов функций", - "pdfInput": "Ввод PDF", - "promptCaching": "Кэширование промпта", - "reasoning": "Рассуждение", - "responseSchema": "Схема ответа", - "toolChoice": "Выбор инструментов", - "vision": "Зрение", - "statusSupported": "Поддерживается", - "statusUnsupported": "Не поддерживается", - "tooltip": "{label}: {status}" - }, - "sync": { - "button": "Синхронизировать облачный прайс-лист", - "syncing": "Синхронизация...", - "checking": "Проверка конфликтов...", - "successWithChanges": "Обновление прайс-листа: добавлено {added}, обновлено {updated}, без изменений {unchanged}", - "successNoChanges": "Прайс-лист актуален, обновление не требуется", - "failed": "Ошибка синхронизации", - "failedError": "Ошибка синхронизации: {error}", - "failedNoResult": "Прайс-лист обновлен но результат не возвращен", - "noModels": "Цены моделей не найдены", - "partialFailure": "Частичное обновление выполнено, но {failed} моделей не удалось обновить", - "failedModels": "Не удалось обновить модели: {models}", - "skippedConflicts": "Пропущено {count} ручных моделей" - }, - "conflict": { - "title": "Выберите элементы для перезаписи", - "description": "Следующие модели имеют ручные цены. Отмеченные будут перезаписаны ценами LiteLLM, неотмеченные останутся без изменений", - "searchPlaceholder": "Поиск моделей...", - "table": { - "modelName": "Модель", - "manualPrice": "Ручная цена", - "litellmPrice": "Цена LiteLLM", - "action": "Действие" - }, - "viewDiff": "Показать различия", - "diffTitle": "Различия цен", - "diff": { - "field": "Поле", - "manual": "Ручное", - "litellm": "LiteLLM", - "inputPrice": "Цена ввода", - "outputPrice": "Цена вывода", - "imagePrice": "Цена изображения", - "provider": "Поставщик", - "mode": "Тип" - }, - "pagination": { - "showing": "Показано {from}-{to} из {total}" - }, - "selectedCount": "Выбрано {count}/{total} моделей", - "noMatch": "Модели не найдены", - "noConflicts": "Конфликтов нет", - "applyOverwrite": "Применить перезапись", - "applying": "Применение..." - }, - "table": { - "modelName": "Название модели", - "provider": "Поставщик", - "capabilities": "Возможности", - "price": "Цена", - "inputPrice": "Цена ввода ($/M)", - "outputPrice": "Цена вывода ($/M)", - "priceInput": "Ввод", - "priceOutput": "Вывод", - "pricePerRequest": "Запрос", - "cacheReadPrice": "Чтение кэша ($/M)", - "cacheCreationPrice": "Создание кэша ($/M)", - "cache5m": "5m", - "cache1h": "1h+", - "copyModelId": "Скопировать ID модели", - "updatedAt": "Обновлено", - "actions": "Действия", - "typeChat": "Чат", - "typeImage": "Генерация изображений", - "typeCompletion": "Дополнение", - "typeUnknown": "Неизвестно", - "loading": "Загрузка...", - "noMatch": "Соответствующие модели не найдены", - "noDataTitle": "Данные о ценах отсутствуют", - "noDataHint": "Система имеет встроенный прайс-лист. Используйте кнопки выше для синхронизации." - }, - "pagination": { - "showing": "Показано {from}-{to} из {total}", - "previous": "Назад", - "next": "Вперёд", - "perPageLabel": "На странице", - "perPage": "{size} на странице" - }, - "stats": { - "totalModels": "Всего моделей: {count}", - "searchResults": "Результатов поиска: {count}", - "lastUpdated": "Последнее обновление: {time}" - }, - "dialog": { - "title": "Обновить прайс-лист", - "description": "Выберите и загрузите JSON или TOML файл с данными о ценах моделей", - "selectFile": "Нажмите для выбора JSON/TOML или перетащите сюда", - "fileSizeLimit": "Размер файла не может превышать 10MB", - "fileSizeLimitSmall": "Размер файла не превышает 10MB", - "invalidFileType": "Пожалуйста, выберите файл JSON или TOML", - "fileTooLarge": "Размер файла превышает лимит 10MB", - "upload": "Загрузить и обновить", - "uploading": "Загрузка...", - "updatePriceTable": "Обновить прайс-лист", - "updating": "Обновление...", - "selectJson": "Выбрать файл", - "updateSuccess": "Прайс-лист успешно обновлён, {count} моделей обновлено", - "updateFailed": "Ошибка обновления", - "systemHasBuiltIn": "Система имеет встроенный прайс-лист", - "manualDownload": "Вы также можете скачать вручную", - "latestPriceTable": "облачный прайс-лист", - "andUploadViaButton": ", и загрузить через кнопку выше", - "cloudModelCountLoading": "Загрузка количества моделей из облака...", - "cloudModelCountFailed": "Не удалось загрузить количество моделей из облака", - "supportedModels": "Поддерживается {count} моделей", - "results": { - "title": "Результаты обновления", - "total": "Всего: {total} моделей", - "success": "Успешно: {success}", - "failed": "Ошибок: {failed}", - "skipped": "Пропущено: {skipped}", - "more": " (+{count})", - "details": "Подробности", - "viewDetails": "Просмотреть подробный журнал" - } - }, - "addModel": "Добавить модель", - "editModel": "Редактировать модель", - "deleteModel": "Удалить модель", - "addModelDescription": "Вручную добавить новую цену модели", - "editModelDescription": "Редактировать цену модели", - "deleteConfirm": "Удалить модель {name}? Это действие необратимо.", - "form": { - "modelName": "ID модели", - "modelNamePlaceholder": "например: gpt-5.2-codex", - "modelNameRequired": "ID модели обязателен", - "displayName": "Отображаемое имя (необязательно)", - "displayNamePlaceholder": "например: GPT-5.2 Codex", - "type": "Тип", - "provider": "Поставщик", - "providerPlaceholder": "например: openai", - "requestPrice": "Цена за вызов ($/request)", - "inputPrice": "Цена ввода ($/M токенов)", - "outputPrice": "Цена вывода ($/M токенов)", - "outputPriceImage": "Цена вывода ($/изображение)", - "cacheReadPrice": "Цена чтения кэша ($/M токенов)", - "cacheCreationPrice5m": "Цена создания кэша (5m, $/M токенов)", - "cacheCreationPrice1h": "Цена создания кэша (1h+, $/M токенов)" - }, - "drawer": { - "prefillLabel": "Поиск существующих моделей для автозаполнения", - "prefillEmpty": "Модели не найдены", - "prefillFailed": "Ошибка поиска", - "promptCachingHint": "Включайте только если модель поддерживает кэширование, и задайте цены кэша ниже", - "cachePricingTitle": "Цены кэша" - }, - "actions": { - "edit": "Редактировать", - "more": "Больше действий", - "delete": "Удалить" - }, - "toast": { - "createSuccess": "Модель добавлена", - "updateSuccess": "Модель обновлена", - "deleteSuccess": "Модель удалена", - "saveFailed": "Ошибка сохранения", - "deleteFailed": "Ошибка удаления" - } - }, - "providers": { - "add": "Добавить поставщика", - "addFailed": "Ошибка добавления поставщика", - "addProvider": "Добавить провайдера", - "addSuccess": "Поставщик добавлен успешно", - "autoSort": { - "button": "Авто сортировка приоритета", - "dialogTitle": "Автоматическая сортировка приоритета поставщиков", - "dialogDescription": "Автоматически назначить приоритет на основе множителя стоимости (низкая стоимость = высокий приоритет)", - "changeCount": "{count} поставщиков будет обновлено", - "noChanges": "Изменения не требуются (уже отсортировано)", - "costMultiplierHeader": "Множитель стоимости", - "priorityHeader": "Приоритет", - "providersHeader": "Поставщики", - "changesTitle": "Детали изменений", - "providerHeader": "Поставщик", - "priorityChangeHeader": "Изменение приоритета", - "confirm": "Применить изменения", - "success": "Обновлён приоритет для {count} поставщиков", - "error": "Не удалось обновить приоритеты" - }, - "circuitBroken": "Цепь разомкнута", - "clone": "Дублировать поставщика", - "cloneFailed": "Ошибка копирования", - "confirmDelete": "Вы уверены, что хотите удалить этого поставщика?", - "confirmDeleteDesc": "Вы уверены, что хотите удалить провайдера \"{name}\"? Это действие не может быть отменено.", - "confirmDeleteProvider": "Подтвердить удаление провайдера?", - "confirmDeleteProviderDesc": "Вы уверены, что хотите удалить провайдера \"{name}\"? Это действие не может быть восстановлено.", - "createProvider": "Добавить провайдера", - "delete": "Удалить поставщика", - "deleteFailed": "Ошибка удаления поставщика", - "deleteSuccess": "Успешно удалено", - "description": "Настройка поставщиков API и контроль статуса доступности.", - "disabledStatus": "Отключено", - "displayCount": "Показано {filtered} / {total} провайдеров", - "edit": "Редактировать поставщика", - "editFailed": "Ошибка обновления поставщика", - "editProvider": "Редактировать провайдера", - "enabledStatus": "Включено", - "form": { - "proxyTest": { - "fillUrlFirst": "Пожалуйста, сначала заполните URL провайдера", - "testFailed": "Тест не пройден", - "testFailedRetry": "Тест не пройден, попробуйте снова", - "noResult": "Тест успешен, но результат не возвращен", - "connectionSuccess": "Соединение успешно", - "connectionFailed": "Соединение не удалось", - "viaProxy": "(через прокси)", - "viaDirect": "(прямое)", - "responseTime": "Время отклика:", - "statusCode": "Код состояния:", - "connectionMethod": "Способ соединения:", - "proxy": "Прокси", - "direct": "Прямое", - "errorType": "Тип ошибки:", - "testing": "Тестирование...", - "testConnection": "Проверить соединение", - "timeoutError": "Тайм-аут соединения (5 секунд). Проверьте:\n1. Доступен ли прокси-сервер\n2. Правильность адреса и порта прокси\n3. Правильность данных аутентификации прокси", - "proxyError": "Ошибка прокси:", - "networkError": "Сетевая ошибка:" - }, - "apiTest": { - "fillUrlFirst": "Пожалуйста, сначала заполните URL провайдера", - "invalidUrl": "URL провайдера недействителен (только http/https)", - "fillKeyFirst": "Пожалуйста, сначала заполните API ключ", - "testFailed": "Тест не пройден", - "testFailedRetry": "Тест не пройден, попробуйте снова", - "noResult": "Тест успешен, но результат не возвращен", - "testSuccess": "Тест модели успешен", - "testApi": "Тест модели провайдера", - "testing": "Тестирование...", - "apiFormat": "Тип провайдера", - "selectApiFormat": "Выберите тип провайдера для тестирования", - "apiFormatDesc": "По умолчанию синхронизируется с типом провайдера в конфигурации маршрутизации", - "formatAnthropicMessages": "Claude (Anthropic Messages API)", - "formatOpenAIChat": "OpenAI Compatible", - "formatOpenAIResponses": "Codex (Response API)", - "testModel": "Тестовая модель", - "testModelDesc": "Оставьте пустым для использования модели по умолчанию или введите вручную", - "requestConfig": "Конфигурация запроса", - "presetConfig": "Пресет", - "customConfig": "Пользовательский", - "selectPreset": "Выберите шаблон пресета", - "presetDesc": "Шаблоны пресетов содержат аутентичные паттерны CLI-запросов для верификации релейного сервиса", - "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", - "customPayloadDesc": "Введите пользовательский JSON payload для замены тела запроса по умолчанию", - "successContains": "Ключевое слово успеха", - "successContainsPlaceholder": "pong", - "successContainsDesc": "Ответ должен содержать это ключевое слово для признания успешным", - "model": "Модель", - "responseModel": "Модель ответа", - "responseTime": "Время ответа", - "usage": "Использование токенов", - "response": "Предварительный просмотр ответа", - "error": "Сообщение об ошибке", - "unknown": "Неизвестно", - "viewDetails": "Подробнее", - "copySuccess": "Скопировано в буфер обмена", - "copyFailed": "Не удалось скопировать", - "copyResult": "Копировать результат", - "close": "Закрыть", - "success": "Успешно", - "failed": "Не удалось", - "streamInfo": "Информация о потоковом ответе", - "chunksReceived": "Полученные чанки", - "streamFormat": "Формат потока", - "streamResponse": "Потоковый ответ", - "chunksCount": "Получено {count} чанков ({format})", - "truncatedPreview": "Показаны первые {length} символов, скопируйте для просмотра полного текста", - "truncatedBrief": "Показаны первые {length} символов, нажмите «Подробнее» для полного просмотра", - "timeout": { - "label": "Таймаут (секунды)", - "desc": "Максимальное время ожидания тестового запроса (5-120 сек)", - "geminiHint": ", для моделей Gemini Thinking рекомендуется 60+ сек" - }, - "geminiAuthFallback": { - "warning": "Ошибка аутентификации через заголовок, использована URL-аутентификация", - "desc": "Реальный прокси использует только аутентификацию через заголовок, что может вызвать ошибки" - }, - "copyFormat": { - "testResult": "Результат теста", - "message": "Сообщение", - "errorDetails": "Детали ошибки" - }, - "disclaimer": { - "title": "Внимание", - "realRequest": "Этот тест отправляет реальный запрос провайдеру и может потреблять небольшую квоту", - "resultReference": "Результаты могут варьироваться в зависимости от провайдера и служат только для справки", - "confirmConfig": "Пожалуйста, проверьте URL провайдера, API ключ и конфигурацию модели" - }, - "resultCard": { - "status": { - "green": "Доступен", - "yellow": "Нестабильно", - "red": "Недоступен" - }, - "dialogTitle": "Детали теста провайдера", - "validation": { - "title": "Детали трехуровневой валидации", - "http": { - "title": "Уровень 1: HTTP статус", - "statusCode": "Код статуса", - "passed": "2xx/3xx успех", - "failed": "4xx/5xx ошибка" - }, - "latency": { - "title": "Уровень 2: Порог задержки", - "actual": "Фактическая задержка", - "passed": "В пределах порога", - "failed": "Превышен порог" - }, - "content": { - "title": "Уровень 3: Валидация контента", - "target": "Цель", - "passed": "Содержит целевую строку", - "failed": "Цель не найдена" - }, - "passed": "Пройдено", - "failed": "Не пройдено", - "timeout": "Тайм-аут" - }, - "labels": { - "http": "HTTP", - "latency": "Задержка", - "content": "Контент", - "model": "Модель", - "firstByte": "1 байт", - "totalLatency": "Общая задержка", - "error": "Ошибка", - "responsePreview": "Предпросмотр ответа" - }, - "timing": { - "title": "Информация о времени", - "totalLatency": "Общая задержка", - "firstByte": "1 байт", - "testedAt": "Время теста" - }, - "tokenUsage": { - "title": "Использование токенов", - "input": "Ввод", - "output": "Вывод", - "cacheCreation": "Создание кэша", - "cacheRead": "Чтение кэша" - }, - "streamInfo": { - "title": "Информация о потоковом ответе", - "isStreaming": "Потоковая передача", - "chunksCount": "Количество чанков", - "yes": "Да", - "no": "Нет" - }, - "rawResponse": { - "title": "Тело ответа", - "hint": "Здесь отображается необработанное содержимое ответа. Вы можете проверить, содержит ли ответ ключевое слово." - }, - "errorDetails": { - "title": "Детали ошибки", - "type": "Тип ошибки" - }, - "copyText": { - "status": "Статус", - "message": "Сообщение", - "latency": "Задержка", - "httpStatus": "HTTP статус", - "model": "Модель", - "usage": "Использование", - "inputOutput": "Ввод {input} / Вывод {output} токенов", - "response": "Ответ", - "error": "Ошибка", - "testedAt": "Время теста", - "validationDetails": "Детали валидации", - "httpCheck": "Проверка HTTP", - "latencyCheck": "Проверка задержки", - "contentCheck": "Проверка контента" - }, - "judgment": "Решение" - } - }, - "urlPreview": { - "title": "Предварительный просмотр URL", - "invalidUrl": "Неверный формат URL", - "invalidUrlDesc": "Пожалуйста, введите действительный HTTP/HTTPS адрес", - "duplicatePath": "Обнаружен дублирующийся путь", - "copy": "Копировать", - "copySuccess": "Скопировано {name} в буфер обмена", - "copyFailed": "Не удалось скопировать" - }, - "modelSelect": { - "allowAllModels": "Разрешить все модели {type}", - "selectedCount": "Выбрано моделей: {count}", - "searchPlaceholder": "Поиск по названию модели...", - "loading": "Загрузка...", - "notFound": "Модели не найдены", - "selectAll": "Выбрать все ({count})", - "clear": "Очистить", - "manualAdd": "Добавить модель вручную", - "manualPlaceholder": "Введите название модели (например, gpt-5-turbo)", - "manualDesc": "Поддержка добавления любого названия модели (не ограничено прайс-листом)", - "claude": "Claude", - "openai": "OpenAI", - "gemini": "Gemini", - "sourceUpstream": "Upstream", - "sourceUpstreamDesc": "Список моделей из API провайдера", - "sourceFallback": "Локально", - "sourceFallbackDesc": "Используется локальный прайс-лист (upstream недоступен или не поддерживается)", - "refresh": "Обновить список моделей" - }, - "modelRedirect": { - "currentRules": "Текущие правила ({count})", - "addNewRule": "Добавить новое правило", - "sourceModel": "Запрашиваемая пользователем модель", - "targetModel": "Фактически перенаправляемая модель", - "sourcePlaceholder": "Например: claude-sonnet-4-5-20250929", - "targetPlaceholder": "Например: glm-4.6", - "add": "Добавить", - "sourceEmpty": "Название исходной модели не может быть пустым", - "targetEmpty": "Название целевой модели не может быть пустым", - "alreadyExists": "Правило перенаправления для модели \"{model}\" уже существует", - "description": "Перенаправлять запросы моделей от клиента Claude Code (например, claude-sonnet-4.5) к моделям, поддерживаемым вышестоящим провайдером (например, glm-4.6, gemini-pro). Используется для оптимизации затрат или подключения сторонних AI-сервисов.", - "emptyState": "Пока нет правил перенаправления. После добавления правил система автоматически переписывает названия моделей в запросах." - }, - "addRedirect": "Добавить переправку", - "allowAllModels": "✓ Разрешить все модели (рекомендуется)", - "apiAddress": "Адрес API", - "apiAddressPlaceholder": "Например: https://open.bigmodel.cn/api/anthropic", - "apiAddressRequired": "Адрес API *", - "apiKey": "API ключ", - "apiKeyCurrent": "Текущий ключ:", - "apiKeyLeaveEmpty": "(оставьте пустым, чтобы не изменять)", - "apiKeyLeaveEmptyDesc": "Оставьте пустым, чтобы не изменять ключ", - "apiKeyOptional": "Оставьте пустым, чтобы оставить текущий ключ", - "apiKeyPlaceholder": "Введите API ключ", - "apiKeyRequired": "API ключ *", - "baseUrl": "Базовый URL", - "baseUrlPlaceholder": "например: https://open.bigmodel.cn/api/anthropic", - "baseUrlRequired": "Пожалуйста, сначала заполните URL поставщика", - "circuitBreakerConfig": "Конфигурация автоматического выключателя", - "circuitBreakerConfigSummary": "{failureThreshold} сбоев / {openDuration} мин. размыкания / {successThreshold} успехов для восстановления / максимум {maxRetryAttempts} попыток на провайдера", - "circuitBreakerDesc": "Автоматическое размыкание при последовательных сбоях провайдера для предотвращения влияния на общее качество сервиса", - "clearSearch": "Очистить поиск", - "codexInstructions": "Политика инструкций Codex", - "codexInstructionsAuto": "Автоматически (рекомендуется)", - "codexInstructionsDesc": "(определяет политику планирования)", - "codexInstructionsForce": "Принудительно официальные", - "codexInstructionsKeep": "Сохранить оригинал", - "codexStrategyAutoDesc": "Передавать instructions клиента, автоматически повторять с официальным prompt при ошибке 400", - "codexStrategyAutoLabel": "Автоматически (рекомендуется)", - "codexStrategyConfig": "Стратегия Codex Instructions", - "codexStrategyConfigAuto": "Автоматически (рекомендуется)", - "codexStrategyConfigForce": "Принудительно официальные", - "codexStrategyConfigKeep": "Передавать как есть", - "codexStrategyDesc": "Управляет обработкой поля instructions в запросах Codex, влияет на совместимость с вышестоящими узлами", - "codexStrategyForceDesc": "Всегда использовать официальные Codex CLI instructions (около 4000+ символов)", - "codexStrategyForceLabel": "Принудительно официальные", - "codexStrategyHint": "Подсказка: некоторые строгие узлы Codex (например, 88code, foxcode) требуют официальные instructions, выберите стратегию \"Автоматически\" или \"Принудительно официальные\"", - "codexStrategyKeepDesc": "Всегда передавать instructions клиента без автоматического повтора (подходит для гибких узлов)", - "codexStrategyKeepLabel": "Передавать как есть", - "codexStrategySelect": "Выбор стратегии", - "collapseAll": "Свернуть все расширенные настройки", - "confirmAdd": "Подтвердить добавление", - "confirmAddPending": "Добавление...", - "confirmUpdate": "Подтвердить обновление", - "confirmUpdatePending": "Обновление...", - "costMultiplier": "Коэф цены", - "costMultiplierDesc": "например: A (стоимость 1.0x), C (стоимость 0.8x)", - "costMultiplierLabel": "Коэффициент стоимости", - "costMultiplierPlaceholder": "1.0", - "deleteButton": "Удалить", - "enabled": "Включено", - "expandAll": "Развернуть все расширенные настройки", - "failureThreshold": "Порог сбоев (раз)", - "failureThresholdDesc": "Сколько последовательных сбоев до размыкания", - "failureThresholdPlaceholder": "5", - "filterAllProviders": "Все поставщики", - "filterByType": "Фильтр по типу", - "filterProvider": "Фильтр типа поставщика", - "group": "Группа", - "groupPlaceholder": "например: premium, economy", - "joinClaudePool": "Присоединиться к пулу планирования Claude", - "joinClaudePoolDesc": "При включении этот поставщик будет участвовать в балансировке нагрузки вместе с поставщиками типа Claude", - "joinClaudePoolHelp": "Доступно только при наличии перенаправлений на модели claude-* в конфигурации. При включении этот поставщик также будет участвовать в выборе при запросах моделей claude-*.", - "leaveEmpty": "Оставьте пустым для неограниченного доступа", - "limit0Means": "0 означает без ограничений", - "limit5hLabel": "Лимит расходов за 5 часов (USD)", - "limitAmount5h": "Лимит расходов за 5 часов (USD)", - "limitAmount5hDesc": "например: Поставщик B имеет лимит $10, уже потрачено $9.8", - "limitAmountMonthly": "Месячный лимит расходов (USD)", - "limitAmountWeekly": "Недельный лимит расходов (USD)", - "limitConcurrent": "Лимит параллельных сеансов", - "limitConcurrentDesc": "например: Поставщик C имеет лимит 2, в данный момент 2 активных сеанса", - "limitConcurrentLabel": "Лимит одновременных сеансов", - "limitMonthlyLabel": "Месячный лимит расходов (USD)", - "limitPlaceholder0": "0 означает без ограничений", - "limitPlaceholderUnlimited": "Оставьте пустым для неограниченного доступа", - "limitWeeklyLabel": "Недельный лимит расходов (USD)", - "modelRedirects": "Перенаправление моделей", - "modelRedirectsAddNew": "Добавить новое правило", - "modelRedirectsCurrentRules": "Текущие правила ({count})", - "modelRedirectsDesc": "Переправить запросы Claude к другим поддерживаемым моделям", - "modelRedirectsEmpty": "Нет правил перенаправления. После добавления правил система автоматически перезапишет имена моделей в запросах.", - "modelRedirectsExists": "Правило перенаправления для модели \"{model}\" уже существует", - "modelRedirectsLabel": "Конфигурация перенаправления моделей", - "modelRedirectsOptional": "(необязательно)", - "modelRedirectsSourceModel": "Модель запроса пользователя", - "modelRedirectsSourcePlaceholder": "например: claude-sonnet-4-5-20250929", - "modelRedirectsSourceRequired": "Имя исходной модели не может быть пустым", - "modelRedirectsTargetModel": "Фактически перенаправляемая модель", - "modelRedirectsTargetPlaceholder": "например: glm-4.6", - "modelRedirectsTargetRequired": "Имя целевой модели не может быть пустым", - "modelWhitelist": "Белый список моделей", - "modelWhitelistAllowAll": "Разрешить все модели {type}", - "modelWhitelistAllowAllClause": "Разрешить все модели Claude", - "modelWhitelistAllowAllOpenAI": "Разрешить все модели OpenAI", - "modelWhitelistClear": "Очистить", - "modelWhitelistDesc": "Ограничить модели, которые может обрабатывать этот поставщик. По умолчанию поставщик может обрабатывать все модели этого типа.", - "modelWhitelistLabel": "Разрешенные модели", - "modelWhitelistLoading": "Загрузка...", - "modelWhitelistManualAdd": "Добавить модель вручную", - "modelWhitelistManualDesc": "Поддерживает добавление любого имени модели (не ограничено прайс-листом)", - "modelWhitelistManualPlaceholder": "Введите имя модели (например, gpt-5-turbo)", - "modelWhitelistNotFound": "Модели не найдены", - "modelWhitelistSearchPlaceholder": "Поиск по имени модели...", - "modelWhitelistSelectAll": "Выбрать все ({count})", - "modelWhitelistSelected": "Выбрано {count} моделей", - "modelWhitelistSelectedOnly": "Разрешены только выбранные {count} моделей. Запросы других моделей не будут направлены к этому поставщику.", - "name": { - "label": "Имя провайдера *", - "placeholder": "например: Zhipu" - }, - "namePlaceholder": "Введите имя поставщика", - "openDuration": "Длительность размыкания (минуты)", - "openDurationDesc": "Время автоматического перехода в полуоткрытое состояние после размыкания", - "openDurationPlaceholder": "30", - "priority": "Приоритет", - "priorityDesc": "В пределах одного приоритета сортировка по множителю стоимости от низкого к высокому", - "priorityLabel": "Приоритет", - "priorityPlaceholder": "0", - "providerGroupDesc": "Метка группы поставщика. Пользователь может использовать этого поставщика только если его providerGroup совпадает с этим значением. Пример: установка \"premium\" означает использование только пользователями с providerGroup=\"premium\"", - "providerGroupLabel": "Группа поставщика", - "providerGroupPlaceholder": "например: premium, economy", - "providerName": "Имя поставщика", - "providerNamePlaceholder": "например: Zhipu", - "providerNameRequired": "Имя поставщика *", - "providerType": "Тип поставщика", - "providerTypeDesc": "Выберите тип формата API поставщика.", - "providerTypeDisabledNote": "Примечание: функции типов Gemini CLI и OpenAI Compatible находятся в разработке и временно недоступны", - "proxy": "Прокси", - "proxyAddressFormats": "Поддерживаемые форматы:", - "proxyAddressLabel": "Адрес прокси", - "proxyAddressOptional": "(необязательно)", - "proxyAddressPlaceholder": "например: http://proxy.example.com:8080 или socks5://127.0.0.1:1080", - "proxyConfig": "Конфигурация прокси", - "proxyConfigDesc": "Настройка прокси-сервера для улучшения подключения к поставщику (поддерживает HTTP, HTTPS, SOCKS4, SOCKS5)", - "proxyConfigNone": "Не настроен", - "proxyConfigSummary": "Прокси настроен", - "proxyConfigSummaryFallback": " (откат включен)", - "proxyConfigured": "Прокси настроен", - "proxyFallback": "Откат при ошибке прокси", - "proxyFallbackDesc": "Перейти на прямое соединение при ошибке прокси", - "proxyFallbackLabel": "Откат на прямое соединение при ошибке прокси", - "proxyNotConfigured": "Не настроен", - "proxyTestButton": "Проверить соединение", - "proxyTestDesc": "Тестирование доступа к URL поставщика через настроенный прокси (использует HEAD запрос, не расходует квоту)", - "proxyTestFailed": "Соединение не удалось", - "proxyTestFillUrl": "Пожалуйста, сначала заполните URL поставщика", - "proxyTestLabel": "Тест соединения", - "proxyTestNetworkError": "Сетевая ошибка: {error}", - "proxyTestProxyError": "Ошибка прокси: {error}", - "proxyTestResponseTime": "Время отклика: {time}", - "proxyTestResultConnectionMethod": "Способ соединения: {via}", - "proxyTestResultConnectionMethodDirect": "Прямое", - "proxyTestResultConnectionMethodProxy": "Прокси", - "proxyTestResultErrorType": "Тип ошибки: {type}", - "proxyTestResultFailed": "Соединение не удалось", - "proxyTestResultMessage": "{message}", - "proxyTestResultResponseTime": "Время отклика: {time}мс", - "proxyTestResultStatusCode": "Код статуса: {code}", - "proxyTestResultSuccess": "Соединение успешно {via}", - "proxyTestStatusCode": "| Код статуса: {code}", - "proxyTestSuccess": "Соединение успешно", - "proxyTestTesting": "Тестирование...", - "proxyTestTimeout": "Тайм-аут соединения (5 секунд). Проверьте:\n1. Доступен ли прокси-сервер\n2. Правильность адреса и порта прокси\n3. Правильность данных аутентификации прокси", - "proxyTestViaDirect": "(прямое)", - "proxyTestViaProxy": "(через прокси)", - "proxyUrl": "Адрес прокси", - "proxyUrlPlaceholder": "например: http://proxy.example.com:8080 или socks5://127.0.0.1:1080", - "rateLimitConfig": "Конфигурация ограничения скорости", - "rateLimitConfigNone": "Без ограничений", - "rateLimitConfigSummary": "5ч: ${fiveHour}, Неделя: ${weekly}, Месяц: ${monthly}, Одновременно: {concurrent}", - "remark": "Примечание", - "remarkPlaceholder": "Необязательно: добавить примечание...", - "removeRedirect": "Удалить переправку", - "routingConfig": "Конфигурация маршрутизации", - "routingConfigNone": "Не настроено", - "routingConfigSummary": "{models} белый список моделей, {redirects} перенаправлений", - "scheduleParams": "Параметры планирования", - "searchClear": "Очистить поиск", - "searchPlaceholder": "Поиск по имени, URL, примечанию...", - "selectProviderType": "Выбрать тип поставщика", - "sort": "Сортировать поставщиков", - "sortByCost": "По стоимости", - "sortByCreated": "По дате создания (новое-старое)", - "sortByName": "По имени (A-Z)", - "sortByPriority": "По приоритету (высокое-низкое)", - "sortByWeight": "По весу (высокое-низкое)", - "sourceModel": "Исходная модель", - "sourceModelPlaceholder": "например: claude-sonnet-4-5-20250929", - "sourceModelRequired": "Имя исходной модели не может быть пустым", - "successThreshold": "Порог восстановления (раз)", - "successThresholdDesc": "Количество успешных попыток в полуоткрытом состоянии для полного восстановления", - "successThresholdPlaceholder": "2", - "targetModel": "Целевая модель", - "targetModelPlaceholder": "например: glm-4.6", - "targetModelRequired": "Имя целевой модели не может быть пустым", - "testProxy": "Проверить соединение", - "testProxyFailed": "Ошибка тестирования прокси", - "testProxyFailedError": "Ошибка проверки соединения:", - "testProxySuccess": "Соединение прокси успешно", - "validUrlRequired": "Пожалуйста, введите действительный адрес API", - "websiteUrl": { - "label": "Сайт провайдера", - "placeholder": "https://example.com", - "desc": "Официальный сайт провайдера для быстрого доступа" - }, - "websiteUrlDesc": "Адрес официального сайта поставщика для быстрого перехода к управлению", - "websiteUrlInvalid": "Пожалуйста, введите действительный адрес официального сайта поставщика", - "websiteUrlPlaceholder": "https://example.com", - "weight": "Вес", - "weightDesc": "Взвешенная случайная вероятность. В пределах одного приоритета большее число означает выше вероятность выбора.", - "weightLabel": "Вес", - "weightPlaceholder": "1", - "title": { - "create": "Добавить провайдера", - "edit": "Редактировать провайдера" - }, - "dialogDescription": "Настройте детали провайдера и расширенные параметры.", - "url": { - "label": "Адрес API *", - "placeholder": "например: https://open.bigmodel.cn/api/anthropic" - }, - "key": { - "label": "API ключ", - "leaveEmpty": "(Оставьте пустым, чтобы не менять)", - "placeholder": "Введите API ключ", - "leaveEmptyDesc": "Пустое значение — без изменений", - "currentKey": "Текущий ключ: {key}" - }, - "buttons": { - "expandAll": "Развернуть все расширенные настройки", - "collapseAll": "Свернуть все расширенные настройки", - "submit": "Подтвердить добавление", - "submitting": "Добавление...", - "update": "Подтвердить обновление", - "updating": "Обновление...", - "delete": "Удалить" - }, - "common": { - "core": "Основная" - }, - "sections": { - "routing": { - "title": "Маршрутизация", - "summary": { - "models": "{count} моделей в белом списке", - "redirects": "{count} правил перенаправления", - "none": "Не настроено" - }, - "providerType": { - "label": "Тип провайдера", - "desc": "(определяет политику выбора)", - "placeholder": "Выберите тип провайдера" - }, - "providerTypeDesc": "Выберите формат API провайдера.", - "providerTypeDisabledNote": "Примечание: функции типа OpenAI Compatible находятся в разработке и пока недоступны", - "modelRedirects": { - "label": "Перенаправление моделей", - "optional": "(необязательно)" - }, - "joinClaudePool": { - "label": "Включить пул маршрутизации Claude", - "desc": "При включении провайдер участвует в балансировке нагрузки вместе с провайдерами типа Claude", - "help": "Доступно только при наличии перенаправления на модели claude-*. При запросе моделей claude-* провайдер также участвует в выборе." - }, - "preserveClientIp": { - "label": "Пробрасывать IP клиента", - "desc": "Передавать x-forwarded-for / x-real-ip в апстрим (может раскрыть реальный IP клиента)", - "help": "По умолчанию выключено для приватности. Включайте только если апстриму нужен IP пользователя." - }, - "modelWhitelist": { - "title": "Список разрешённых моделей", - "desc": "Ограничьте модели, которые может обслуживать провайдер. По умолчанию доступны все модели данного типа.", - "label": "Разрешённые модели", - "optional": "(необязательно)", - "allowAll": "✓ Разрешить все модели (рекомендуется)", - "selectedOnly": "Разрешены только выбранные {count} моделей. Другие модели не будут направляться к этому провайдеру.", - "moreModels": "+{count} ещё" - }, - "scheduleParams": { - "title": "Параметры выбора", - "priority": { - "label": "Приоритет", - "placeholder": "0", - "desc": "Меньше — выше приоритет (0 — наивысший). Система выбирает только из провайдеров с максимальным приоритетом. Рекомендации: основной=0, резерв=1, аварийный=2" - }, - "weight": { - "label": "Вес", - "placeholder": "1", - "desc": "Взвешенное случайное распределение. В пределах одного приоритета больший вес увеличивает вероятность выбора. Пример 1:2:3 ≈ 16%:33%:50%" - }, - "costMultiplier": { - "label": "Множитель стоимости", - "placeholder": "1.0", - "desc": "Множитель при расчёте стоимости. Официальный=1.0, дешевле на 20%=0.8, дороже на 20%=1.2 (до 4 знаков после запятой)" - }, - "group": { - "label": "Группа провайдера", - "placeholder": "например: premium, economy", - "desc": "Метка группы. Пользователь может использовать провайдера только если его providerGroup совпадает. Пример: значение \"premium\" — только для пользователей с providerGroup=\"premium\"" - } - }, - "cacheTtl": { - "label": "Переопределение Cache TTL", - "options": { - "inherit": "Не переопределять (следовать клиенту)", - "5m": "5 минут", - "1h": "1 час" - }, - "desc": "Принудительно задать TTL кэша промптов; влияет только на запросы с cache_control." - }, - "context1m": { - "label": "Контекстное окно 1M", - "options": { - "inherit": "Наследовать (следовать клиенту)", - "forceEnable": "Принудительно включить", - "disabled": "Отключено" - }, - "desc": "Настройка поддержки контекстного окна 1M. Применяется только к моделям Sonnet (claude-sonnet-4-5, claude-sonnet-4). При включении применяется многоуровневая тарификация." - }, - "codexOverrides": { - "reasoningEffort": { - "label": "Переопределение уровня рассуждений", - "help": "Управляет тем, сколько усилий модель тратит на рассуждения перед ответом. \"inherit\" следует запросу клиента, остальные значения принудительно задают reasoning.effort. Примечание: \"none\" поддерживается только моделями GPT-5.1, а \"xhigh\" — только GPT-5.1-Codex-Max. Неподдерживаемые значения приведут к ошибке у апстрима.", - "options": { - "inherit": "Не переопределять (следовать клиенту)", - "minimal": "minimal", - "low": "low", - "medium": "medium (по умолчанию)", - "high": "high", - "xhigh": "xhigh (только GPT-5.1-Codex-Max)", - "none": "none (только GPT-5.1)" - } - }, - "reasoningSummary": { - "label": "Переопределение сводки рассуждений", - "help": "Управляет тем, возвращает ли Responses API сводку рассуждений. auto — кратко, detailed — подробнее. \"inherit\" следует запросу клиента.", - "options": { - "inherit": "Не переопределять (следовать клиенту)", - "auto": "auto", - "detailed": "detailed" - } - }, - "textVerbosity": { - "label": "Переопределение подробности текста", - "help": "Управляет подробностью ответа. low — короче, high — подробнее. \"inherit\" следует запросу клиента.", - "options": { - "inherit": "Не переопределять (следовать клиенту)", - "low": "low", - "medium": "medium (по умолчанию)", - "high": "high" - } - }, - "parallelToolCalls": { - "label": "Переопределение параллельных tool calls", - "help": "Управляет тем, разрешены ли параллельные вызовы инструментов. \"inherit\" следует запросу клиента. Отключение может снизить параллельность вызовов инструментов.", - "options": { - "inherit": "Не переопределять (следовать клиенту)", - "true": "Принудительно включить", - "false": "Принудительно отключить" - } - } - } - }, - "rateLimit": { - "title": "Ограничения", - "summary": { - "fiveHour": "5ч: ${amount}", - "weekly": "Неделя: ${amount}", - "monthly": "Месяц: ${amount}", - "total": "Всего: ${amount}", - "concurrent": "Параллельно: {count}", - "none": "Без ограничений" - }, - "limit5h": { - "label": "Лимит за 5 часов (USD)", - "placeholder": "Пусто — без ограничений" - }, - "limitWeekly": { - "label": "Недельный лимит (USD)", - "placeholder": "Пусто — без ограничений" - }, - "limitMonthly": { - "label": "Месячный лимит (USD)", - "placeholder": "Пусто — без ограничений" - }, - "limitTotal": { - "label": "Общий лимит (USD)", - "placeholder": "Пусто — без ограничений" - }, - "limitConcurrent": { - "label": "Лимит параллельных сессий", - "placeholder": "0 — без ограничений" - } - }, - "circuitBreaker": { - "title": "Предохранитель", - "summary": "{failureThreshold} неудач / {openDuration} мин. блокировки / {successThreshold} успеха для восстановления / до {maxRetryAttempts} попыток на провайдера", - "desc": "Автоматическое отключение при серии неудач для защиты качества сервиса", - "failureThreshold": { - "label": "Порог неудач", - "placeholder": "5", - "desc": "Сколько подряд неудач для срабатывания" - }, - "openDuration": { - "label": "Длительность блокировки (мин)", - "placeholder": "30", - "desc": "Через сколько перейти в полураскрытое состояние" - }, - "successThreshold": { - "label": "Порог восстановления", - "placeholder": "2", - "desc": "Сколько успешных запросов в полураскрытом режиме для полного восстановления" - }, - "maxRetryAttempts": { - "label": "Максимум попыток на провайдера", - "placeholder": "2", - "desc": "Общее число попыток (включая первую) перед переключением на другого провайдера. Оставьте пустым для значения по умолчанию." - } - }, - "proxy": { - "title": "Прокси", - "summary": { - "configured": "Прокси настроен", - "fallback": " (включён откат)", - "none": "Не настроено" - }, - "desc": "Настройте прокси для улучшения соединения (поддержка HTTP, HTTPS, SOCKS4, SOCKS5)", - "url": { - "label": "URL прокси", - "optional": "(необязательно)", - "placeholder": "например: http://proxy.example.com:8080 или socks5://127.0.0.1:1080", - "formats": "Поддерживаемые форматы:" - }, - "fallback": { - "label": "Откат к прямому соединению при сбое прокси", - "desc": "При включении будет предпринята попытка прямого соединения при сбое прокси" - }, - "test": { - "label": "Проверка соединения", - "desc": "Проверка доступа к URL провайдера через прокси (запрос HEAD, без списания средств)" - } - }, - "timeout": { - "title": "Конфигурация тайм-аута", - "summary": "1 байт: {streaming}с | поток: {idle}с | не поток: {nonStreaming}с", - "desc": "Установить время ожидания запроса, 0 означает отключение тайм-аута", - "streamingFirstByte": { - "label": "Тайм-аут первого байта потока (секунды)", - "placeholder": "30", - "desc": "Тайм-аут первого байта потоковой передачи, диапазон 1-120 секунд, значение по умолчанию 30 секунд", - "core": "true" - }, - "streamingIdle": { - "label": "Тайм-аут простоя потока (секунды)", - "placeholder": "60", - "desc": "Тайм-аут простоя потоковой передачи, диапазон 60-600 секунд, введите 0 для отключения (предотвращение застревания)", - "core": "true" - }, - "nonStreamingTotal": { - "label": "Полный тайм-аут непотоковой передачи (секунды)", - "placeholder": "600", - "desc": "Полный тайм-аут непотоковой передачи, диапазон 60-1200 секунд, значение по умолчанию 600 секунд (10 минут)", - "core": "true" - }, - "disableHint": "Установите 0 для отключения тайм-аута (только для сценариев отката канарейки, не рекомендуется)" - }, - "codexStrategy": { - "title": "Политика Codex Instructions", - "summary": { - "auto": "Авто (рекомендуется)", - "force": "Только официальные", - "keep": "Как есть" - }, - "desc": "Управление полем instructions в запросах Codex; влияет на совместимость с шлюзами", - "select": { - "label": "Выбор стратегии", - "placeholder": "Выберите стратегию", - "auto": { - "label": "Авто (рекомендуется)", - "desc": "Передавать инструкции клиента; при 400 повтор с официальным промптом" - }, - "force": { - "label": "Только официальные", - "desc": "Всегда использовать официальные инструкции Codex CLI (~4000+ символов)" - }, - "keep": { - "label": "Как есть", - "desc": "Всегда передавать инструкции клиента без автоповторной попытки (для более лояльных прокси)" - } - }, - "hint": "Подсказка: некоторым строгим шлюзам Codex (например, 88code, foxcode) требуются официальные инструкции. Выберите «Авто» или «Только официальные»." - }, - "mcpPassthrough": { - "title": "Конфигурация сквозной передачи MCP", - "summary": { - "none": "Отключено", - "minimax": "Minimax", - "glm": "GLM", - "custom": "Пользовательский (Зарезервировано)" - }, - "desc": "При включении передаёт вызовы инструментов MCP указанному AI-провайдеру (например, minimax для распознавания изображений, веб-поиска)", - "select": { - "label": "Тип сквозной передачи", - "none": { - "label": "Отключено", - "desc": "Не включать сквозную передачу MCP (по умолчанию)" - }, - "minimax": { - "label": "Minimax", - "desc": "Сквозная передача в сервис minimax MCP (поддержка распознавания изображений, веб-поиска и т.д.)" - }, - "glm": { - "label": "GLM", - "desc": "Сквозная передача в сервис GLM MCP (поддержка анализа изображений, видео и т.д.)" - }, - "custom": { - "label": "Пользовательский", - "desc": "Сквозная передача в пользовательский сервис MCP (зарезервировано, не реализовано)" - }, - "placeholder": "Выберите тип сквозной передачи" - }, - "hint": "Подсказка: сквозная передача MCP позволяет клиенту Claude Code использовать возможности инструментов, предоставляемых сторонними AI-провайдерами (например, распознавание изображений, веб-поиск)", - "urlLabel": "URL сквозной передачи MCP", - "urlPlaceholder": "https://api.minimaxi.com", - "urlDesc": "Базовый URL сервиса MCP. Оставьте пустым для автоматического извлечения из URL провайдера", - "urlAuto": "Автоматически извлечено: {url}" - } - }, - "providerTypes": { - "claude": "Claude (Anthropic Messages API)", - "claudeAuth": "Claude (Anthropic Auth Token)", - "codex": "Codex (Response API)", - "gemini": "Gemini (Google Gemini API)", - "geminiCli": "Gemini CLI", - "geminiCliDisabled": "", - "openaiCompatible": "OpenAI Compatible", - "openaiCompatibleDisabled": " - в разработке" - }, - "deleteDialog": { - "title": "Удалить провайдера", - "description": "Удалить провайдера «{name}»? Это действие необратимо.", - "cancel": "Отмена", - "confirm": "Подтвердить удаление" - }, - "failureThresholdConfirmDialog": { - "title": "Подтвердите особую конфигурацию", - "descriptionDisabledPrefix": "Вы устанавливаете порог сбоев автоматического выключателя на ", - "descriptionDisabledValue": "0", - "descriptionDisabledMiddle": ", что означает ", - "descriptionDisabledAction": "отключение автоматического выключателя", - "descriptionDisabledSuffix": ". Провайдер не будет отключаться из-за последовательных сбоев.", - "descriptionHighValuePrefix": "Вы устанавливаете порог сбоев автоматического выключателя на ", - "descriptionHighValueSuffix": ", что является высоким значением и может привести к отключению провайдера только после многочисленных сбоев.", - "confirmQuestion": "Вы уверены, что хотите сохранить эту конфигурацию?", - "cancel": "Отмена", - "confirm": "Подтвердить сохранение" - }, - "errors": { - "invalidUrl": "Введите корректный адрес API", - "invalidWebsiteUrl": "Введите корректный адрес сайта провайдера", - "groupTagTooLong": "Список групп провайдера слишком длинный (макс. {max} символов всего)", - "addFailed": "Не удалось добавить провайдера", - "updateFailed": "Не удалось обновить провайдера", - "deleteFailed": "Не удалось удалить провайдера" - }, - "success": { - "created": "Провайдер успешно добавлен", - "createdDesc": "Провайдер «{name}» добавлен" - } - }, - "guide": { - "after": "После фильтрации:", - "before": "До фильтрации:", - "bestPracticesConcurrent": "• Контроль параллелизма: установите количество одновременных сеансов на основе ограничений API поставщика", - "bestPracticesCost": "• Множитель стоимости: официальный множитель = 1.0, собственный сервис можно установить 0.8-1.2", - "bestPracticesLimit": "• Настройка лимитов: установите лимиты на 5 часов, 7 дней, 30 дней в соответствии с бюджетом", - "bestPracticesPriority": "• Настройка приоритета: основные поставщики = 0, резервные = 1-3", - "bestPracticesTitle": "Рекомендации по лучшим практикам", - "bestPracticesWeight": "• Настройка веса: установите вес в соответствии с емкостью поставщика (большая емкость = больший вес)", - "circuitBreaker": "Проверка автоматического выключателя", - "circuitBreakerOpen": "A отфильтрован, осталось: B, C, D", - "circuitBreakerRecovery": "A автоматически восстанавливается в полуоткрытое после 60 секунд", - "circuitBreakerRecovery5h": "Автоматическое восстановление после 5-часового скользящего окна", - "costOptimize": "2️⃣ Оптимизация стоимости: в пределах одного приоритета поставщики с низким множителем стоимости имеют более высокую вероятность", - "costSort": "Откат по стоимости", - "costSortExample": "Все поставщики: A (default), B (premium), C (premium), D (economy)", - "costSortProb": "Более дешевый C имеет более высокую вероятность выбора", - "costSortResult": "После сортировки: C (0.8x), A (1.0x)", - "decision": "Решение:", - "group": "Фильтрация групп пользователей", - "groupDesc": "Если пользователь указал группу поставщиков, система приоритизирует выбор из этой группы", - "groupDowngrade": "Записать предупреждение и выбрать из глобального пула", - "groupExample": "Пользователь настроил providerGroup = 'premium'", - "groupFallback": "Если в группе нет доступных поставщиков, откат на всех поставщиков", - "groupFiltered": "Выбрать только из A и C, B и D отфильтрованы", - "groupUnavailable": "Все поставщики в группе 'vip' отключены или превышены", - "health": "Фильтрация здоровья (автоматический выключатель + ограничение)", - "healthCheck": "Проверить, включен ли B и работоспособен", - "healthCheckAmountLimit": "Проверить превышение лимитов (5ч, 7д, 30д)", - "healthCheckAmountLimitExample": "Лимит поставщика B $10 (5ч), потрачено $9.8", - "healthCheckCircuit": "Поставщик A потерпел неудачу 5 раз, статус: открыт", - "healthCheckConcurrent": "Проверить количество активных сеансов на лимит", - "healthCheckConcurrentExample": "Лимит поставщика C 2, активных 2 сеанса", - "healthFilter": "3️⃣ Фильтрация здоровья: автоматически пропускать поставщиков с размыканием или превышением лимитов", - "healthFiltered": "B отфильтрован (близко к лимиту), осталось: C, D", - "healthFiltered2": "C отфильтрован (полный), осталось: D", - "history": "Проверить историю запросов", - "historyDesc": "Запрос поставщиков, используемых этим ключом за последние 10 секунд", - "priority": "Приоритизация по уровням", - "priorityExample": "4 включенных поставщика с разными приоритетами", - "priorityFirst": "1️⃣ Приоритет в первую очередь: выбирать только из поставщиков с наивысшим приоритетом (наименьшее число)", - "priorityResult": "Отфильтровано к приоритету (0): A, C", - "priorityStep": "Система сначала фильтрует по приоритету, выбирая только из поставщиков с наивысшим приоритетом", - "randomResult": "В конце выбран C", - "randomSelect": "Взвешенный случайный", - "reset": "Ручной сброс автоматического выключателя", - "resetSuccess": "Автоматический выключатель сброшен", - "scenario1Desc": "Система сначала фильтрует по приоритету, выбирая только из поставщиков с наивысшим приоритетом", - "scenario1Step1": "Начальное состояние", - "scenario1Step1After": "Отфильтрованы поставщики с наивысшим приоритетом (0): A, C", - "scenario1Step1Before": "Поставщик A (приоритет 0), B (приоритет 1), C (приоритет 0), D (приоритет 2)", - "scenario1Step1Decision": "Выбирать только из A и C, B и D отфильтрованы", - "scenario1Step1Desc": "Имеется 4 включенных поставщика с разными приоритетами", - "scenario1Step2": "Сортировка по стоимости", - "scenario1Step2After": "После сортировки: C (0.8x), A (1.0x)", - "scenario1Step2Before": "A (стоимость 1.0x), C (стоимость 0.8x)", - "scenario1Step2Decision": "C с более низкой стоимостью имеет более высокую вероятность выбора", - "scenario1Step2Desc": "В пределах одного приоритета сортировка по множителю стоимости от низкого к высокому", - "scenario1Step3": "Взвешенный случайный выбор", - "scenario1Step3After": "C имеет вероятность выбора 75%, A имеет вероятность 25%", - "scenario1Step3Before": "C (вес 3), A (вес 1)", - "scenario1Step3Decision": "В итоге выбран C", - "scenario1Step3Desc": "Использование веса для случайного выбора, чем выше вес, тем выше вероятность выбора", - "scenario1Title": "Выбор по уровням приоритета", - "scenario2Desc": "Если пользователь указал группу поставщиков, система приоритизирует выбор из этой группы", - "scenario2Step1": "Проверка группы пользователя", - "scenario2Step1After": "Отфильтрована группа 'premium': B, C", - "scenario2Step1Before": "Все поставщики: A (default), B (premium), C (premium), D (economy)", - "scenario2Step1Decision": "Выбирать только из B и C", - "scenario2Step1Desc": "Пользователь настроил providerGroup = 'premium'", - "scenario2Step2": "Откат группировки", - "scenario2Step2After": "Откат на всех включенных поставщиков: A, B, C, D", - "scenario2Step2Before": "Все поставщики в группе пользователя 'vip' отключены или превышены", - "scenario2Step2Decision": "Записать предупреждение и выбрать из глобального пула поставщиков", - "scenario2Step2Desc": "Если в группе пользователя нет доступных поставщиков, откат на всех поставщиков", - "scenario2Title": "Фильтрация группы пользователей", - "scenario3Desc": "Система автоматически фильтрует поставщиков с размыканием или превышением лимитов", - "scenario3Step1": "Проверка автоматического выключателя", - "scenario3Step1After": "A отфильтрован, осталось: B, C, D", - "scenario3Step1Before": "Поставщик A потерпел неудачу 5 раз подряд, состояние выключателя: открыт", - "scenario3Step1Decision": "A автоматически восстанавливается в полуоткрытое состояние через 60 секунд", - "scenario3Step1Desc": "После 5 последовательных сбоев выключатель открывается, недоступен в течение 60 секунд", - "scenario3Step2": "Ограничение по сумме", - "scenario3Step2After": "B отфильтрован (близко к лимиту), осталось: C, D", - "scenario3Step2Before": "Лимит поставщика B за 5 часов $10, потрачено $9.8", - "scenario3Step2Decision": "Автоматическое восстановление после сдвига 5-часового окна", - "scenario3Step2Desc": "Проверка превышения лимитов расходов за 5 часов, 7 дней, 30 дней", - "scenario3Step3": "Ограничение одновременных сеансов", - "scenario3Step3After": "C отфильтрован (полон), осталось: D", - "scenario3Step3Before": "Лимит одновременных сеансов поставщика C 2, текущее количество активных сеансов: 2", - "scenario3Step3Decision": "Автоматическое освобождение после истечения сеанса (5 минут)", - "scenario3Step3Desc": "Проверка превышения настроенного лимита одновременных сеансов", - "scenario3Title": "Фильтрация здоровья (автоматический выключатель + ограничение)", - "scenario4Desc": "Последовательные диалоги приоритетно используют одного поставщика для использования кэша контекста Claude", - "scenario4Step1": "Проверка истории запросов", - "scenario4Step1After": "Проверить, включен ли B и работоспособен", - "scenario4Step1Before": "Последний запрос использовал поставщика B", - "scenario4Step1Decision": "B доступен, повторно использовать напрямую, пропустить случайный выбор", - "scenario4Step1Desc": "Запрос поставщиков, используемых этим API ключом за последние 10 секунд", - "scenario4Step2": "Повторное использование недействительно", - "scenario4Step2After": "Вход в нормальный процесс выбора", - "scenario4Step2Before": "Последний использованный поставщик B был отключен или разомкнут", - "scenario4Step2Decision": "Выбрать из других доступных поставщиков", - "scenario4Step2Desc": "Если последний использованный поставщик недоступен, выполнить повторный выбор", - "scenario4Title": "Механизм повторного использования сеансов", - "scenariosTitle": "Интерактивная демонстрация сценариев", - "session": "Механизм переиспользования сеансов", - "sessionDesc": "Если последний использованный поставщик недоступен, переходим к переполнению", - "sessionExample": "Последний запрос использовал поставщика B", - "sessionExpired": "Сеанс автоматически освобождается после истечения (5 минут)", - "sessionFallback": "Выбрать из других доступных поставщиков", - "sessionLastUsed": "B доступен, переиспользуем, пропускаем случайный выбор", - "sessionReuse": "4️⃣ Повторное использование сеанса: последовательные диалоги повторно используют одного поставщика для экономии затрат на контекст", - "sessionUnavailable": "Последний использованный поставщик B отключен или замкнут", - "step": "Шаг", - "title": "Основные принципы", - "weight": "Взвешенный случайный выбор по весу", - "weightCalc": "C имеет вероятность 75%, A имеет вероятность 25%", - "weightExample": "C (вес 3), A (вес 1)" - }, - "keyLoading": "Загрузка...", - "noProviders": "Нет настроенных поставщиков", - "noProvidersDesc": "Добавьте вашего первого поставщика API", - "notFound": "Поставщики не найдены", - "official": "Официальный сайт", - "resetCircuit": "Автоматический выключатель сброшен", - "resetCircuitDesc": "Состояние размыкания поставщика \"{name}\" снято", - "resetCircuitFailed": "Не удалось сбросить автоматический выключатель", - "scheduling": "Подробное объяснение политики планирования", - "schedulingDesc": "Понимание того, как работает выбор поставщика: приоритизация, переиспользование сеансов, балансировка нагрузки и отказоустойчивость", - "searchNoResults": "Поставщики не найдены", - "searchResults": "Найдено {count} поставщиков", - "section": { - "description": "Настройка ограничений по расходам и параллельным сеансам для вышестоящих поставщиков.", - "leaderboard": "Рейтинг", - "title": "Поставщики" - }, - "filter": { - "status": { - "all": "Все статусы", - "active": "Активные", - "inactive": "Неактивные" - }, - "groups": { - "label": "Группы:", - "all": "Все", - "default": "default" - }, - "circuitBroken": "Сбой соединения" - }, - "subtitle": "Поставщики", - "subtitleDesc": "Настройка ограничений по расходам и параллельным сеансам.", - "title": "Поставщики", - "todayUsage": "Сегодня", - "todayUsageCount": "{count} раз", - "toggleFailed": "Не удалось переключить статус", - "toggleSuccess": "Поставщик {status}", - "toggleSuccessDesc": "Статус поставщика \"{name}\" обновлен", - "updateFailed": "Не удалось обновить поставщика", - "viewKey": "Просмотреть полный API ключ", - "viewKeyDesc": "Пожалуйста, храните бережно и не раскрывайте другим", - "types": { - "claude": { - "label": "Claude", - "description": "Официальный API Anthropic" - }, - "claudeAuth": { - "label": "Claude Auth", - "description": "Служба ретрансляции Claude" - }, - "codex": { - "label": "Codex", - "description": "Codex CLI API" - }, - "gemini": { - "label": "Gemini", - "description": "Google Gemini API" - }, - "geminiCli": { - "label": "Gemini CLI", - "description": "Gemini CLI API" - }, - "openaiCompatible": { - "label": "OpenAI Compatible", - "description": "Совместимый с OpenAI API" - } - }, - "list": { - "priority": "Приоритет", - "weight": "Вес", - "costMultiplier": "Коэф цены", - "todayUsageLabel": "Сегодня", - "todayUsageCount": "{count} раз(а)", - "circuitBroken": "Разорвано", - "officialWebsite": "Официальный", - "viewFullKey": "Просмотр полного API-ключа", - "viewFullKeyDesc": "Пожалуйста, храните его в безопасности и не делитесь с другими", - "keyLoading": "Загрузка...", - "confirmDeleteTitle": "Подтвердить удаление провайдера?", - "confirmDeleteMessage": "Вы уверены, что хотите удалить провайдера \"{name}\"? Это действие нельзя отменить.", - "deleteButton": "Удалить", - "cancelButton": "Отмена", - "deleteSuccess": "Успешно удалено", - "deleteSuccessDesc": "Провайдер \"{name}\" был удален", - "deleteFailed": "Не удалось удалить", - "deleteError": "Произошла ошибка во время операции", - "unknownError": "Неизвестная ошибка", - "getKeyFailed": "Не удалось получить ключ", - "keyCopied": "Ключ скопирован в буфер обмена", - "copyFailed": "Не удалось скопировать", - "clipboardUnavailable": "Буфер обмена недоступен в этой среде. Скопируйте ключ вручную.", - "resetCircuitSuccess": "Автоматический выключатель сброшен", - "resetCircuitSuccessDesc": "Статус автоматического выключателя провайдера \"{name}\" очищен", - "resetCircuitFailed": "Не удалось сбросить автоматический выключатель", - "resetUsageTitle": "Сбросить общий расход", - "resetUsageSuccess": "Общий расход сброшен", - "resetUsageSuccessDesc": "Общий расход провайдера \"{name}\" был сброшен", - "resetUsageFailed": "Не удалось сбросить общий расход", - "toggleSuccess": "Провайдер {status}", - "toggleSuccessDesc": "Статус провайдера \"{name}\" обновлен", - "toggleFailed": "Не удалось переключить", - "statusEnabled": "включен", - "statusDisabled": "отключен" - }, - "inlineEdit": { - "save": "Сохранить", - "cancel": "Отмена", - "saveSuccess": "Успешно сохранено", - "saveFailed": "Не удалось сохранить", - "priorityLabel": "Приоритет", - "weightLabel": "Вес", - "costMultiplierLabel": "Коэф цены", - "priorityInvalid": "Введите целое число >= 0", - "weightInvalid": "Введите целое число от 1 до 100", - "costMultiplierInvalid": "Введите число не меньше 0" - }, - "schedulingDialog": { - "title": "Правила планирования провайдеров", - "description": "Узнайте, как система интеллектуально выбирает вышестоящих провайдеров для высокой доступности и оптимизации затрат", - "triggerButton": "Правила", - "step": "Шаг", - "before": "До:", - "after": "После:", - "decision": "Решение:" - }, - "sort": { - "byName": "По имени (A-Z)", - "byPriority": "По приоритету (выс-низ)", - "byWeight": "По весу (выс-низ)", - "byActualPriority": "По фактическому приоритету выбора", - "byCreatedAt": "По дате создания (нов-стар)", - "placeholder": "Сортировать провайдеров" - }, - "search": { - "placeholder": "Поиск по имени, URL, заметкам...", - "clear": "Очистить поиск", - "found": "Найдено {count} совпадающих провайдеров", - "notFound": "Совпадающие провайдеры не найдены", - "showing": "Показано {filtered} / {total} провайдеров" - } - }, - "sensitiveWords": { - "add": "Добавить чувствительное слово", - "addFailed": "Ошибка создания чувствительного слова", - "addSuccess": "Чувствительное слово создано успешно", - "cacheStats": "Статистика кэша: Содержит({containsCount}) Точное({exactCount}) Регулярное({regexCount})", - "confirmDelete": "Вы уверены, что хотите удалить чувствительное слово \"{word}\"?", - "delete": "Удалить чувствительное слово", - "deleteFailed": "Ошибка удаления", - "deleteSuccess": "Чувствительное слово удалено успешно", - "description": "Настройка фильтрации чувствительных слов для блокирования чувствительного контента.", - "dialog": { - "addDescription": "Настройте правила фильтрации чувствительных слов. Совпадающие запросы не будут пересылаться.", - "addTitle": "Добавить чувствительное слово", - "creating": "Создание...", - "descriptionLabel": "Описание", - "descriptionPlaceholder": "Необязательно: Добавить описание...", - "editDescription": "Изменить конфигурацию чувствительного слова. Изменения автоматически обновят кэш.", - "editTitle": "Редактировать чувствительное слово", - "matchTypeContains": "Частичное совпадение - Блокировать, если текст содержит это слово", - "matchTypeExact": "Точное совпадение - Блокировать только при точном совпадении", - "matchTypeLabel": "Тип совпадения *", - "matchTypeRegex": "Регулярное выражение - Поддержка сложного сопоставления шаблонов", - "saving": "Сохранение...", - "wordLabel": "Чувствительное слово *", - "wordPlaceholder": "Введите чувствительное слово...", - "wordRequired": "Пожалуйста, введите чувствительное слово" - }, - "disable": "Чувствительное слово отключено", - "edit": "Редактировать чувствительное слово", - "editFailed": "Ошибка обновления чувствительного слова", - "editSuccess": "Чувствительное слово обновлено успешно", - "emptyState": "Пока нет чувствительных слов. Нажмите 'Добавить чувствительное слово' в правом верхнем углу для начала настройки.", - "enable": "Чувствительное слово включено", - "refreshCache": "Обновить кэш", - "refreshCacheFailed": "Не удалось обновить кэш", - "refreshCacheSuccess": "Кэш успешно обновлен, загружено {count} чувствительных слов", - "section": { - "description": "Запросы, заблокированные чувствительными словами, не будут пересылаться и не будут тарифицироваться. Поддерживает частичное совпадение, точное совпадение и регулярные выражения.", - "title": "Список чувствительных слов" - }, - "table": { - "actions": "Действия", - "createdAt": "Создано", - "description": "Описание", - "matchType": "Тип совпадения", - "matchTypeContains": "Частичное", - "matchTypeExact": "Точное", - "matchTypeRegex": "Регулярное", - "status": "Статус", - "word": "Чувствительное слово" - }, - "title": "Управление чувствительными словами", - "toggleFailed": "Ошибка переключения", - "toggleFailedError": "Ошибка переключения:" - }, - "requestFilters": { - "nav": "Фильтры запросов", - "title": "Фильтры запросов", - "description": "Настройте удаление/замену заголовков и замену тела перед отправкой вверх по цепочке для маскировки запросов.", - "add": "Добавить фильтр", - "addSuccess": "Создано", - "addFailed": "Не удалось создать", - "edit": "Редактировать фильтр", - "editSuccess": "Обновлено", - "editFailed": "Не удалось обновить", - "delete": "Удалить фильтр", - "deleteSuccess": "Удалено", - "deleteFailed": "Не удалось удалить", - "enable": "Включено", - "disable": "Отключено", - "confirmDelete": "Удалить фильтр \"{name}\"?", - "empty": "Фильтров пока нет. Добавьте новый.", - "refreshCache": "Обновить кэш", - "refreshSuccess": "Кэш обновлен, загружено {count} фильтров", - "refreshFailed": "Обновление не удалось", - "dialog": { - "createTitle": "Добавить фильтр", - "editTitle": "Редактировать фильтр", - "name": "Название", - "scope": "Область", - "action": "Действие", - "target": "Поле/путь", - "replacement": "Значение (опционально)", - "description": "Описание (опционально)", - "priority": "Приоритет", - "matchType": "Тип совпадения", - "matchTypeContains": "Содержит", - "matchTypeExact": "Точное", - "matchTypeRegex": "Регулярное выражение", - "jsonPathPlaceholder": "например: messages.0.content или data.items[0].token", - "targetPlaceholder": "Имя заголовка или текст/путь", - "replacementPlaceholder": "Строка или JSON, пусто — удалить", - "save": "Сохранить", - "saving": "Сохранение...", - "validation": { - "fieldRequired": "Название и цель обязательны" - }, - "bindingType": "Применить к", - "bindingGlobal": "Все провайдеры (глобально)", - "bindingProviders": "Конкретные провайдеры", - "bindingGroups": "Группы провайдеров", - "selectProviders": "Выберите провайдеров...", - "selectGroups": "Выберите группы...", - "searchProviders": "Поиск провайдеров...", - "searchGroups": "Поиск групп...", - "noProvidersFound": "Провайдеры не найдены", - "noGroupsFound": "Группы не найдены", - "providersSelected": "Выбрано провайдеров: {count}", - "groupsSelected": "Выбрано групп: {count}", - "loading": "Загрузка...", - "clear": "Очистить", - "selectAll": "Выбрать все" - }, - "table": { - "name": "Название", - "scope": "Область", - "action": "Действие", - "target": "Цель", - "replacement": "Значение", - "priority": "Приоритет", - "apply": "Область", - "status": "Статус", - "createdAt": "Создано", - "actions": "Действия" - }, - "scopeLabel": { - "header": "Header", - "body": "Body" - }, - "actionLabel": { - "remove": "Удалить header", - "set": "Установить header", - "json_path": "Замена по JSON пути", - "text_replace": "Замена текста" - }, - "applyToAll": "Применяется ко всем запросам", - "providers": "Провайдеры", - "groups": "Группы" - }, - "errorRules": { - "nav": "Правила ошибок", - "title": "Управление правилами ошибок", - "description": "Управление правилами клиентских ошибок, которые не требуют автоматических повторов. После настройки ошибки, соответствующие правилам, будут возвращены пользователям напрямую без повторов и не будут учитываться в пороге срабатывания автоматического выключателя провайдера.", - "section": { - "title": "Список правил ошибок" - }, - "tester": { - "title": "Тестирование правил ошибок", - "description": "Введите сообщение об ошибке, чтобы проверить совпадение с настроенными правилами и увидеть итоговый ответ.", - "inputLabel": "Тестовое сообщение об ошибке", - "inputPlaceholder": "Введите сообщение об ошибке для проверки...", - "testButton": "Запустить тест", - "testing": "Тестирование...", - "matched": "Совпало с правилом ошибки", - "notMatched": "Правила не совпали", - "finalResponse": "Ответ замены", - "ruleInfo": "Совпавшее правило", - "noRule": "Совпавших правил нет", - "category": "Категория", - "pattern": "Шаблон", - "matchType": "Тип совпадения", - "overrideStatusCode": "Код статуса замены", - "testFailed": "Тест не удался, попробуйте позже", - "messageRequired": "Введите сообщение об ошибке для проверки", - "warnings": "Предупреждения конфигурации", - "statusCodeOnlyOverride": "Заменяется только код статуса, тело ответа будет использовать исходную ошибку" - }, - "add": "Добавить правило ошибки", - "addSuccess": "Правило ошибки успешно создано", - "addFailed": "Не удалось создать правило ошибки", - "edit": "Редактировать правило ошибки", - "editSuccess": "Правило ошибки успешно обновлено", - "editFailed": "Не удалось обновить правило ошибки", - "delete": "Удалить правило ошибки", - "deleteSuccess": "Правило ошибки успешно удалено", - "deleteFailed": "Удаление не удалось", - "enable": "Правило ошибки включено", - "disable": "Правило ошибки отключено", - "toggleFailed": "Переключение не удалось", - "toggleFailedError": "Переключение не удалось:", - "refreshCache": "Синхронизировать правила", - "refreshCacheSuccess": "Правила успешно синхронизированы, загружено {count} правил ошибок", - "refreshCacheFailed": "Не удалось синхронизировать правила", - "cacheStats": "Кэшировано: {totalCount} правил", - "emptyState": "Правил ошибок пока нет. Нажмите 'Добавить правило ошибки' в правом верхнем углу, чтобы начать настройку.", - "confirmDelete": "Вы уверены, что хотите удалить правило ошибки \"{pattern}\"?", - "dialog": { - "addTitle": "Добавить правило ошибки", - "addDescription": "Настройте шаблоны регулярных выражений для сообщений об ошибках. Совпадающие ошибки будут определены как неповторяемые клиентские ошибки.", - "editTitle": "Редактировать правило ошибки", - "editDescription": "Измените конфигурацию правила ошибки. Изменения автоматически обновят кэш.", - "patternLabel": "Регулярное выражение *", - "patternPlaceholder": "Введите регулярное выражение...", - "patternRequired": "Пожалуйста, введите регулярное выражение", - "patternHint": "Поддерживает синтаксис регулярных выражений JavaScript, например: prompt is too long|invalid.*request", - "categoryLabel": "Категория правила *", - "categoryPlaceholder": "Выберите категорию правила", - "categoryRequired": "Пожалуйста, выберите категорию правила", - "categoryHint": "Выберите категорию ошибки для классификации и статистики", - "descriptionLabel": "Описание", - "descriptionPlaceholder": "Необязательно: Добавить описание...", - "invalidRegex": "Неверный синтаксис регулярного выражения", - "regexTester": "Тестер регулярных выражений", - "testMessageLabel": "Тестовое сообщение", - "testMessagePlaceholder": "Введите сообщение об ошибке для тестирования...", - "matchSuccess": "Совпадение найдено", - "matchFailed": "Совпадение не найдено", - "invalidPattern": "Недействительное регулярное выражение", - "matchedText": "Совпавший текст", - "defaultRuleHint": "Шаблон правила по умолчанию не может быть изменен", - "enableOverride": "Включить переопределение ошибки", - "enableOverrideHint": "При включении вы можете настроить ответ об ошибке и код статуса, возвращаемые клиентам. Исходные ошибки по-прежнему записываются в базу данных. В настоящее время поддерживается только формат ошибок Claude API.", - "overrideResponseLabel": "Ответ замены (JSON)", - "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"Ваше пользовательское сообщение\"\n }\n}", - "overrideResponseHint": "Оставьте пустым для переопределения только кода статуса.", - "overrideStatusCodeLabel": "Код статуса замены (Необязательно)", - "overrideStatusCodePlaceholder": "например, 400", - "overrideStatusCodeHint": "Оставьте пустым для использования кода статуса upstream. Диапазон: 400-599.", - "useTemplate": "Шаблон Claude Error", - "useTemplateConfirm": "Существующее содержимое будет заменено шаблоном. Продолжить?", - "validJson": "Формат JSON корректен", - "invalidJson": "Неверный формат JSON", - "invalidStatusCode": "Код статуса должен быть между 400-599", - "creating": "Создание...", - "saving": "Сохранение..." - }, - "table": { - "pattern": "Регулярное выражение", - "category": "Категория правила", - "description": "Описание", - "status": "Статус", - "default": "По умолчанию", - "isEnabled": "Статус включения", - "isDefault": "Правило по умолчанию", - "createdAt": "Дата создания", - "actions": "Действия" - }, - "form": { - "fields": { - "pattern": "Регулярное выражение", - "category": "Категория правила", - "description": "Описание" - }, - "placeholders": { - "pattern": "например: prompt is too long", - "category": "Выбрать категорию", - "description": "Необязательно: Добавить описание..." - }, - "labels": { - "pattern": "Регулярное выражение *", - "category": "Категория правила *", - "description": "Описание", - "isEnabled": "Статус включения" - } - }, - "actions": { - "add": "Добавить", - "edit": "Редактировать", - "delete": "Удалить", - "refresh": "Обновить", - "test": "Тест", - "messages": { - "success": "Операция успешна", - "error": "Операция не удалась" - } - }, - "validation": { - "patternRequired": "Пожалуйста, введите регулярное выражение", - "categoryRequired": "Пожалуйста, выберите категорию правила", - "patternInvalid": "Неверный синтаксис регулярного выражения", - "redosRisk": "Регулярное выражение имеет риск ReDoS, упростите шаблон", - "patternTooComplex": "Регулярное выражение слишком сложное" - }, - "categories": { - "prompt_limit": "Ограничение длины промпта", - "content_filter": "Фильтр контента", - "pdf_limit": "Ограничение страниц PDF", - "thinking_error": "Ошибка формата Thinking", - "parameter_error": "Ошибка валидации параметров", - "invalid_request": "Недопустимый запрос", - "cache_limit": "Ограничение управления кэшем" - }, - "regexTester": { - "title": "Тестер регулярных выражений", - "testMessage": "Тестовое сообщение", - "testMessagePlaceholder": "Введите сообщение об ошибке для тестирования...", - "matchResult": "Результат сопоставления", - "matched": "Совпало", - "notMatched": "Не совпало", - "test": "Тест" - }, - "defaultRules": { - "cannotDelete": "Правила по умолчанию не могут быть удалены", - "cannotDisable": "Рекомендуется сохранить правила по умолчанию включенными" - } - }, - "mcpPassthroughConfig": "Конфигурация сквозной передачи MCP", - "mcpPassthroughConfigNone": "Отключено", - "mcpPassthroughConfigMinimax": "Minimax", - "mcpPassthroughConfigGlm": "GLM", - "mcpPassthroughConfigCustom": "Пользовательский (Зарезервировано)", - "mcpPassthroughDesc": "При включении передаёт вызовы инструментов MCP указанному AI-провайдеру (например, minimax для распознавания изображений, веб-поиска)", - "mcpPassthroughSelect": "Тип сквозной передачи", - "mcpPassthroughNoneLabel": "Отключено", - "mcpPassthroughNoneDesc": "Не включать сквозную передачу MCP (по умолчанию)", - "mcpPassthroughMinimaxLabel": "Minimax", - "mcpPassthroughMinimaxDesc": "Сквозная передача в сервис minimax MCP (поддержка распознавания изображений, веб-поиска и т.д.)", - "mcpPassthroughGlmLabel": "GLM", - "mcpPassthroughGlmDesc": "Сквозная передача в сервис GLM MCP (поддержка анализа изображений, видео и т.д.)", - "mcpPassthroughCustomLabel": "Пользовательский", - "mcpPassthroughCustomDesc": "Сквозная передача в пользовательский сервис MCP (зарезервировано, не реализовано)", - "mcpPassthroughHint": "Подсказка: сквозная передача MCP позволяет клиенту Claude Code использовать возможности инструментов, предоставляемых сторонними AI-провайдерами (например, распознавание изображений, веб-поиск)", - "mcpPassthroughUrlLabel": "URL сквозной передачи MCP", - "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com", - "mcpPassthroughUrlDesc": "Базовый URL сервиса MCP. Оставьте пустым для автоматического извлечения из URL провайдера", - "mcpPassthroughUrlAuto": "Автоматически извлечено: {url}" -} diff --git a/messages/ru/settings/clientVersions.json b/messages/ru/settings/clientVersions.json new file mode 100644 index 000000000..98166ed4a --- /dev/null +++ b/messages/ru/settings/clientVersions.json @@ -0,0 +1,51 @@ +{ + "description": "Управление требованиями версии клиента для обеспечения использования последней стабильной версии. VSCode и CLI управляются отдельно.", + "empty": { + "description": "За последние 7 дней не было активных пользователей с распознаваемыми клиентами", + "title": "Нет данных о клиентах" + }, + "features": { + "activeWindow": "Активное окно:", + "activeWindowDesc": "Учитываются только пользователи с запросами за последние 7 дней", + "autoDetect": "Система автоматически определяет последнюю стабильную версию (GA версию) каждого типа клиента", + "blockOldVersion": "Пользователи старых версий будут получать HTTP 400 ошибку и не смогут продолжить использование сервиса", + "errorMessage": "Сообщение об ошибке будет содержать текущую версию и требуемую версию для обновления", + "gaRule": "Правила определения:", + "gaRuleDesc": "Версия считается GA версией, когда её используют более 1 пользователя", + "recommendation": "Рекомендуемый подход:", + "recommendationDesc": "Сначала изучите распределение версий ниже, убедитесь в стабильности новой версии перед включением.", + "title": "Описание функции", + "whatHappens": "Что произойдет после включения:" + }, + "section": { + "distribution": { + "description": "Показывает информацию о версиях клиентов активных пользователей за последние 7 дней. GA версия рассчитывается независимо для каждого типа клиента.", + "title": "Распределение версий клиентов" + }, + "settings": { + "description": "После включения система будет автоматически проверять версию клиента и блокировать запросы от пользователей со старыми версиями.", + "title": "Настройки напоминания об обновлении" + } + }, + "table": { + "currentGA": "Текущая GA версия:", + "internalType": "Внутренний тип:", + "lastActive": "Последняя активность", + "latest": "Последняя", + "needsUpgrade": "Требуется обновление", + "noUsers": "Нет данных о пользователях", + "status": "Статус", + "unknown": "Неизвестно", + "user": "Пользователь", + "usersCount": "{count} пользователей", + "version": "Текущая версия" + }, + "title": "Напоминание об обновлении клиента", + "toggle": { + "description": "При включении система автоматически проверяет версию и блокирует старые версии.", + "disableSuccess": "Проверка версии клиента отключена", + "enable": "Включить проверку версии клиента", + "enableSuccess": "Проверка версии клиента включена", + "toggleFailed": "Ошибка переключения" + } +} diff --git a/messages/ru/settings/common.json b/messages/ru/settings/common.json new file mode 100644 index 000000000..5498a362a --- /dev/null +++ b/messages/ru/settings/common.json @@ -0,0 +1,31 @@ +{ + "cancel": "Отменить", + "completed": "Завершено", + "confirm": "Подтвердить", + "copied": "Ключ скопирован в буфер обмена", + "copy": "Копировать", + "copyFailed": "Ошибка копирования", + "create": "Создать", + "creating": "Создание...", + "delete": "Удалить", + "disabled": "Отключено", + "edit": "Редактировать", + "empty": "Результаты не найдены", + "enabled": "Включено", + "error": "Неизвестная ошибка", + "failed": "Ошибка", + "loading": "Загрузка...", + "none": "Нет (нет пользователей с этой версией)", + "refresh": "Обновить", + "reset": "Сброс", + "save": "Сохранить", + "saving": "Сохранение...", + "submit": "Отправить", + "success": "Успех", + "test": "Тестировать", + "testing": "Тестирование...", + "unlimited": "Без ограничений", + "unlimited_desc": "Без ограничений", + "update": "Обновить", + "updating": "Обновление..." +} diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json new file mode 100644 index 000000000..8e39df2e2 --- /dev/null +++ b/messages/ru/settings/config.json @@ -0,0 +1,89 @@ +{ + "autoCleanup": "Автоматическая очистка логов", + "autoCleanupDesc": "Автоматически очищать исторические логи по расписанию для освобождения места в БД.", + "description": "Управление основными параметрами системы, влияющими на отображение и поведение статистики.", + "form": { + "allowGlobalView": "Разрешить просмотр глобального использования", + "allowGlobalViewDesc": "При отключении обычные пользователи могут видеть только статистику использования своих ключей на панели.", + "autoCleanupSaved": "Конфигурация автоочистки сохранена", + "billingModelSource": "Источник модели для тарификации", + "billingModelSourceDesc": "Настройте, какую модель использовать для тарификации при перенаправлении модели. «До перенаправления» использует исходную модель, запрошенную пользователем, «После перенаправления» использует фактически вызванную модель.", + "billingModelSourceOptions": { + "original": "До перенаправления (исходная модель)", + "redirected": "После перенаправления (фактическая модель)" + }, + "billingModelSourcePlaceholder": "Выберите источник модели для тарификации", + "cleanupBatchSize": "Размер пакета", + "cleanupBatchSizeDesc": "Количество записей для удаления за раз (диапазон: 1000-100000, рекомендуется 10000)", + "cleanupBatchSizePlaceholder": "10000", + "cleanupBatchSizeRequired": "Размер пакета *", + "cleanupRetentionDays": "Хранить дней", + "cleanupRetentionDaysDesc": "Логи старше этого количества дней будут автоматически очищены (диапазон: 1-365 дней)", + "cleanupRetentionDaysPlaceholder": "30", + "cleanupRetentionDaysRequired": "Хранить дней *", + "cleanupSchedule": "График очистки", + "cleanupScheduleCronDesc": "Cron-выражение, по умолчанию: 0 2 * * * (2 часа ночи ежедневно)", + "cleanupScheduleCronExample": "Пример: 0 3 * * 0 (3 часа ночи каждое воскресенье)", + "cleanupScheduleDesc": "Выбрать расписание автоматической очистки", + "cleanupScheduleLabel": "Время выполнения (Cron)", + "cleanupSchedulePlaceholder": "0 2 * * *", + "cleanupScheduleRequired": "Время выполнения (Cron) *", + "configUpdated": "Параметры системы обновлены. Страница обновится для применения изменений валюты.", + "currencies": { + "CNY": "¥ Китайский юань (CNY)", + "EUR": "€ Евро (EUR)", + "GBP": "£ Фунт стерлингов (GBP)", + "HKD": "HK$ Гонконгский доллар (HKD)", + "JPY": "¥ Японская иена (JPY)", + "KRW": "₩ Южнокорейская вона (KRW)", + "SGD": "S$ Сингапурский доллар (SGD)", + "TWD": "NT$ Новый тайваньский доллар (TWD)", + "USD": "$ Доллар США (USD)" + }, + "currencyDisplay": "Валюта", + "currencyDisplayDesc": "После изменения все страницы и API будут использовать соответствующий символ валюты (только символ, без конвертации).", + "currencyDisplayPlaceholder": "Выберите валюту", + "enableAutoCleanup": "Включить автоочистку", + "enableAutoCleanupDesc": "Автоматически очищать исторические логи по расписанию", + "enableHttp2": "Включить HTTP/2", + "enableHttp2Desc": "При включении прокси-запросы будут отдавать приоритет HTTP/2. Если HTTP/2 не удастся, произойдёт автоматическое понижение до HTTP/1.1.", + "enableResponseFixer": "Включить исправление ответов", + "enableResponseFixerDesc": "Автоматически исправляет распространённые проблемы ответа у провайдеров (кодировка, SSE, обрезанный JSON). Включено по умолчанию.", + "enableThinkingSignatureRectifier": "Включить исправление thinking-signature", + "enableThinkingSignatureRectifierDesc": "Если Anthropic-провайдер возвращает ошибку несовместимой подписи thinking или некорректного запроса, автоматически удаляет несовместимые thinking-блоки и повторяет запрос один раз к тому же провайдеру (включено по умолчанию).", + "interceptAnthropicWarmupRequests": "Перехватывать Warmup-запросы (Anthropic)", + "interceptAnthropicWarmupRequestsDesc": "Если включено, Warmup-пробные запросы Claude Code будут отвечены самим CCH без обращения к провайдерам; запрос сохраняется в логах, но не тарифицируется, не учитывается в лимитах и исключается из статистики.", + "keepDays": "Хранить дней", + "keepDaysDesc": "Очищать логи старше указанного количества дней", + "responseFixerFixEncoding": "Исправлять кодировку", + "responseFixerFixEncodingDesc": "Удаляет BOM/нулевые байты и нормализует невалидный UTF-8.", + "responseFixerFixSseFormat": "Исправлять формат SSE", + "responseFixerFixSseFormatDesc": "Добавляет отсутствующий префикс data:, нормализует переводы строк и исправляет распространённые поля.", + "responseFixerFixTruncatedJson": "Исправлять обрезанный JSON", + "responseFixerFixTruncatedJsonDesc": "Закрывает незакрытые скобки/кавычки, удаляет завершающие запятые и при необходимости дополняет null.", + "saveConfig": "Сохранить конфигурацию", + "saveError": "Ошибка сохранения", + "saveFailed": "Ошибка сохранения", + "saveSettings": "Сохранить настройки", + "saveSuccess": "Сохранено успешно", + "siteTitle": "Название сайта", + "siteTitleDesc": "Используется для установки заголовка вкладки браузера и имени системы по умолчанию.", + "siteTitlePlaceholder": "например: Claude Code Hub", + "siteTitleRequired": "Название сайта не может быть пустым", + "verboseProviderError": "Подробные ошибки провайдеров", + "verboseProviderErrorDesc": "При включении возвращает подробные сообщения об ошибках при недоступности всех провайдеров (количество провайдеров, причины ограничений и т.д.); при отключении возвращает только простой код ошибки." + }, + "section": { + "autoCleanup": { + "description": "Автоматически очищать исторические логи по расписанию для освобождения места в БД.", + "title": "Автоматическая очистка логов" + }, + "siteParams": { + "description": "Настройка заголовка сайта, валюты отображения и политики отображения статистики на панели.", + "title": "Параметры сайта" + } + }, + "siteSettings": "Параметры сайта", + "siteSettingsDesc": "Настройка названия сайта, валюты и политики отображения статистики.", + "title": "Конфигурация" +} diff --git a/messages/ru/settings/data.json b/messages/ru/settings/data.json new file mode 100644 index 000000000..08d92f68a --- /dev/null +++ b/messages/ru/settings/data.json @@ -0,0 +1,133 @@ +{ + "cleanup": { + "backupRecommendation": "Рекомендация: Экспортируйте резервную копию базы данных перед очисткой на случай, если потребуется восстановление.", + "button": "Очистить логи", + "cancel": "Отмена", + "cleaning": "Очистка...", + "confirm": "Подтвердить очистку", + "confirmTitle": "Подтверждение очистки логов", + "confirmWarning": "Эта операция навсегда удалит все записи логов с {range} и не может быть отменена.", + "descriptionWarning": "Очистка исторических данных логов для освобождения дискового пространства базы данных. Примечание: Статистические данные будут сохранены, но детали логов будут удалены навсегда.", + "error": "Не удалось очистить логи", + "failed": "Очистка не удалась", + "logsDeleted": "✗ Детали логов будут удалены (содержимое запроса/ответа, информация об ошибках и т.д.)", + "previewCount": "Будет удалено {count} записей логов", + "previewError": "Не удается получить предварительный просмотр", + "previewLoading": "Подсчет...", + "range": { + "180days": "Логи старше 6 месяцев (180 дней)", + "30days": "Логи старше 1 месяца (30 дней)", + "7days": "Логи старше 1 недели (7 дней)", + "90days": "Логи старше 3 месяцев (90 дней)" + }, + "rangeDescription": { + "180days": "6 месяцев назад", + "30days": "1 месяц назад", + "7days": "1 неделю назад", + "90days": "3 месяца назад", + "default": "{days} дней назад" + }, + "rangeLabel": "Диапазон очистки", + "statisticsRetained": "✓ Статистические данные будут сохранены (для анализа трендов)", + "successMessage": "Успешно очищено {count} записей логов ({batches} пакетов, заняло {duration}с)", + "willClean": "Будут очищены все записи логов с {range}" + }, + "description": "Управление резервной копией и восстановлением БД с полным импортом/экспортом и очисткой логов.", + "export": { + "button": "Экспортировать базу данных", + "descriptionFull": "Экспорт полного файла резервной копии базы данных (формат .dump) для миграции или восстановления данных. Резервная копия использует формат PostgreSQL custom format, автоматически сжимается и совместима с разными версиями базы данных.", + "error": "Не удалось экспортировать базу данных", + "exporting": "Экспорт...", + "failed": "Экспорт не удался", + "successMessage": "База данных успешно экспортирована!" + }, + "guide": { + "items": { + "cleanup": { + "description": "Физически удаляет исторические логи (необратимо). Таблица статистики будет сохранена. Рекомендуется сначала экспортировать резервную копию.", + "title": "Очистка логов" + }, + "environment": { + "description": "Требует развертывания Docker Compose. Локальная разработка может не поддерживаться.", + "title": "Требования окружения" + }, + "format": { + "description": "Использует PostgreSQL custom format (.dump), автоматически сжимается и совместим с разными версиями БД.", + "title": "Формат резервной копии" + }, + "merge": { + "description": "Сохраняет существующие данные и пытается вставить из резервной копии. Конфликты первичного ключа могут привести к ошибкам.", + "title": "Режим объединения" + }, + "overwrite": { + "description": "Удаляет все существующие данные перед импортом. Лучше всего для полного восстановления.", + "title": "Режим перезаписи" + }, + "safety": { + "description": "Перед импортом сначала экспортируйте текущую БД как резервную копию.", + "title": "Рекомендация безопасности" + } + }, + "title": "Инструкции и меры предосторожности" + }, + "import": { + "backupFile": "Файл резервной копии:", + "backupRecommendation": "Рекомендуется экспортировать текущую базу данных в качестве резервной копии перед продолжением.", + "button": "Импортировать базу данных", + "cancel": "Отмена", + "cleanFirstDescription": "Удалить все существующие данные перед импортом, чтобы база данных точно соответствовала резервной копии. Если не отмечено, будет предпринята попытка объединения данных, но это может не удаться из-за конфликтов первичных ключей.", + "cleanFirstLabel": "Очистить существующие данные (режим перезаписи)", + "confirm": "Подтвердить импорт", + "confirmMerge": "Вы выбрали 'Режим объединения', который попытается импортировать резервную копию, сохраняя существующие данные.", + "confirmOverwrite": "Вы выбрали 'Режим перезаписи', который удалит все существующие данные перед импортом резервной копии.", + "confirmTitle": "Подтверждение импорта базы данных", + "descriptionFull": "Восстановление базы данных из файла резервной копии. Поддерживает файлы резервных копий в формате PostgreSQL custom format (.dump).", + "error": "Не удалось импортировать базу данных", + "errorUnknown": "Неизвестная ошибка", + "failedMessage": "Импорт данных не удался, проверьте подробные логи", + "fileError": "Пожалуйста, выберите файл резервной копии в формате .dump", + "fileSelected": "Выбрано: {name} ({size} МБ)", + "importing": "Импорт...", + "noFileSelected": "Сначала выберите файл резервной копии", + "parseError": "Не удалось разобрать данные ответа", + "progressTitle": "Прогресс импорта", + "selectFileLabel": "Выбрать файл резервной копии", + "streamError": "Не удается прочитать поток ответа", + "streamInterrupted": "Поток данных неожиданно прервался", + "streamInterruptedDesc": "Импорт не завершился корректно. Проверьте логи и убедитесь в целостности данных. При необходимости выполните импорт повторно.", + "successCleanModeDesc": "Все данные успешно восстановлены. Если страница отображается некорректно, обновите браузер.", + "successMergeModeDesc": "Данные успешно импортированы и объединены. Если страница отображается некорректно, обновите браузер.", + "successMessage": "Импорт данных завершен!", + "successWithWarnings": "Импорт данных завершён (с предупреждениями)", + "successWithWarningsDesc": "Данные успешно импортированы, но некоторые уже существующие объекты были пропущены. Если страница отображается некорректно, обновите браузер или перезапустите приложение.", + "warningMerge": "Примечание: Импорт может не удаться при наличии конфликтов первичных ключей.", + "warningOverwrite": "Предупреждение: Это действие необратимо, все текущие данные будут навсегда удалены!" + }, + "section": { + "cleanup": { + "description": "Очистка исторических данных логов для освобождения дискового пространства базы данных. Примечание: Статистические данные будут сохранены, но детали логов будут удалены навсегда.", + "title": "Очистка логов" + }, + "export": { + "description": "Экспорт полного файла резервной копии базы данных (формат .dump) для миграции или восстановления данных.", + "title": "Экспорт данных" + }, + "import": { + "description": "Восстановление базы данных из файла резервной копии. Поддерживает файлы резервных копий в формате PostgreSQL custom format (.dump).", + "title": "Импорт данных" + }, + "status": { + "description": "Просмотр текущего статуса подключения к базе данных и основной информации.", + "title": "Статус базы данных" + } + }, + "status": { + "connected": "База данных подключена", + "error": "Не удалось получить статус базы данных", + "loading": "Загрузка...", + "retry": "Повторить", + "tables": "{count} таблиц", + "unavailable": "База данных недоступна" + }, + "title": "Управление данными" +} diff --git a/messages/ru/settings/errorRules.json b/messages/ru/settings/errorRules.json new file mode 100644 index 000000000..e91d21573 --- /dev/null +++ b/messages/ru/settings/errorRules.json @@ -0,0 +1,157 @@ +{ + "actions": { + "add": "Добавить", + "delete": "Удалить", + "edit": "Редактировать", + "messages": { + "error": "Операция не удалась", + "success": "Операция успешна" + }, + "refresh": "Обновить", + "test": "Тест" + }, + "add": "Добавить правило ошибки", + "addFailed": "Не удалось создать правило ошибки", + "addSuccess": "Правило ошибки успешно создано", + "cacheStats": "Кэшировано: {totalCount} правил", + "categories": { + "cache_limit": "Ограничение управления кэшем", + "content_filter": "Фильтр контента", + "invalid_request": "Недопустимый запрос", + "parameter_error": "Ошибка валидации параметров", + "pdf_limit": "Ограничение страниц PDF", + "prompt_limit": "Ограничение длины промпта", + "thinking_error": "Ошибка формата Thinking" + }, + "confirmDelete": "Вы уверены, что хотите удалить правило ошибки \"{pattern}\"?", + "defaultRules": { + "cannotDelete": "Правила по умолчанию не могут быть удалены", + "cannotDisable": "Рекомендуется сохранить правила по умолчанию включенными" + }, + "delete": "Удалить правило ошибки", + "deleteFailed": "Удаление не удалось", + "deleteSuccess": "Правило ошибки успешно удалено", + "description": "Управление правилами клиентских ошибок, которые не требуют автоматических повторов. После настройки ошибки, соответствующие правилам, будут возвращены пользователям напрямую без повторов и не будут учитываться в пороге срабатывания автоматического выключателя провайдера.", + "dialog": { + "addDescription": "Настройте шаблоны регулярных выражений для сообщений об ошибках. Совпадающие ошибки будут определены как неповторяемые клиентские ошибки.", + "addTitle": "Добавить правило ошибки", + "categoryHint": "Выберите категорию ошибки для классификации и статистики", + "categoryLabel": "Категория правила *", + "categoryPlaceholder": "Выберите категорию правила", + "categoryRequired": "Пожалуйста, выберите категорию правила", + "creating": "Создание...", + "defaultRuleHint": "Шаблон правила по умолчанию не может быть изменен", + "descriptionLabel": "Описание", + "descriptionPlaceholder": "Необязательно: Добавить описание...", + "editDescription": "Измените конфигурацию правила ошибки. Изменения автоматически обновят кэш.", + "editTitle": "Редактировать правило ошибки", + "enableOverride": "Включить переопределение ошибки", + "enableOverrideHint": "При включении вы можете настроить ответ об ошибке и код статуса, возвращаемые клиентам. Исходные ошибки по-прежнему записываются в базу данных. В настоящее время поддерживается только формат ошибок Claude API.", + "invalidJson": "Неверный формат JSON", + "invalidPattern": "Недействительное регулярное выражение", + "invalidRegex": "Неверный синтаксис регулярного выражения", + "invalidStatusCode": "Код статуса должен быть между 400-599", + "matchFailed": "Совпадение не найдено", + "matchSuccess": "Совпадение найдено", + "matchedText": "Совпавший текст", + "overrideResponseHint": "Оставьте пустым для переопределения только кода статуса.", + "overrideResponseLabel": "Ответ замены (JSON)", + "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"Ваше пользовательское сообщение\"\n }\n}", + "overrideStatusCodeHint": "Оставьте пустым для использования кода статуса upstream. Диапазон: 400-599.", + "overrideStatusCodeLabel": "Код статуса замены (Необязательно)", + "overrideStatusCodePlaceholder": "например, 400", + "patternHint": "Поддерживает синтаксис регулярных выражений JavaScript, например: prompt is too long|invalid.*request", + "patternLabel": "Регулярное выражение *", + "patternPlaceholder": "Введите регулярное выражение...", + "patternRequired": "Пожалуйста, введите регулярное выражение", + "regexTester": "Тестер регулярных выражений", + "saving": "Сохранение...", + "testMessageLabel": "Тестовое сообщение", + "testMessagePlaceholder": "Введите сообщение об ошибке для тестирования...", + "useTemplate": "Шаблон Claude Error", + "useTemplateConfirm": "Существующее содержимое будет заменено шаблоном. Продолжить?", + "validJson": "Формат JSON корректен" + }, + "disable": "Правило ошибки отключено", + "edit": "Редактировать правило ошибки", + "editFailed": "Не удалось обновить правило ошибки", + "editSuccess": "Правило ошибки успешно обновлено", + "emptyState": "Правил ошибок пока нет. Нажмите 'Добавить правило ошибки' в правом верхнем углу, чтобы начать настройку.", + "enable": "Правило ошибки включено", + "form": { + "fields": { + "category": "Категория правила", + "description": "Описание", + "pattern": "Регулярное выражение" + }, + "labels": { + "category": "Категория правила *", + "description": "Описание", + "isEnabled": "Статус включения", + "pattern": "Регулярное выражение *" + }, + "placeholders": { + "category": "Выбрать категорию", + "description": "Необязательно: Добавить описание...", + "pattern": "например: prompt is too long" + } + }, + "nav": "Правила ошибок", + "refreshCache": "Синхронизировать правила", + "refreshCacheFailed": "Не удалось синхронизировать правила", + "refreshCacheSuccess": "Правила успешно синхронизированы, загружено {count} правил ошибок", + "regexTester": { + "matchResult": "Результат сопоставления", + "matched": "Совпало", + "notMatched": "Не совпало", + "test": "Тест", + "testMessage": "Тестовое сообщение", + "testMessagePlaceholder": "Введите сообщение об ошибке для тестирования...", + "title": "Тестер регулярных выражений" + }, + "section": { + "title": "Список правил ошибок" + }, + "table": { + "actions": "Действия", + "category": "Категория правила", + "createdAt": "Дата создания", + "default": "По умолчанию", + "description": "Описание", + "isDefault": "Правило по умолчанию", + "isEnabled": "Статус включения", + "pattern": "Регулярное выражение", + "status": "Статус" + }, + "tester": { + "category": "Категория", + "description": "Введите сообщение об ошибке, чтобы проверить совпадение с настроенными правилами и увидеть итоговый ответ.", + "finalResponse": "Ответ замены", + "inputLabel": "Тестовое сообщение об ошибке", + "inputPlaceholder": "Введите сообщение об ошибке для проверки...", + "matchType": "Тип совпадения", + "matched": "Совпало с правилом ошибки", + "messageRequired": "Введите сообщение об ошибке для проверки", + "noRule": "Совпавших правил нет", + "notMatched": "Правила не совпали", + "overrideStatusCode": "Код статуса замены", + "pattern": "Шаблон", + "ruleInfo": "Совпавшее правило", + "statusCodeOnlyOverride": "Заменяется только код статуса, тело ответа будет использовать исходную ошибку", + "testButton": "Запустить тест", + "testFailed": "Тест не удался, попробуйте позже", + "testing": "Тестирование...", + "title": "Тестирование правил ошибок", + "warnings": "Предупреждения конфигурации" + }, + "title": "Управление правилами ошибок", + "toggleFailed": "Переключение не удалось", + "toggleFailedError": "Переключение не удалось:", + "validation": { + "categoryRequired": "Пожалуйста, выберите категорию правила", + "patternInvalid": "Неверный синтаксис регулярного выражения", + "patternRequired": "Пожалуйста, введите регулярное выражение", + "patternTooComplex": "Регулярное выражение слишком сложное", + "redosRisk": "Регулярное выражение имеет риск ReDoS, упростите шаблон" + } +} diff --git a/messages/ru/settings/errors.json b/messages/ru/settings/errors.json new file mode 100644 index 000000000..7aa2a0697 --- /dev/null +++ b/messages/ru/settings/errors.json @@ -0,0 +1,17 @@ +{ + "addFailed": "Не удалось добавить провайдера", + "addSuccess": "Добавление успешно", + "deleteFailed": "Не удалось удалить провайдера", + "deleteSuccess": "Удаление успешно", + "editFailed": "Не удалось обновить провайдера", + "editSuccess": "Обновление успешно", + "loadFailed": "Не удалось загрузить настройки уведомлений", + "saveFailed": "Ошибка сохранения", + "saveFailed_error": "Не удалось сохранить настройки", + "saveSuccess": "Сохранение успешно", + "syncFailed": "Ошибка синхронизации", + "syncSuccess": "Синхронизация успешна", + "testFailed": "Тест не пройден", + "testFailedRetry": "Тест не пройден, попробуйте снова", + "unknownError": "Во время операции произошло исключение" +} diff --git a/messages/ru/settings/index.ts b/messages/ru/settings/index.ts new file mode 100644 index 000000000..414b5daa4 --- /dev/null +++ b/messages/ru/settings/index.ts @@ -0,0 +1,101 @@ +import clientVersions from "./clientVersions.json"; +import common from "./common.json"; +import config from "./config.json"; +import data from "./data.json"; +import errorRules from "./errorRules.json"; +import errors from "./errors.json"; +import logs from "./logs.json"; +import nav from "./nav.json"; +import notifications from "./notifications.json"; +import prices from "./prices.json"; +import requestFilters from "./requestFilters.json"; +import sensitiveWords from "./sensitiveWords.json"; +import strings from "./strings.json"; + +import providersAutoSort from "./providers/autoSort.json"; +import providersFilter from "./providers/filter.json"; +import providersGuide from "./providers/guide.json"; +import providersInlineEdit from "./providers/inlineEdit.json"; +import providersList from "./providers/list.json"; +import providersSchedulingDialog from "./providers/schedulingDialog.json"; +import providersSearch from "./providers/search.json"; +import providersSection from "./providers/section.json"; +import providersSort from "./providers/sort.json"; +import providersStrings from "./providers/strings.json"; +import providersTypes from "./providers/types.json"; + +import providersFormApiTest from "./providers/form/apiTest.json"; +import providersFormButtons from "./providers/form/buttons.json"; +import providersFormCommon from "./providers/form/common.json"; +import providersFormDeleteDialog from "./providers/form/deleteDialog.json"; +import providersFormErrors from "./providers/form/errors.json"; +import providersFormFailureThresholdConfirmDialog from "./providers/form/failureThresholdConfirmDialog.json"; +import providersFormKey from "./providers/form/key.json"; +import providersFormMaxRetryAttempts from "./providers/form/maxRetryAttempts.json"; +import providersFormModelRedirect from "./providers/form/modelRedirect.json"; +import providersFormModelSelect from "./providers/form/modelSelect.json"; +import providersFormName from "./providers/form/name.json"; +import providersFormProviderTypes from "./providers/form/providerTypes.json"; +import providersFormProxyTest from "./providers/form/proxyTest.json"; +import providersFormSections from "./providers/form/sections.json"; +import providersFormStrings from "./providers/form/strings.json"; +import providersFormSuccess from "./providers/form/success.json"; +import providersFormTitle from "./providers/form/title.json"; +import providersFormUrl from "./providers/form/url.json"; +import providersFormUrlPreview from "./providers/form/urlPreview.json"; +import providersFormWebsiteUrl from "./providers/form/websiteUrl.json"; + +const providersForm = { + ...providersFormStrings, + apiTest: providersFormApiTest, + buttons: providersFormButtons, + common: providersFormCommon, + deleteDialog: providersFormDeleteDialog, + errors: providersFormErrors, + failureThresholdConfirmDialog: providersFormFailureThresholdConfirmDialog, + key: providersFormKey, + maxRetryAttempts: providersFormMaxRetryAttempts, + modelRedirect: providersFormModelRedirect, + modelSelect: providersFormModelSelect, + name: providersFormName, + providerTypes: providersFormProviderTypes, + proxyTest: providersFormProxyTest, + sections: providersFormSections, + success: providersFormSuccess, + title: providersFormTitle, + url: providersFormUrl, + urlPreview: providersFormUrlPreview, + websiteUrl: providersFormWebsiteUrl, +}; + +const providers = { + ...providersStrings, + autoSort: providersAutoSort, + filter: providersFilter, + form: providersForm, + guide: providersGuide, + inlineEdit: providersInlineEdit, + list: providersList, + schedulingDialog: providersSchedulingDialog, + search: providersSearch, + section: providersSection, + sort: providersSort, + types: providersTypes, +}; + +export default { + nav, + common, + config, + providers, + prices, + sensitiveWords, + requestFilters, + logs, + data, + clientVersions, + notifications, + errors, + errorRules, + ...strings, +}; diff --git a/messages/ru/settings/logs.json b/messages/ru/settings/logs.json new file mode 100644 index 000000000..acd64ae01 --- /dev/null +++ b/messages/ru/settings/logs.json @@ -0,0 +1,54 @@ +{ + "description": "Динамическая регулировка уровня логирования для контроля подробности.", + "form": { + "changeNotice": "Текущий уровень {current}, после сохранения переключится на {selected}", + "currentLevel": "Текущий уровень логирования", + "effectiveImmediately": "Изменения уровня логирования вступают в силу немедленно без перезапуска сервиса.", + "failed": "Ошибка установки", + "failedError": "Ошибка установки уровня логирования", + "fetchFailed": "Ошибка получения уровня логирования", + "levelGuideDebug": "Debug (Рекомендуется для разработки): Включает подробную отладочную информацию, подходит для устранения проблем", + "levelGuideFatal": "Fatal/Error: Показывать только ошибки, минимальное логирование, подходит для высоконагруженного продакшена", + "levelGuideInfo": "Info (Рекомендуется для продакшена): Показывает ключевые бизнес-события (выбор поставщика, повторное использование сессии, синхронизация цен) + Предупреждения + Ошибки", + "levelGuideTitle": "Руководство по уровням логирования", + "levelGuideTrace": "Trace: Очень подробная трассировочная информация, включает все детали", + "levelGuideWarn": "Warn: Включает предупреждения (ограничение скорости, открытие автомата защиты и т.д.) + Ошибки", + "save": "Сохранить параметры", + "saving": "Сохранение...", + "selectLevel": "Выбрать уровень логирования", + "success": "Уровень логирования установлен: {level}" + }, + "levels": { + "debug": { + "description": "Отладочная информация + Все уровни (Рекомендуется для разработки)", + "label": "Debug" + }, + "error": { + "description": "Сообщения об ошибках", + "label": "Error" + }, + "fatal": { + "description": "Только критические ошибки", + "label": "Fatal" + }, + "info": { + "description": "Ключевые бизнес-события + Предупреждения + Ошибки (Рекомендуется для продакшена)", + "label": "Info" + }, + "trace": { + "description": "Очень подробная трассировка + Все уровни", + "label": "Trace" + }, + "warn": { + "description": "Предупреждения + Ошибки", + "label": "Warn" + } + }, + "section": { + "description": "Изменения вступают в силу немедленно без перезапуска сервиса.", + "title": "Контроль уровня логирования" + }, + "subtitle": "Контроль уровня логирования", + "subtitleDesc": "Изменения вступают в силу немедленно без перезагрузки. Полезно для отладки в производстве.", + "title": "Управление логами" +} diff --git a/messages/ru/settings/nav.json b/messages/ru/settings/nav.json new file mode 100644 index 000000000..1dd63a0ae --- /dev/null +++ b/messages/ru/settings/nav.json @@ -0,0 +1,15 @@ +{ + "apiDocs": "API док.", + "clientVersions": "Обновления", + "config": "Конфиг", + "data": "Данные", + "docs": "Документация", + "errorRules": "Ошибки", + "feedback": "Отзывы", + "logs": "Логи", + "notifications": "Уведомления", + "prices": "Цены", + "providers": "Поставщики", + "requestFilters": "Запросы", + "sensitiveWords": "Фильтры" +} diff --git a/messages/ru/settings/notifications.json b/messages/ru/settings/notifications.json new file mode 100644 index 000000000..7809e0136 --- /dev/null +++ b/messages/ru/settings/notifications.json @@ -0,0 +1,146 @@ +{ + "bindings": { + "advanced": "Дополнительно", + "bindTarget": "Привязать цель", + "boundCount": "Привязано: {count}", + "editTemplateOverride": "Редактировать", + "enable": "Включить", + "enableType": "Включить это уведомление", + "enabledCount": "Включено: {count}", + "noTargets": "Нет доступных целей отправки.", + "scheduleCron": "Cron", + "scheduleCronPlaceholder": "например, 0 9 * * *", + "scheduleTimezone": "Часовой пояс", + "templateOverride": "Переопределение шаблона", + "templateOverrideTitle": "Редактировать переопределение шаблона", + "title": "Привязки" + }, + "circuitBreaker": { + "description": "Отправить оповещение немедленно при полном размыкании провайдера", + "enable": "Включить оповещение о размыкателе цепи", + "test": "Тест подключения", + "title": "Оповещение о размыкателе цепи", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." + }, + "costAlert": { + "description": "Триггер оповещения при превышении порога квоты потребления пользователя/провайдера", + "enable": "Включить оповещение о расходах", + "interval": "Интервал проверки (минуты)", + "test": "Тест подключения", + "threshold": "Порог оповещения", + "thresholdHelp": "Оповещение при достижении {percent}% квоты", + "thresholdLabel": "Порог оповещения: {percent}%", + "title": "Оповещение о расходах", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeFeishu": "Feishu", + "webhookTypeUnknown": "Неизвестная платформа. Используйте URL вебхука WeCom или Feishu", + "webhookTypeWeCom": "WeCom" + }, + "dailyLeaderboard": { + "description": "Ежедневная отправка рейтинга топ N пользователей по потреблению", + "enable": "Включить ежедневный рейтинг", + "test": "Тест подключения", + "time": "Время отправки", + "timeError": "Ошибка формата времени, должен быть HH:mm", + "timePlaceholder": "09:00", + "title": "Ежедневный рейтинг потребления пользователей", + "topN": "Показать топ N", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." + }, + "description": "Настройка push-уведомлений Webhook", + "form": { + "loadError": "Не удалось загрузить настройки уведомлений", + "loading": "Загрузка...", + "save": "Сохранить настройки", + "saveError": "Не удалось сохранить настройки", + "saveFailed": "Ошибка сохранения", + "saving": "Сохранение...", + "success": "Настройки уведомлений сохранены и задачи перепланированы", + "testError": "Ошибка тестирования подключения", + "testFailed": "Тест не пройден", + "testFailedRetry": "Тест не пройден, попробуйте снова", + "testNoResult": "Тест пройден, но результат не возвращен", + "testSuccess": "Тестовое сообщение отправлено", + "webhookRequired": "Сначала заполните Webhook URL" + }, + "global": { + "description": "Включить или отключить все функции push-уведомлений", + "enable": "Включить push-уведомления", + "legacyModeDescription": "Сейчас используется устаревшая схема уведомлений с одним URL. Создайте цель отправки, чтобы перейти на режим с несколькими целями.", + "legacyModeTitle": "Режим совместимости", + "title": "Главный переключатель уведомлений" + }, + "targetDialog": { + "createTitle": "Добавить цель", + "customHeaders": "Пользовательские заголовки (JSON)", + "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", + "dingtalkSecret": "Секрет DingTalk", + "dingtalkSecretPlaceholder": "Необязательно, для подписи", + "editTitle": "Редактировать цель", + "enable": "Включить", + "errors": { + "headersInvalidJson": "Заголовки должны быть корректным JSON", + "headersMustBeObject": "Заголовки должны быть JSON-объектом", + "headersValueMustBeString": "Значения заголовков должны быть строками" + }, + "name": "Название", + "namePlaceholder": "например, Ops Group", + "proxy": { + "fallbackToDirect": "При ошибке прокси — прямое подключение", + "title": "Прокси", + "toggle": "Показать/скрыть настройки прокси", + "url": "URL прокси", + "urlPlaceholder": "http://127.0.0.1:7890" + }, + "selectType": "Выберите платформу", + "telegramBotToken": "Telegram Bot Token", + "telegramBotTokenPlaceholder": "например, 123456:ABCDEF...", + "telegramChatId": "Telegram Chat ID", + "telegramChatIdPlaceholder": "например, -1001234567890", + "type": "Платформа", + "types": { + "custom": "Custom Webhook", + "dingtalk": "DingTalk", + "feishu": "Feishu", + "telegram": "Telegram", + "wechat": "WeCom" + }, + "webhookUrl": "Webhook URL", + "webhookUrlPlaceholder": "https://example.com/webhook" + }, + "targets": { + "add": "Добавить цель", + "bindingsSaved": "Привязки сохранены", + "created": "Цель создана", + "delete": "Удалить", + "deleteConfirm": "Удалить эту цель? Связанные привязки также будут удалены.", + "deleteConfirmTitle": "Удалить цель", + "deleted": "Цель удалена", + "description": "Управление целями отправки. Поддерживает WeCom, Feishu, DingTalk, Telegram и пользовательский Webhook.", + "edit": "Редактировать", + "emptyHint": "Целей нет. Нажмите «Добавить цель», чтобы создать.", + "enable": "Включить цель", + "lastTestAt": "Последний тест", + "lastTestFailed": "Тест не пройден", + "lastTestNever": "Тестов не было", + "lastTestSuccess": "Тест OK", + "statusDisabled": "Отключено", + "statusEnabled": "Включено", + "test": "Тест", + "testSelectType": "Выберите тип теста", + "title": "Цели отправки", + "update": "Сохранить цель", + "updated": "Цель обновлена" + }, + "templateEditor": { + "insert": "Вставить", + "jsonInvalid": "Некорректный JSON", + "placeholder": "Введите JSON-шаблон...", + "placeholders": "Плейсхолдеры", + "title": "Шаблон (JSON)" + }, + "title": "Push-уведомления" +} diff --git a/messages/ru/settings/prices.json b/messages/ru/settings/prices.json new file mode 100644 index 000000000..ace5a1423 --- /dev/null +++ b/messages/ru/settings/prices.json @@ -0,0 +1,191 @@ +{ + "title": "Прайс-лист", + "description": "Управление конфигурацией платформы и ценами моделей", + "section": { + "title": "Цены моделей", + "description": "Управление ценами AI моделей" + }, + "searchPlaceholder": "Поиск по названию модели...", + "filters": { + "all": "Все", + "local": "Локальные", + "anthropic": "Anthropic", + "openai": "OpenAI", + "vertex": "Vertex" + }, + "badges": { + "local": "Локальная" + }, + "capabilities": { + "assistantPrefill": "Предзаполнение ассистента", + "computerUse": "Использование компьютера", + "functionCalling": "Вызов функций", + "pdfInput": "Ввод PDF", + "promptCaching": "Кэширование промпта", + "reasoning": "Рассуждение", + "responseSchema": "Схема ответа", + "toolChoice": "Выбор инструментов", + "vision": "Зрение", + "statusSupported": "Поддерживается", + "statusUnsupported": "Не поддерживается", + "tooltip": "{label}: {status}" + }, + "sync": { + "button": "Синхронизировать облачный прайс-лист", + "syncing": "Синхронизация...", + "checking": "Проверка конфликтов...", + "successWithChanges": "Обновление прайс-листа: добавлено {added}, обновлено {updated}, без изменений {unchanged}", + "successNoChanges": "Прайс-лист актуален, обновление не требуется", + "failed": "Ошибка синхронизации", + "failedError": "Ошибка синхронизации: {error}", + "failedNoResult": "Прайс-лист обновлен но результат не возвращен", + "noModels": "Цены моделей не найдены", + "partialFailure": "Частичное обновление выполнено, но {failed} моделей не удалось обновить", + "failedModels": "Не удалось обновить модели: {models}", + "skippedConflicts": "Пропущено {count} ручных моделей" + }, + "conflict": { + "title": "Выберите элементы для перезаписи", + "description": "Следующие модели имеют ручные цены. Отмеченные будут перезаписаны ценами LiteLLM, неотмеченные останутся без изменений", + "searchPlaceholder": "Поиск моделей...", + "table": { + "modelName": "Модель", + "manualPrice": "Ручная цена", + "litellmPrice": "Цена LiteLLM", + "action": "Действие" + }, + "viewDiff": "Показать различия", + "diffTitle": "Различия цен", + "diff": { + "field": "Поле", + "manual": "Ручное", + "litellm": "LiteLLM", + "inputPrice": "Цена ввода", + "outputPrice": "Цена вывода", + "imagePrice": "Цена изображения", + "provider": "Поставщик", + "mode": "Тип" + }, + "pagination": { + "showing": "Показано {from}-{to} из {total}" + }, + "selectedCount": "Выбрано {count}/{total} моделей", + "noMatch": "Модели не найдены", + "noConflicts": "Конфликтов нет", + "applyOverwrite": "Применить перезапись", + "applying": "Применение..." + }, + "table": { + "modelName": "Название модели", + "provider": "Поставщик", + "capabilities": "Возможности", + "price": "Цена", + "inputPrice": "Цена ввода ($/M)", + "outputPrice": "Цена вывода ($/M)", + "priceInput": "Ввод", + "priceOutput": "Вывод", + "pricePerRequest": "Запрос", + "cacheReadPrice": "Чтение кэша ($/M)", + "cacheCreationPrice": "Создание кэша ($/M)", + "cache5m": "5m", + "cache1h": "1h+", + "copyModelId": "Скопировать ID модели", + "updatedAt": "Обновлено", + "actions": "Действия", + "typeChat": "Чат", + "typeImage": "Генерация изображений", + "typeCompletion": "Дополнение", + "typeUnknown": "Неизвестно", + "loading": "Загрузка...", + "noMatch": "Соответствующие модели не найдены", + "noDataTitle": "Данные о ценах отсутствуют", + "noDataHint": "Система имеет встроенный прайс-лист. Используйте кнопки выше для синхронизации." + }, + "pagination": { + "showing": "Показано {from}-{to} из {total}", + "previous": "Назад", + "next": "Вперёд", + "perPageLabel": "На странице", + "perPage": "{size} на странице" + }, + "stats": { + "totalModels": "Всего моделей: {count}", + "searchResults": "Результатов поиска: {count}", + "lastUpdated": "Последнее обновление: {time}" + }, + "dialog": { + "title": "Обновить прайс-лист", + "description": "Выберите и загрузите JSON или TOML файл с данными о ценах моделей", + "selectFile": "Нажмите для выбора JSON/TOML или перетащите сюда", + "fileSizeLimit": "Размер файла не может превышать 10MB", + "fileSizeLimitSmall": "Размер файла не превышает 10MB", + "invalidFileType": "Пожалуйста, выберите файл JSON или TOML", + "fileTooLarge": "Размер файла превышает лимит 10MB", + "upload": "Загрузить и обновить", + "uploading": "Загрузка...", + "updatePriceTable": "Обновить прайс-лист", + "updating": "Обновление...", + "selectJson": "Выбрать файл", + "updateSuccess": "Прайс-лист успешно обновлён, {count} моделей обновлено", + "updateFailed": "Ошибка обновления", + "systemHasBuiltIn": "Система имеет встроенный прайс-лист", + "manualDownload": "Вы также можете скачать вручную", + "latestPriceTable": "облачный прайс-лист", + "andUploadViaButton": ", и загрузить через кнопку выше", + "cloudModelCountLoading": "Загрузка количества моделей из облака...", + "cloudModelCountFailed": "Не удалось загрузить количество моделей из облака", + "supportedModels": "Поддерживается {count} моделей", + "results": { + "title": "Результаты обновления", + "total": "Всего: {total} моделей", + "success": "Успешно: {success}", + "failed": "Ошибок: {failed}", + "skipped": "Пропущено: {skipped}", + "more": " (+{count})", + "details": "Подробности", + "viewDetails": "Просмотреть подробный журнал" + } + }, + "addModel": "Добавить модель", + "editModel": "Редактировать модель", + "deleteModel": "Удалить модель", + "addModelDescription": "Вручную добавить новую цену модели", + "editModelDescription": "Редактировать цену модели", + "deleteConfirm": "Удалить модель {name}? Это действие необратимо.", + "form": { + "modelName": "ID модели", + "modelNamePlaceholder": "например: gpt-5.2-codex", + "modelNameRequired": "ID модели обязателен", + "displayName": "Отображаемое имя (необязательно)", + "displayNamePlaceholder": "например: GPT-5.2 Codex", + "type": "Тип", + "provider": "Поставщик", + "providerPlaceholder": "например: openai", + "requestPrice": "Цена за вызов ($/request)", + "inputPrice": "Цена ввода ($/M токенов)", + "outputPrice": "Цена вывода ($/M токенов)", + "outputPriceImage": "Цена вывода ($/изображение)", + "cacheReadPrice": "Цена чтения кэша ($/M токенов)", + "cacheCreationPrice5m": "Цена создания кэша (5m, $/M токенов)", + "cacheCreationPrice1h": "Цена создания кэша (1h+, $/M токенов)" + }, + "drawer": { + "prefillLabel": "Поиск существующих моделей для автозаполнения", + "prefillEmpty": "Модели не найдены", + "prefillFailed": "Ошибка поиска", + "promptCachingHint": "Включайте только если модель поддерживает кэширование, и задайте цены кэша ниже", + "cachePricingTitle": "Цены кэша" + }, + "actions": { + "edit": "Редактировать", + "more": "Больше действий", + "delete": "Удалить" + }, + "toast": { + "createSuccess": "Модель добавлена", + "updateSuccess": "Модель обновлена", + "deleteSuccess": "Модель удалена", + "saveFailed": "Ошибка сохранения", + "deleteFailed": "Ошибка удаления" + } +} diff --git a/messages/ru/settings/providers/autoSort.json b/messages/ru/settings/providers/autoSort.json new file mode 100644 index 000000000..cecb10d7f --- /dev/null +++ b/messages/ru/settings/providers/autoSort.json @@ -0,0 +1,16 @@ +{ + "button": "Авто сортировка приоритета", + "changeCount": "{count} поставщиков будет обновлено", + "changesTitle": "Детали изменений", + "confirm": "Применить изменения", + "costMultiplierHeader": "Множитель стоимости", + "dialogDescription": "Автоматически назначить приоритет на основе множителя стоимости (низкая стоимость = высокий приоритет)", + "dialogTitle": "Автоматическая сортировка приоритета поставщиков", + "error": "Не удалось обновить приоритеты", + "noChanges": "Изменения не требуются (уже отсортировано)", + "priorityChangeHeader": "Изменение приоритета", + "priorityHeader": "Приоритет", + "providerHeader": "Поставщик", + "providersHeader": "Поставщики", + "success": "Обновлён приоритет для {count} поставщиков" +} diff --git a/messages/ru/settings/providers/filter.json b/messages/ru/settings/providers/filter.json new file mode 100644 index 000000000..075abdc72 --- /dev/null +++ b/messages/ru/settings/providers/filter.json @@ -0,0 +1,13 @@ +{ + "circuitBroken": "Сбой соединения", + "groups": { + "all": "Все", + "default": "default", + "label": "Группы:" + }, + "status": { + "active": "Активные", + "all": "Все статусы", + "inactive": "Неактивные" + } +} diff --git a/messages/ru/settings/providers/form/apiTest.json b/messages/ru/settings/providers/form/apiTest.json new file mode 100644 index 000000000..94382567a --- /dev/null +++ b/messages/ru/settings/providers/form/apiTest.json @@ -0,0 +1,157 @@ +{ + "apiFormat": "Тип провайдера", + "apiFormatDesc": "По умолчанию синхронизируется с типом провайдера в конфигурации маршрутизации", + "chunksCount": "Получено {count} чанков ({format})", + "chunksReceived": "Полученные чанки", + "close": "Закрыть", + "copyFailed": "Не удалось скопировать", + "copyFormat": { + "errorDetails": "Детали ошибки", + "message": "Сообщение", + "testResult": "Результат теста" + }, + "copyResult": "Копировать результат", + "copySuccess": "Скопировано в буфер обмена", + "customConfig": "Пользовательский", + "customPayloadDesc": "Введите пользовательский JSON payload для замены тела запроса по умолчанию", + "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", + "disclaimer": { + "confirmConfig": "Пожалуйста, проверьте URL провайдера, API ключ и конфигурацию модели", + "realRequest": "Этот тест отправляет реальный запрос провайдеру и может потреблять небольшую квоту", + "resultReference": "Результаты могут варьироваться в зависимости от провайдера и служат только для справки", + "title": "Внимание" + }, + "error": "Сообщение об ошибке", + "failed": "Не удалось", + "fillKeyFirst": "Пожалуйста, сначала заполните API ключ", + "fillUrlFirst": "Пожалуйста, сначала заполните URL провайдера", + "formatAnthropicMessages": "Claude (Anthropic Messages API)", + "formatOpenAIChat": "OpenAI Compatible", + "formatOpenAIResponses": "Codex (Response API)", + "geminiAuthFallback": { + "desc": "Реальный прокси использует только аутентификацию через заголовок, что может вызвать ошибки", + "warning": "Ошибка аутентификации через заголовок, использована URL-аутентификация" + }, + "invalidUrl": "URL провайдера недействителен (только http/https)", + "model": "Модель", + "noResult": "Тест успешен, но результат не возвращен", + "presetConfig": "Пресет", + "presetDesc": "Шаблоны пресетов содержат аутентичные паттерны CLI-запросов для верификации релейного сервиса", + "requestConfig": "Конфигурация запроса", + "response": "Предварительный просмотр ответа", + "responseModel": "Модель ответа", + "responseTime": "Время ответа", + "resultCard": { + "copyText": { + "contentCheck": "Проверка контента", + "error": "Ошибка", + "httpCheck": "Проверка HTTP", + "httpStatus": "HTTP статус", + "inputOutput": "Ввод {input} / Вывод {output} токенов", + "latency": "Задержка", + "latencyCheck": "Проверка задержки", + "message": "Сообщение", + "model": "Модель", + "response": "Ответ", + "status": "Статус", + "testedAt": "Время теста", + "usage": "Использование", + "validationDetails": "Детали валидации" + }, + "dialogTitle": "Детали теста провайдера", + "errorDetails": { + "title": "Детали ошибки", + "type": "Тип ошибки" + }, + "judgment": "Решение", + "labels": { + "content": "Контент", + "error": "Ошибка", + "firstByte": "1 байт", + "http": "HTTP", + "latency": "Задержка", + "model": "Модель", + "responsePreview": "Предпросмотр ответа", + "totalLatency": "Общая задержка" + }, + "rawResponse": { + "hint": "Здесь отображается необработанное содержимое ответа. Вы можете проверить, содержит ли ответ ключевое слово.", + "title": "Тело ответа" + }, + "status": { + "green": "Доступен", + "red": "Недоступен", + "yellow": "Нестабильно" + }, + "streamInfo": { + "chunksCount": "Количество чанков", + "isStreaming": "Потоковая передача", + "no": "Нет", + "title": "Информация о потоковом ответе", + "yes": "Да" + }, + "timing": { + "firstByte": "1 байт", + "testedAt": "Время теста", + "title": "Информация о времени", + "totalLatency": "Общая задержка" + }, + "tokenUsage": { + "cacheCreation": "Создание кэша", + "cacheRead": "Чтение кэша", + "input": "Ввод", + "output": "Вывод", + "title": "Использование токенов" + }, + "validation": { + "content": { + "failed": "Цель не найдена", + "passed": "Содержит целевую строку", + "target": "Цель", + "title": "Уровень 3: Валидация контента" + }, + "failed": "Не пройдено", + "http": { + "failed": "4xx/5xx ошибка", + "passed": "2xx/3xx успех", + "statusCode": "Код статуса", + "title": "Уровень 1: HTTP статус" + }, + "latency": { + "actual": "Фактическая задержка", + "failed": "Превышен порог", + "passed": "В пределах порога", + "title": "Уровень 2: Порог задержки" + }, + "passed": "Пройдено", + "timeout": "Тайм-аут", + "title": "Детали трехуровневой валидации" + } + }, + "selectApiFormat": "Выберите тип провайдера для тестирования", + "selectPreset": "Выберите шаблон пресета", + "streamFormat": "Формат потока", + "streamInfo": "Информация о потоковом ответе", + "streamResponse": "Потоковый ответ", + "success": "Успешно", + "successContains": "Ключевое слово успеха", + "successContainsDesc": "Ответ должен содержать это ключевое слово для признания успешным", + "successContainsPlaceholder": "pong", + "testApi": "Тест модели провайдера", + "testFailed": "Тест не пройден", + "testFailedRetry": "Тест не пройден, попробуйте снова", + "testModel": "Тестовая модель", + "testModelDesc": "Оставьте пустым для использования модели по умолчанию или введите вручную", + "testSuccess": "Тест модели успешен", + "testing": "Тестирование...", + "timeout": { + "desc": "Максимальное время ожидания тестового запроса (5-120 сек)", + "geminiHint": ", для моделей Gemini Thinking рекомендуется 60+ сек", + "label": "Таймаут (секунды)" + }, + "truncatedBrief": "Показаны первые {length} символов, нажмите «Подробнее» для полного просмотра", + "truncatedPreview": "Показаны первые {length} символов, скопируйте для просмотра полного текста", + "unknown": "Неизвестно", + "usage": "Использование токенов", + "viewDetails": "Подробнее" +} diff --git a/messages/ru/settings/providers/form/buttons.json b/messages/ru/settings/providers/form/buttons.json new file mode 100644 index 000000000..731b31a38 --- /dev/null +++ b/messages/ru/settings/providers/form/buttons.json @@ -0,0 +1,9 @@ +{ + "collapseAll": "Свернуть все расширенные настройки", + "delete": "Удалить", + "expandAll": "Развернуть все расширенные настройки", + "submit": "Подтвердить добавление", + "submitting": "Добавление...", + "update": "Подтвердить обновление", + "updating": "Обновление..." +} diff --git a/messages/ru/settings/providers/form/common.json b/messages/ru/settings/providers/form/common.json new file mode 100644 index 000000000..fd4722431 --- /dev/null +++ b/messages/ru/settings/providers/form/common.json @@ -0,0 +1,3 @@ +{ + "core": "Основная" +} diff --git a/messages/ru/settings/providers/form/deleteDialog.json b/messages/ru/settings/providers/form/deleteDialog.json new file mode 100644 index 000000000..728e8e1f1 --- /dev/null +++ b/messages/ru/settings/providers/form/deleteDialog.json @@ -0,0 +1,6 @@ +{ + "cancel": "Отмена", + "confirm": "Подтвердить удаление", + "description": "Удалить провайдера «{name}»? Это действие необратимо.", + "title": "Удалить провайдера" +} diff --git a/messages/ru/settings/providers/form/errors.json b/messages/ru/settings/providers/form/errors.json new file mode 100644 index 000000000..fb82e35be --- /dev/null +++ b/messages/ru/settings/providers/form/errors.json @@ -0,0 +1,8 @@ +{ + "addFailed": "Не удалось добавить провайдера", + "deleteFailed": "Не удалось удалить провайдера", + "groupTagTooLong": "Список групп провайдера слишком длинный (макс. {max} символов всего)", + "invalidUrl": "Введите корректный адрес API", + "invalidWebsiteUrl": "Введите корректный адрес сайта провайдера", + "updateFailed": "Не удалось обновить провайдера" +} diff --git a/messages/ru/settings/providers/form/failureThresholdConfirmDialog.json b/messages/ru/settings/providers/form/failureThresholdConfirmDialog.json new file mode 100644 index 000000000..77c2f583f --- /dev/null +++ b/messages/ru/settings/providers/form/failureThresholdConfirmDialog.json @@ -0,0 +1,13 @@ +{ + "cancel": "Отмена", + "confirm": "Подтвердить сохранение", + "confirmQuestion": "Вы уверены, что хотите сохранить эту конфигурацию?", + "descriptionDisabledAction": "отключение автоматического выключателя", + "descriptionDisabledMiddle": ", что означает ", + "descriptionDisabledPrefix": "Вы устанавливаете порог сбоев автоматического выключателя на ", + "descriptionDisabledSuffix": ". Провайдер не будет отключаться из-за последовательных сбоев.", + "descriptionDisabledValue": "0", + "descriptionHighValuePrefix": "Вы устанавливаете порог сбоев автоматического выключателя на ", + "descriptionHighValueSuffix": ", что является высоким значением и может привести к отключению провайдера только после многочисленных сбоев.", + "title": "Подтвердите особую конфигурацию" +} diff --git a/messages/ru/settings/providers/form/key.json b/messages/ru/settings/providers/form/key.json new file mode 100644 index 000000000..719e8fb39 --- /dev/null +++ b/messages/ru/settings/providers/form/key.json @@ -0,0 +1,7 @@ +{ + "currentKey": "Текущий ключ: {key}", + "label": "API ключ", + "leaveEmpty": "(Оставьте пустым, чтобы не менять)", + "leaveEmptyDesc": "Пустое значение — без изменений", + "placeholder": "Введите API ключ" +} diff --git a/messages/ru/settings/providers/form/maxRetryAttempts.json b/messages/ru/settings/providers/form/maxRetryAttempts.json new file mode 100644 index 000000000..74ad25097 --- /dev/null +++ b/messages/ru/settings/providers/form/maxRetryAttempts.json @@ -0,0 +1,5 @@ +{ + "desc": "С учётом первой попытки: сколько раз максимум пробовать один провайдер перед переключением. Оставьте пустым для значения по умолчанию.", + "label": "Максимум попыток на провайдер", + "placeholder": "2" +} diff --git a/messages/ru/settings/providers/form/modelRedirect.json b/messages/ru/settings/providers/form/modelRedirect.json new file mode 100644 index 000000000..6ec29dce4 --- /dev/null +++ b/messages/ru/settings/providers/form/modelRedirect.json @@ -0,0 +1,14 @@ +{ + "add": "Добавить", + "addNewRule": "Добавить новое правило", + "alreadyExists": "Правило перенаправления для модели \"{model}\" уже существует", + "currentRules": "Текущие правила ({count})", + "description": "Перенаправлять запросы моделей от клиента Claude Code (например, claude-sonnet-4.5) к моделям, поддерживаемым вышестоящим провайдером (например, glm-4.6, gemini-pro). Используется для оптимизации затрат или подключения сторонних AI-сервисов.", + "emptyState": "Пока нет правил перенаправления. После добавления правил система автоматически переписывает названия моделей в запросах.", + "sourceEmpty": "Название исходной модели не может быть пустым", + "sourceModel": "Запрашиваемая пользователем модель", + "sourcePlaceholder": "Например: claude-sonnet-4-5-20250929", + "targetEmpty": "Название целевой модели не может быть пустым", + "targetModel": "Фактически перенаправляемая модель", + "targetPlaceholder": "Например: glm-4.6" +} diff --git a/messages/ru/settings/providers/form/modelSelect.json b/messages/ru/settings/providers/form/modelSelect.json new file mode 100644 index 000000000..2fb1713ef --- /dev/null +++ b/messages/ru/settings/providers/form/modelSelect.json @@ -0,0 +1,20 @@ +{ + "allowAllModels": "Разрешить все модели {type}", + "claude": "Claude", + "clear": "Очистить", + "gemini": "Gemini", + "loading": "Загрузка...", + "manualAdd": "Добавить модель вручную", + "manualDesc": "Поддержка добавления любого названия модели (не ограничено прайс-листом)", + "manualPlaceholder": "Введите название модели (например, gpt-5-turbo)", + "notFound": "Модели не найдены", + "openai": "OpenAI", + "refresh": "Обновить список моделей", + "searchPlaceholder": "Поиск по названию модели...", + "selectAll": "Выбрать все ({count})", + "selectedCount": "Выбрано моделей: {count}", + "sourceFallback": "Локально", + "sourceFallbackDesc": "Используется локальный прайс-лист (upstream недоступен или не поддерживается)", + "sourceUpstream": "Upstream", + "sourceUpstreamDesc": "Список моделей из API провайдера" +} diff --git a/messages/ru/settings/providers/form/name.json b/messages/ru/settings/providers/form/name.json new file mode 100644 index 000000000..1140a7325 --- /dev/null +++ b/messages/ru/settings/providers/form/name.json @@ -0,0 +1,4 @@ +{ + "label": "Имя провайдера *", + "placeholder": "например: Zhipu" +} diff --git a/messages/ru/settings/providers/form/providerTypes.json b/messages/ru/settings/providers/form/providerTypes.json new file mode 100644 index 000000000..3799c1c0f --- /dev/null +++ b/messages/ru/settings/providers/form/providerTypes.json @@ -0,0 +1,10 @@ +{ + "claude": "Claude (Anthropic Messages API)", + "claudeAuth": "Claude (Anthropic Auth Token)", + "codex": "Codex (Response API)", + "gemini": "Gemini (Google Gemini API)", + "geminiCli": "Gemini CLI", + "geminiCliDisabled": "", + "openaiCompatible": "OpenAI Compatible", + "openaiCompatibleDisabled": " - в разработке" +} diff --git a/messages/ru/settings/providers/form/proxyTest.json b/messages/ru/settings/providers/form/proxyTest.json new file mode 100644 index 000000000..89e715a06 --- /dev/null +++ b/messages/ru/settings/providers/form/proxyTest.json @@ -0,0 +1,21 @@ +{ + "connectionFailed": "Соединение не удалось", + "connectionMethod": "Способ соединения:", + "connectionSuccess": "Соединение успешно", + "direct": "Прямое", + "errorType": "Тип ошибки:", + "fillUrlFirst": "Пожалуйста, сначала заполните URL провайдера", + "networkError": "Сетевая ошибка:", + "noResult": "Тест успешен, но результат не возвращен", + "proxy": "Прокси", + "proxyError": "Ошибка прокси:", + "responseTime": "Время отклика:", + "statusCode": "Код состояния:", + "testConnection": "Проверить соединение", + "testFailed": "Тест не пройден", + "testFailedRetry": "Тест не пройден, попробуйте снова", + "testing": "Тестирование...", + "timeoutError": "Тайм-аут соединения (5 секунд). Проверьте:\n1. Доступен ли прокси-сервер\n2. Правильность адреса и порта прокси\n3. Правильность данных аутентификации прокси", + "viaDirect": "(прямое)", + "viaProxy": "(через прокси)" +} diff --git a/messages/ru/settings/providers/form/sections.json b/messages/ru/settings/providers/form/sections.json new file mode 100644 index 000000000..6da02eba6 --- /dev/null +++ b/messages/ru/settings/providers/form/sections.json @@ -0,0 +1,313 @@ +{ + "apiTest": { + "desc": "Проверяет доступность модели у провайдера. По умолчанию соответствует типу провайдера, выбранному в настройках маршрутизации.", + "summary": "Проверка связности провайдера и модели", + "testLabel": "Тест модели провайдера", + "title": "Тест модели провайдера" + }, + "circuitBreaker": { + "desc": "Автоматическое отключение при серии неудач для защиты качества сервиса", + "failureThreshold": { + "desc": "Сколько подряд неудач для срабатывания", + "label": "Порог неудач", + "placeholder": "5" + }, + "maxRetryAttempts": { + "desc": "Общее число попыток (включая первую) перед переключением на другого провайдера. Оставьте пустым для значения по умолчанию.", + "label": "Максимум попыток на провайдера", + "placeholder": "2" + }, + "openDuration": { + "desc": "Через сколько перейти в полураскрытое состояние", + "label": "Длительность блокировки (мин)", + "placeholder": "30" + }, + "successThreshold": { + "desc": "Сколько успешных запросов в полураскрытом режиме для полного восстановления", + "label": "Порог восстановления", + "placeholder": "2" + }, + "summary": "{failureThreshold} неудач / {openDuration} мин. блокировки / {successThreshold} успеха для восстановления / до {maxRetryAttempts} попыток на провайдера", + "title": "Предохранитель" + }, + "codexStrategy": { + "desc": "Управление полем instructions в запросах Codex; влияет на совместимость с шлюзами", + "hint": "Подсказка: некоторым строгим шлюзам Codex (например, 88code, foxcode) требуются официальные инструкции. Выберите «Авто» или «Только официальные».", + "select": { + "auto": { + "desc": "Передавать инструкции клиента; при 400 повтор с официальным промптом", + "label": "Авто (рекомендуется)" + }, + "force": { + "desc": "Всегда использовать официальные инструкции Codex CLI (~4000+ символов)", + "label": "Только официальные" + }, + "keep": { + "desc": "Всегда передавать инструкции клиента без автоповторной попытки (для более лояльных прокси)", + "label": "Как есть" + }, + "label": "Выбор стратегии", + "placeholder": "Выберите стратегию" + }, + "summary": { + "auto": "Авто (рекомендуется)", + "force": "Только официальные", + "keep": "Как есть" + }, + "title": "Политика Codex Instructions" + }, + "mcpPassthrough": { + "desc": "При включении передаёт вызовы инструментов MCP указанному AI-провайдеру (например, minimax для распознавания изображений, веб-поиска)", + "hint": "Подсказка: сквозная передача MCP позволяет клиенту Claude Code использовать возможности инструментов, предоставляемых сторонними AI-провайдерами (например, распознавание изображений, веб-поиск)", + "select": { + "custom": { + "desc": "Сквозная передача в пользовательский сервис MCP (зарезервировано, не реализовано)", + "label": "Пользовательский" + }, + "glm": { + "desc": "Сквозная передача в сервис GLM MCP (поддержка анализа изображений, видео и т.д.)", + "label": "GLM" + }, + "label": "Тип сквозной передачи", + "minimax": { + "desc": "Сквозная передача в сервис minimax MCP (поддержка распознавания изображений, веб-поиска и т.д.)", + "label": "Minimax" + }, + "none": { + "desc": "Не включать сквозную передачу MCP (по умолчанию)", + "label": "Отключено" + }, + "placeholder": "Выберите тип сквозной передачи" + }, + "summary": { + "custom": "Пользовательский (Зарезервировано)", + "glm": "GLM", + "minimax": "Minimax", + "none": "Отключено" + }, + "title": "Конфигурация сквозной передачи MCP", + "urlAuto": "Автоматически извлечено: {url}", + "urlDesc": "Базовый URL сервиса MCP. Оставьте пустым для автоматического извлечения из URL провайдера", + "urlLabel": "URL сквозной передачи MCP", + "urlPlaceholder": "https://api.minimaxi.com" + }, + "proxy": { + "desc": "Настройте прокси для улучшения соединения (поддержка HTTP, HTTPS, SOCKS4, SOCKS5)", + "fallback": { + "desc": "При включении будет предпринята попытка прямого соединения при сбое прокси", + "label": "Откат к прямому соединению при сбое прокси" + }, + "summary": { + "configured": "Прокси настроен", + "fallback": " (включён откат)", + "none": "Не настроено" + }, + "test": { + "desc": "Проверка доступа к URL провайдера через прокси (запрос HEAD, без списания средств)", + "label": "Проверка соединения" + }, + "title": "Прокси", + "url": { + "formats": "Поддерживаемые форматы:", + "label": "URL прокси", + "optional": "(необязательно)", + "placeholder": "например: http://proxy.example.com:8080 или socks5://127.0.0.1:1080" + } + }, + "rateLimit": { + "dailyResetMode": { + "desc": { + "fixed": "Сбрасывать квоту каждый день в фиксированное время", + "rolling": "Считать от первой заявки, сброс через 24 часа" + }, + "label": "Режим ежедневного сброса", + "options": { + "fixed": "Фиксированное время", + "rolling": "Скользящее окно (24 часа)" + } + }, + "dailyResetTime": { + "label": "Время ежедневного сброса (HH:mm)" + }, + "limit5h": { + "label": "Лимит за 5 часов (USD)", + "placeholder": "Пусто — без ограничений" + }, + "limitConcurrent": { + "label": "Лимит параллельных сессий", + "placeholder": "0 — без ограничений" + }, + "limitDaily": { + "label": "Дневной лимит расходов (USD)", + "placeholder": "Пусто — без ограничений" + }, + "limitMonthly": { + "label": "Месячный лимит (USD)", + "placeholder": "Пусто — без ограничений" + }, + "limitTotal": { + "label": "Общий лимит (USD)", + "placeholder": "Пусто — без ограничений" + }, + "limitWeekly": { + "label": "Недельный лимит (USD)", + "placeholder": "Пусто — без ограничений" + }, + "summary": { + "concurrent": "Параллельно: {count}", + "daily": "день: {amount} (сброс {resetTime})", + "fiveHour": "5ч: ${amount}", + "monthly": "Месяц: ${amount}", + "none": "Без ограничений", + "total": "Всего: ${amount}", + "weekly": "Неделя: ${amount}" + }, + "title": "Ограничения" + }, + "routing": { + "cacheTtl": { + "desc": "Принудительно задать TTL кэша промптов; влияет только на запросы с cache_control.", + "label": "Переопределение Cache TTL", + "options": { + "1h": "1 час", + "5m": "5 минут", + "inherit": "Не переопределять (следовать клиенту)" + } + }, + "codexOverrides": { + "parallelToolCalls": { + "help": "Управляет тем, разрешены ли параллельные вызовы инструментов. \"inherit\" следует запросу клиента. Отключение может снизить параллельность вызовов инструментов.", + "label": "Переопределение параллельных tool calls", + "options": { + "false": "Принудительно отключить", + "inherit": "Не переопределять (следовать клиенту)", + "true": "Принудительно включить" + } + }, + "reasoningEffort": { + "help": "Управляет тем, сколько усилий модель тратит на рассуждения перед ответом. \"inherit\" следует запросу клиента, остальные значения принудительно задают reasoning.effort. Примечание: \"none\" поддерживается только моделями GPT-5.1, а \"xhigh\" — только GPT-5.1-Codex-Max. Неподдерживаемые значения приведут к ошибке у апстрима.", + "label": "Переопределение уровня рассуждений", + "options": { + "high": "high", + "inherit": "Не переопределять (следовать клиенту)", + "low": "low", + "medium": "medium (по умолчанию)", + "minimal": "minimal", + "none": "none (только GPT-5.1)", + "xhigh": "xhigh (только GPT-5.1-Codex-Max)" + } + }, + "reasoningSummary": { + "help": "Управляет тем, возвращает ли Responses API сводку рассуждений. auto — кратко, detailed — подробнее. \"inherit\" следует запросу клиента.", + "label": "Переопределение сводки рассуждений", + "options": { + "auto": "auto", + "detailed": "detailed", + "inherit": "Не переопределять (следовать клиенту)" + } + }, + "textVerbosity": { + "help": "Управляет подробностью ответа. low — короче, high — подробнее. \"inherit\" следует запросу клиента.", + "label": "Переопределение подробности текста", + "options": { + "high": "high", + "inherit": "Не переопределять (следовать клиенту)", + "low": "low", + "medium": "medium (по умолчанию)" + } + } + }, + "context1m": { + "desc": "Настройка поддержки контекстного окна 1M. Применяется только к моделям Sonnet (claude-sonnet-4-5, claude-sonnet-4). При включении применяется многоуровневая тарификация.", + "label": "Контекстное окно 1M", + "options": { + "disabled": "Отключено", + "forceEnable": "Принудительно включить", + "inherit": "Наследовать (следовать клиенту)" + } + }, + "joinClaudePool": { + "desc": "При включении провайдер участвует в балансировке нагрузки вместе с провайдерами типа Claude", + "help": "Доступно только при наличии перенаправления на модели claude-*. При запросе моделей claude-* провайдер также участвует в выборе.", + "label": "Включить пул маршрутизации Claude" + }, + "modelRedirects": { + "label": "Перенаправление моделей", + "optional": "(необязательно)" + }, + "modelWhitelist": { + "allowAll": "✓ Разрешить все модели (рекомендуется)", + "desc": "Ограничьте модели, которые может обслуживать провайдер. По умолчанию доступны все модели данного типа.", + "label": "Разрешённые модели", + "moreModels": "+{count} ещё", + "optional": "(необязательно)", + "selectedOnly": "Разрешены только выбранные {count} моделей. Другие модели не будут направляться к этому провайдеру.", + "title": "Список разрешённых моделей" + }, + "preserveClientIp": { + "desc": "Передавать x-forwarded-for / x-real-ip в апстрим (может раскрыть реальный IP клиента)", + "help": "По умолчанию выключено для приватности. Включайте только если апстриму нужен IP пользователя.", + "label": "Пробрасывать IP клиента" + }, + "providerType": { + "desc": "(определяет политику выбора)", + "label": "Тип провайдера", + "placeholder": "Выберите тип провайдера" + }, + "providerTypeDesc": "Выберите формат API провайдера.", + "providerTypeDisabledNote": "Примечание: функции типа OpenAI Compatible находятся в разработке и пока недоступны", + "scheduleParams": { + "costMultiplier": { + "desc": "Множитель при расчёте стоимости. Официальный=1.0, дешевле на 20%=0.8, дороже на 20%=1.2 (до 4 знаков после запятой)", + "label": "Множитель стоимости", + "placeholder": "1.0" + }, + "group": { + "desc": "Метка группы. Пользователь может использовать провайдера только если его providerGroup совпадает. Пример: значение \"premium\" — только для пользователей с providerGroup=\"premium\"", + "label": "Группа провайдера", + "placeholder": "например: premium, economy" + }, + "priority": { + "desc": "Меньше — выше приоритет (0 — наивысший). Система выбирает только из провайдеров с максимальным приоритетом. Рекомендации: основной=0, резерв=1, аварийный=2", + "label": "Приоритет", + "placeholder": "0" + }, + "title": "Параметры выбора", + "weight": { + "desc": "Взвешенное случайное распределение. В пределах одного приоритета больший вес увеличивает вероятность выбора. Пример 1:2:3 ≈ 16%:33%:50%", + "label": "Вес", + "placeholder": "1" + } + }, + "summary": { + "models": "{count} моделей в белом списке", + "none": "Не настроено", + "redirects": "{count} правил перенаправления" + }, + "title": "Маршрутизация" + }, + "timeout": { + "desc": "Установить время ожидания запроса, 0 означает отключение тайм-аута", + "disableHint": "Установите 0 для отключения тайм-аута (только для сценариев отката канарейки, не рекомендуется)", + "nonStreamingTotal": { + "core": "true", + "desc": "Полный тайм-аут непотоковой передачи, диапазон 60-1200 секунд, значение по умолчанию 600 секунд (10 минут)", + "label": "Полный тайм-аут непотоковой передачи (секунды)", + "placeholder": "600" + }, + "streamingFirstByte": { + "core": "true", + "desc": "Тайм-аут первого байта потоковой передачи, диапазон 1-120 секунд, значение по умолчанию 30 секунд", + "label": "Тайм-аут первого байта потока (секунды)", + "placeholder": "30" + }, + "streamingIdle": { + "core": "true", + "desc": "Тайм-аут простоя потоковой передачи, диапазон 60-600 секунд, введите 0 для отключения (предотвращение застревания)", + "label": "Тайм-аут простоя потока (секунды)", + "placeholder": "60" + }, + "summary": "1 байт: {streaming}с | поток: {idle}с | не поток: {nonStreaming}с", + "title": "Конфигурация тайм-аута" + } +} diff --git a/messages/ru/settings/providers/form/strings.json b/messages/ru/settings/providers/form/strings.json new file mode 100644 index 000000000..abc81bdb8 --- /dev/null +++ b/messages/ru/settings/providers/form/strings.json @@ -0,0 +1,204 @@ +{ + "addRedirect": "Добавить переправку", + "allowAllModels": "✓ Разрешить все модели (рекомендуется)", + "apiAddress": "Адрес API", + "apiAddressPlaceholder": "Например: https://open.bigmodel.cn/api/anthropic", + "apiAddressRequired": "Адрес API *", + "apiKey": "API ключ", + "apiKeyCurrent": "Текущий ключ:", + "apiKeyLeaveEmpty": "(оставьте пустым, чтобы не изменять)", + "apiKeyLeaveEmptyDesc": "Оставьте пустым, чтобы не изменять ключ", + "apiKeyOptional": "Оставьте пустым, чтобы оставить текущий ключ", + "apiKeyPlaceholder": "Введите API ключ", + "apiKeyRequired": "API ключ *", + "baseUrl": "Базовый URL", + "baseUrlPlaceholder": "например: https://open.bigmodel.cn/api/anthropic", + "baseUrlRequired": "Пожалуйста, сначала заполните URL поставщика", + "circuitBreakerConfig": "Конфигурация автоматического выключателя", + "circuitBreakerConfigSummary": "{failureThreshold} сбоев / {openDuration} мин. размыкания / {successThreshold} успехов для восстановления / максимум {maxRetryAttempts} попыток на провайдера", + "circuitBreakerDesc": "Автоматическое размыкание при последовательных сбоях провайдера для предотвращения влияния на общее качество сервиса", + "clearSearch": "Очистить поиск", + "codexInstructions": "Политика инструкций Codex", + "codexInstructionsAuto": "Автоматически (рекомендуется)", + "codexInstructionsDesc": "(определяет политику планирования)", + "codexInstructionsForce": "Принудительно официальные", + "codexInstructionsKeep": "Сохранить оригинал", + "codexStrategyAutoDesc": "Передавать instructions клиента, автоматически повторять с официальным prompt при ошибке 400", + "codexStrategyAutoLabel": "Автоматически (рекомендуется)", + "codexStrategyConfig": "Стратегия Codex Instructions", + "codexStrategyConfigAuto": "Автоматически (рекомендуется)", + "codexStrategyConfigForce": "Принудительно официальные", + "codexStrategyConfigKeep": "Передавать как есть", + "codexStrategyDesc": "Управляет обработкой поля instructions в запросах Codex, влияет на совместимость с вышестоящими узлами", + "codexStrategyForceDesc": "Всегда использовать официальные Codex CLI instructions (около 4000+ символов)", + "codexStrategyForceLabel": "Принудительно официальные", + "codexStrategyHint": "Подсказка: некоторые строгие узлы Codex (например, 88code, foxcode) требуют официальные instructions, выберите стратегию \"Автоматически\" или \"Принудительно официальные\"", + "codexStrategyKeepDesc": "Всегда передавать instructions клиента без автоматического повтора (подходит для гибких узлов)", + "codexStrategyKeepLabel": "Передавать как есть", + "codexStrategySelect": "Выбор стратегии", + "collapseAll": "Свернуть все расширенные настройки", + "confirmAdd": "Подтвердить добавление", + "confirmAddPending": "Добавление...", + "confirmUpdate": "Подтвердить обновление", + "confirmUpdatePending": "Обновление...", + "costMultiplier": "Коэф цены", + "costMultiplierDesc": "например: A (стоимость 1.0x), C (стоимость 0.8x)", + "costMultiplierLabel": "Коэффициент стоимости", + "costMultiplierPlaceholder": "1.0", + "deleteButton": "Удалить", + "dialogDescription": "Настройте детали провайдера и расширенные параметры.", + "enabled": "Включено", + "expandAll": "Развернуть все расширенные настройки", + "failureThreshold": "Порог сбоев (раз)", + "failureThresholdDesc": "Сколько последовательных сбоев до размыкания", + "failureThresholdPlaceholder": "5", + "filterAllProviders": "Все поставщики", + "filterByType": "Фильтр по типу", + "filterProvider": "Фильтр типа поставщика", + "group": "Группа", + "groupPlaceholder": "например: premium, economy", + "joinClaudePool": "Присоединиться к пулу планирования Claude", + "joinClaudePoolDesc": "При включении этот поставщик будет участвовать в балансировке нагрузки вместе с поставщиками типа Claude", + "joinClaudePoolHelp": "Доступно только при наличии перенаправлений на модели claude-* в конфигурации. При включении этот поставщик также будет участвовать в выборе при запросах моделей claude-*.", + "leaveEmpty": "Оставьте пустым для неограниченного доступа", + "limit0Means": "0 означает без ограничений", + "limit5hLabel": "Лимит расходов за 5 часов (USD)", + "limitAmount5h": "Лимит расходов за 5 часов (USD)", + "limitAmount5hDesc": "например: Поставщик B имеет лимит $10, уже потрачено $9.8", + "limitAmountMonthly": "Месячный лимит расходов (USD)", + "limitAmountWeekly": "Недельный лимит расходов (USD)", + "limitConcurrent": "Лимит параллельных сеансов", + "limitConcurrentDesc": "например: Поставщик C имеет лимит 2, в данный момент 2 активных сеанса", + "limitConcurrentLabel": "Лимит одновременных сеансов", + "limitMonthlyLabel": "Месячный лимит расходов (USD)", + "limitPlaceholder0": "0 означает без ограничений", + "limitPlaceholderUnlimited": "Оставьте пустым для неограниченного доступа", + "limitWeeklyLabel": "Недельный лимит расходов (USD)", + "modelRedirects": "Перенаправление моделей", + "modelRedirectsAddNew": "Добавить новое правило", + "modelRedirectsCurrentRules": "Текущие правила ({count})", + "modelRedirectsDesc": "Переправить запросы Claude к другим поддерживаемым моделям", + "modelRedirectsEmpty": "Нет правил перенаправления. После добавления правил система автоматически перезапишет имена моделей в запросах.", + "modelRedirectsExists": "Правило перенаправления для модели \"{model}\" уже существует", + "modelRedirectsLabel": "Конфигурация перенаправления моделей", + "modelRedirectsOptional": "(необязательно)", + "modelRedirectsSourceModel": "Модель запроса пользователя", + "modelRedirectsSourcePlaceholder": "например: claude-sonnet-4-5-20250929", + "modelRedirectsSourceRequired": "Имя исходной модели не может быть пустым", + "modelRedirectsTargetModel": "Фактически перенаправляемая модель", + "modelRedirectsTargetPlaceholder": "например: glm-4.6", + "modelRedirectsTargetRequired": "Имя целевой модели не может быть пустым", + "modelWhitelist": "Белый список моделей", + "modelWhitelistAllowAll": "Разрешить все модели {type}", + "modelWhitelistAllowAllClause": "Разрешить все модели Claude", + "modelWhitelistAllowAllOpenAI": "Разрешить все модели OpenAI", + "modelWhitelistClear": "Очистить", + "modelWhitelistDesc": "Ограничить модели, которые может обрабатывать этот поставщик. По умолчанию поставщик может обрабатывать все модели этого типа.", + "modelWhitelistLabel": "Разрешенные модели", + "modelWhitelistLoading": "Загрузка...", + "modelWhitelistManualAdd": "Добавить модель вручную", + "modelWhitelistManualDesc": "Поддерживает добавление любого имени модели (не ограничено прайс-листом)", + "modelWhitelistManualPlaceholder": "Введите имя модели (например, gpt-5-turbo)", + "modelWhitelistNotFound": "Модели не найдены", + "modelWhitelistSearchPlaceholder": "Поиск по имени модели...", + "modelWhitelistSelectAll": "Выбрать все ({count})", + "modelWhitelistSelected": "Выбрано {count} моделей", + "modelWhitelistSelectedOnly": "Разрешены только выбранные {count} моделей. Запросы других моделей не будут направлены к этому поставщику.", + "namePlaceholder": "Введите имя поставщика", + "openDuration": "Длительность размыкания (минуты)", + "openDurationDesc": "Время автоматического перехода в полуоткрытое состояние после размыкания", + "openDurationPlaceholder": "30", + "priority": "Приоритет", + "priorityDesc": "В пределах одного приоритета сортировка по множителю стоимости от низкого к высокому", + "priorityLabel": "Приоритет", + "priorityPlaceholder": "0", + "providerGroupDesc": "Метка группы поставщика. Пользователь может использовать этого поставщика только если его providerGroup совпадает с этим значением. Пример: установка \"premium\" означает использование только пользователями с providerGroup=\"premium\"", + "providerGroupLabel": "Группа поставщика", + "providerGroupPlaceholder": "например: premium, economy", + "providerName": "Имя поставщика", + "providerNamePlaceholder": "например: Zhipu", + "providerNameRequired": "Имя поставщика *", + "providerType": "Тип поставщика", + "providerTypeDesc": "Выберите тип формата API поставщика.", + "providerTypeDisabledNote": "Примечание: функции типов Gemini CLI и OpenAI Compatible находятся в разработке и временно недоступны", + "proxy": "Прокси", + "proxyAddressFormats": "Поддерживаемые форматы:", + "proxyAddressLabel": "Адрес прокси", + "proxyAddressOptional": "(необязательно)", + "proxyAddressPlaceholder": "например: http://proxy.example.com:8080 или socks5://127.0.0.1:1080", + "proxyConfig": "Конфигурация прокси", + "proxyConfigDesc": "Настройка прокси-сервера для улучшения подключения к поставщику (поддерживает HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyConfigNone": "Не настроен", + "proxyConfigSummary": "Прокси настроен", + "proxyConfigSummaryFallback": " (откат включен)", + "proxyConfigured": "Прокси настроен", + "proxyFallback": "Откат при ошибке прокси", + "proxyFallbackDesc": "Перейти на прямое соединение при ошибке прокси", + "proxyFallbackLabel": "Откат на прямое соединение при ошибке прокси", + "proxyNotConfigured": "Не настроен", + "proxyTestButton": "Проверить соединение", + "proxyTestDesc": "Тестирование доступа к URL поставщика через настроенный прокси (использует HEAD запрос, не расходует квоту)", + "proxyTestFailed": "Соединение не удалось", + "proxyTestFillUrl": "Пожалуйста, сначала заполните URL поставщика", + "proxyTestLabel": "Тест соединения", + "proxyTestNetworkError": "Сетевая ошибка: {error}", + "proxyTestProxyError": "Ошибка прокси: {error}", + "proxyTestResponseTime": "Время отклика: {time}", + "proxyTestResultConnectionMethod": "Способ соединения: {via}", + "proxyTestResultConnectionMethodDirect": "Прямое", + "proxyTestResultConnectionMethodProxy": "Прокси", + "proxyTestResultErrorType": "Тип ошибки: {type}", + "proxyTestResultFailed": "Соединение не удалось", + "proxyTestResultMessage": "{message}", + "proxyTestResultResponseTime": "Время отклика: {time}мс", + "proxyTestResultStatusCode": "Код статуса: {code}", + "proxyTestResultSuccess": "Соединение успешно {via}", + "proxyTestStatusCode": "| Код статуса: {code}", + "proxyTestSuccess": "Соединение успешно", + "proxyTestTesting": "Тестирование...", + "proxyTestTimeout": "Тайм-аут соединения (5 секунд). Проверьте:\n1. Доступен ли прокси-сервер\n2. Правильность адреса и порта прокси\n3. Правильность данных аутентификации прокси", + "proxyTestViaDirect": "(прямое)", + "proxyTestViaProxy": "(через прокси)", + "proxyUrl": "Адрес прокси", + "proxyUrlPlaceholder": "например: http://proxy.example.com:8080 или socks5://127.0.0.1:1080", + "rateLimitConfig": "Конфигурация ограничения скорости", + "rateLimitConfigNone": "Без ограничений", + "rateLimitConfigSummary": "5ч: ${fiveHour}, Неделя: ${weekly}, Месяц: ${monthly}, Одновременно: {concurrent}", + "remark": "Примечание", + "remarkPlaceholder": "Необязательно: добавить примечание...", + "removeRedirect": "Удалить переправку", + "routingConfig": "Конфигурация маршрутизации", + "routingConfigNone": "Не настроено", + "routingConfigSummary": "{models} белый список моделей, {redirects} перенаправлений", + "scheduleParams": "Параметры планирования", + "searchClear": "Очистить поиск", + "searchPlaceholder": "Поиск по имени, URL, примечанию...", + "selectProviderType": "Выбрать тип поставщика", + "sort": "Сортировать поставщиков", + "sortByCost": "По стоимости", + "sortByCreated": "По дате создания (новое-старое)", + "sortByName": "По имени (A-Z)", + "sortByPriority": "По приоритету (высокое-низкое)", + "sortByWeight": "По весу (высокое-низкое)", + "sourceModel": "Исходная модель", + "sourceModelPlaceholder": "например: claude-sonnet-4-5-20250929", + "sourceModelRequired": "Имя исходной модели не может быть пустым", + "successThreshold": "Порог восстановления (раз)", + "successThresholdDesc": "Количество успешных попыток в полуоткрытом состоянии для полного восстановления", + "successThresholdPlaceholder": "2", + "targetModel": "Целевая модель", + "targetModelPlaceholder": "например: glm-4.6", + "targetModelRequired": "Имя целевой модели не может быть пустым", + "testProxy": "Проверить соединение", + "testProxyFailed": "Ошибка тестирования прокси", + "testProxyFailedError": "Ошибка проверки соединения:", + "testProxySuccess": "Соединение прокси успешно", + "validUrlRequired": "Пожалуйста, введите действительный адрес API", + "websiteUrlDesc": "Адрес официального сайта поставщика для быстрого перехода к управлению", + "websiteUrlInvalid": "Пожалуйста, введите действительный адрес официального сайта поставщика", + "websiteUrlPlaceholder": "https://example.com", + "weight": "Вес", + "weightDesc": "Взвешенная случайная вероятность. В пределах одного приоритета большее число означает выше вероятность выбора.", + "weightLabel": "Вес", + "weightPlaceholder": "1" +} diff --git a/messages/ru/settings/providers/form/success.json b/messages/ru/settings/providers/form/success.json new file mode 100644 index 000000000..8a6de6453 --- /dev/null +++ b/messages/ru/settings/providers/form/success.json @@ -0,0 +1,4 @@ +{ + "created": "Провайдер успешно добавлен", + "createdDesc": "Провайдер «{name}» добавлен" +} diff --git a/messages/ru/settings/providers/form/title.json b/messages/ru/settings/providers/form/title.json new file mode 100644 index 000000000..44ef32746 --- /dev/null +++ b/messages/ru/settings/providers/form/title.json @@ -0,0 +1,4 @@ +{ + "create": "Добавить провайдера", + "edit": "Редактировать провайдера" +} diff --git a/messages/ru/settings/providers/form/url.json b/messages/ru/settings/providers/form/url.json new file mode 100644 index 000000000..8d0a5fcbe --- /dev/null +++ b/messages/ru/settings/providers/form/url.json @@ -0,0 +1,4 @@ +{ + "label": "Адрес API *", + "placeholder": "например: https://open.bigmodel.cn/api/anthropic" +} diff --git a/messages/ru/settings/providers/form/urlPreview.json b/messages/ru/settings/providers/form/urlPreview.json new file mode 100644 index 000000000..1d01b571c --- /dev/null +++ b/messages/ru/settings/providers/form/urlPreview.json @@ -0,0 +1,9 @@ +{ + "copy": "Копировать", + "copyFailed": "Не удалось скопировать", + "copySuccess": "Скопировано {name} в буфер обмена", + "duplicatePath": "Обнаружен дублирующийся путь", + "invalidUrl": "Неверный формат URL", + "invalidUrlDesc": "Пожалуйста, введите действительный HTTP/HTTPS адрес", + "title": "Предварительный просмотр URL" +} diff --git a/messages/ru/settings/providers/form/websiteUrl.json b/messages/ru/settings/providers/form/websiteUrl.json new file mode 100644 index 000000000..5bd8bd255 --- /dev/null +++ b/messages/ru/settings/providers/form/websiteUrl.json @@ -0,0 +1,5 @@ +{ + "desc": "Официальный сайт провайдера для быстрого доступа", + "label": "Сайт провайдера", + "placeholder": "https://example.com" +} diff --git a/messages/ru/settings/providers/guide.json b/messages/ru/settings/providers/guide.json new file mode 100644 index 000000000..d0326de81 --- /dev/null +++ b/messages/ru/settings/providers/guide.json @@ -0,0 +1,120 @@ +{ + "after": "После фильтрации:", + "before": "До фильтрации:", + "bestPracticesConcurrent": "• Контроль параллелизма: установите количество одновременных сеансов на основе ограничений API поставщика", + "bestPracticesCost": "• Множитель стоимости: официальный множитель = 1.0, собственный сервис можно установить 0.8-1.2", + "bestPracticesLimit": "• Настройка лимитов: установите лимиты на 5 часов, 7 дней, 30 дней в соответствии с бюджетом", + "bestPracticesPriority": "• Настройка приоритета: основные поставщики = 0, резервные = 1-3", + "bestPracticesTitle": "Рекомендации по лучшим практикам", + "bestPracticesWeight": "• Настройка веса: установите вес в соответствии с емкостью поставщика (большая емкость = больший вес)", + "circuitBreaker": "Проверка автоматического выключателя", + "circuitBreakerOpen": "A отфильтрован, осталось: B, C, D", + "circuitBreakerRecovery": "A автоматически восстанавливается в полуоткрытое после 60 секунд", + "circuitBreakerRecovery5h": "Автоматическое восстановление после 5-часового скользящего окна", + "costOptimize": "2. Оптимизация стоимости: в пределах одного приоритета поставщики с низким множителем стоимости имеют более высокую вероятность", + "costSort": "Откат по стоимости", + "costSortExample": "Все поставщики: A (default), B (premium), C (premium), D (economy)", + "costSortProb": "Более дешевый C имеет более высокую вероятность выбора", + "costSortResult": "После сортировки: C (0.8x), A (1.0x)", + "decision": "Решение:", + "group": "Фильтрация групп пользователей", + "groupDesc": "Если пользователь указал группу поставщиков, система приоритизирует выбор из этой группы", + "groupDowngrade": "Записать предупреждение и выбрать из глобального пула", + "groupExample": "Пользователь настроил providerGroup = 'premium'", + "groupFallback": "Если в группе нет доступных поставщиков, откат на всех поставщиков", + "groupFiltered": "Выбрать только из A и C, B и D отфильтрованы", + "groupUnavailable": "Все поставщики в группе 'vip' отключены или превышены", + "health": "Фильтрация здоровья (автоматический выключатель + ограничение)", + "healthCheck": "Проверить, включен ли B и работоспособен", + "healthCheckAmountLimit": "Проверить превышение лимитов (5ч, 7д, 30д)", + "healthCheckAmountLimitExample": "Лимит поставщика B $10 (5ч), потрачено $9.8", + "healthCheckCircuit": "Поставщик A потерпел неудачу 5 раз, статус: открыт", + "healthCheckConcurrent": "Проверить количество активных сеансов на лимит", + "healthCheckConcurrentExample": "Лимит поставщика C 2, активных 2 сеанса", + "healthFilter": "3. Фильтрация здоровья: автоматически пропускать поставщиков с размыканием или превышением лимитов", + "healthFiltered": "B отфильтрован (близко к лимиту), осталось: C, D", + "healthFiltered2": "C отфильтрован (полный), осталось: D", + "history": "Проверить историю запросов", + "historyDesc": "Запрос поставщиков, используемых этим ключом за последние 10 секунд", + "priority": "Приоритизация по уровням", + "priorityExample": "4 включенных поставщика с разными приоритетами", + "priorityFirst": "1. Приоритет в первую очередь: выбирать только из поставщиков с наивысшим приоритетом (наименьшее число)", + "priorityResult": "Отфильтровано к приоритету (0): A, C", + "priorityStep": "Система сначала фильтрует по приоритету, выбирая только из поставщиков с наивысшим приоритетом", + "randomResult": "В конце выбран C", + "randomSelect": "Взвешенный случайный", + "reset": "Ручной сброс автоматического выключателя", + "resetSuccess": "Автоматический выключатель сброшен", + "scenario1Desc": "Система сначала фильтрует по приоритету, выбирая только из поставщиков с наивысшим приоритетом", + "scenario1Step1": "Начальное состояние", + "scenario1Step1After": "Отфильтрованы поставщики с наивысшим приоритетом (0): A, C", + "scenario1Step1Before": "Поставщик A (приоритет 0), B (приоритет 1), C (приоритет 0), D (приоритет 2)", + "scenario1Step1Decision": "Выбирать только из A и C, B и D отфильтрованы", + "scenario1Step1Desc": "Имеется 4 включенных поставщика с разными приоритетами", + "scenario1Step2": "Сортировка по стоимости", + "scenario1Step2After": "После сортировки: C (0.8x), A (1.0x)", + "scenario1Step2Before": "A (стоимость 1.0x), C (стоимость 0.8x)", + "scenario1Step2Decision": "C с более низкой стоимостью имеет более высокую вероятность выбора", + "scenario1Step2Desc": "В пределах одного приоритета сортировка по множителю стоимости от низкого к высокому", + "scenario1Step3": "Взвешенный случайный выбор", + "scenario1Step3After": "C имеет вероятность выбора 75%, A имеет вероятность 25%", + "scenario1Step3Before": "C (вес 3), A (вес 1)", + "scenario1Step3Decision": "В итоге выбран C", + "scenario1Step3Desc": "Использование веса для случайного выбора, чем выше вес, тем выше вероятность выбора", + "scenario1Title": "Выбор по уровням приоритета", + "scenario2Desc": "Если пользователь указал группу поставщиков, система приоритизирует выбор из этой группы", + "scenario2Step1": "Проверка группы пользователя", + "scenario2Step1After": "Отфильтрована группа 'premium': B, C", + "scenario2Step1Before": "Все поставщики: A (default), B (premium), C (premium), D (economy)", + "scenario2Step1Decision": "Выбирать только из B и C", + "scenario2Step1Desc": "Пользователь настроил providerGroup = 'premium'", + "scenario2Step2": "Откат группировки", + "scenario2Step2After": "Откат на всех включенных поставщиков: A, B, C, D", + "scenario2Step2Before": "Все поставщики в группе пользователя 'vip' отключены или превышены", + "scenario2Step2Decision": "Записать предупреждение и выбрать из глобального пула поставщиков", + "scenario2Step2Desc": "Если в группе пользователя нет доступных поставщиков, откат на всех поставщиков", + "scenario2Title": "Фильтрация группы пользователей", + "scenario3Desc": "Система автоматически фильтрует поставщиков с размыканием или превышением лимитов", + "scenario3Step1": "Проверка автоматического выключателя", + "scenario3Step1After": "A отфильтрован, осталось: B, C, D", + "scenario3Step1Before": "Поставщик A потерпел неудачу 5 раз подряд, состояние выключателя: открыт", + "scenario3Step1Decision": "A автоматически восстанавливается в полуоткрытое состояние через 60 секунд", + "scenario3Step1Desc": "После 5 последовательных сбоев выключатель открывается, недоступен в течение 60 секунд", + "scenario3Step2": "Ограничение по сумме", + "scenario3Step2After": "B отфильтрован (близко к лимиту), осталось: C, D", + "scenario3Step2Before": "Лимит поставщика B за 5 часов $10, потрачено $9.8", + "scenario3Step2Decision": "Автоматическое восстановление после сдвига 5-часового окна", + "scenario3Step2Desc": "Проверка превышения лимитов расходов за 5 часов, 7 дней, 30 дней", + "scenario3Step3": "Ограничение одновременных сеансов", + "scenario3Step3After": "C отфильтрован (полон), осталось: D", + "scenario3Step3Before": "Лимит одновременных сеансов поставщика C 2, текущее количество активных сеансов: 2", + "scenario3Step3Decision": "Автоматическое освобождение после истечения сеанса (5 минут)", + "scenario3Step3Desc": "Проверка превышения настроенного лимита одновременных сеансов", + "scenario3Title": "Фильтрация здоровья (автоматический выключатель + ограничение)", + "scenario4Desc": "Последовательные диалоги приоритетно используют одного поставщика для использования кэша контекста Claude", + "scenario4Step1": "Проверка истории запросов", + "scenario4Step1After": "Проверить, включен ли B и работоспособен", + "scenario4Step1Before": "Последний запрос использовал поставщика B", + "scenario4Step1Decision": "B доступен, повторно использовать напрямую, пропустить случайный выбор", + "scenario4Step1Desc": "Запрос поставщиков, используемых этим API ключом за последние 10 секунд", + "scenario4Step2": "Повторное использование недействительно", + "scenario4Step2After": "Вход в нормальный процесс выбора", + "scenario4Step2Before": "Последний использованный поставщик B был отключен или разомкнут", + "scenario4Step2Decision": "Выбрать из других доступных поставщиков", + "scenario4Step2Desc": "Если последний использованный поставщик недоступен, выполнить повторный выбор", + "scenario4Title": "Механизм повторного использования сеансов", + "scenariosTitle": "Интерактивная демонстрация сценариев", + "session": "Механизм переиспользования сеансов", + "sessionDesc": "Если последний использованный поставщик недоступен, переходим к переполнению", + "sessionExample": "Последний запрос использовал поставщика B", + "sessionExpired": "Сеанс автоматически освобождается после истечения (5 минут)", + "sessionFallback": "Выбрать из других доступных поставщиков", + "sessionLastUsed": "B доступен, переиспользуем, пропускаем случайный выбор", + "sessionReuse": "4. Повторное использование сеанса: последовательные диалоги повторно используют одного поставщика для экономии затрат на контекст", + "sessionUnavailable": "Последний использованный поставщик B отключен или замкнут", + "step": "Шаг", + "title": "Основные принципы", + "weight": "Взвешенный случайный выбор по весу", + "weightCalc": "C имеет вероятность 75%, A имеет вероятность 25%", + "weightExample": "C (вес 3), A (вес 1)" +} diff --git a/messages/ru/settings/providers/inlineEdit.json b/messages/ru/settings/providers/inlineEdit.json new file mode 100644 index 000000000..2e614a3d9 --- /dev/null +++ b/messages/ru/settings/providers/inlineEdit.json @@ -0,0 +1,12 @@ +{ + "cancel": "Отмена", + "costMultiplierInvalid": "Введите число не меньше 0", + "costMultiplierLabel": "Коэф цены", + "priorityInvalid": "Введите целое число >= 0", + "priorityLabel": "Приоритет", + "save": "Сохранить", + "saveFailed": "Не удалось сохранить", + "saveSuccess": "Успешно сохранено", + "weightInvalid": "Введите целое число от 1 до 100", + "weightLabel": "Вес" +} diff --git a/messages/ru/settings/providers/list.json b/messages/ru/settings/providers/list.json new file mode 100644 index 000000000..4a6e90a4f --- /dev/null +++ b/messages/ru/settings/providers/list.json @@ -0,0 +1,37 @@ +{ + "cancelButton": "Отмена", + "circuitBroken": "Разорвано", + "clipboardUnavailable": "Буфер обмена недоступен в этой среде. Скопируйте ключ вручную.", + "confirmDeleteMessage": "Вы уверены, что хотите удалить провайдера \"{name}\"? Это действие нельзя отменить.", + "confirmDeleteTitle": "Подтвердить удаление провайдера?", + "copyFailed": "Не удалось скопировать", + "costMultiplier": "Коэф цены", + "deleteButton": "Удалить", + "deleteError": "Произошла ошибка во время операции", + "deleteFailed": "Не удалось удалить", + "deleteSuccess": "Успешно удалено", + "deleteSuccessDesc": "Провайдер \"{name}\" был удален", + "getKeyFailed": "Не удалось получить ключ", + "keyCopied": "Ключ скопирован в буфер обмена", + "keyLoading": "Загрузка...", + "officialWebsite": "Официальный", + "priority": "Приоритет", + "resetCircuitFailed": "Не удалось сбросить автоматический выключатель", + "resetCircuitSuccess": "Автоматический выключатель сброшен", + "resetCircuitSuccessDesc": "Статус автоматического выключателя провайдера \"{name}\" очищен", + "resetUsageFailed": "Не удалось сбросить общий расход", + "resetUsageSuccess": "Общий расход сброшен", + "resetUsageSuccessDesc": "Общий расход провайдера \"{name}\" был сброшен", + "resetUsageTitle": "Сбросить общий расход", + "statusDisabled": "отключен", + "statusEnabled": "включен", + "todayUsageCount": "{count} раз(а)", + "todayUsageLabel": "Сегодня", + "toggleFailed": "Не удалось переключить", + "toggleSuccess": "Провайдер {status}", + "toggleSuccessDesc": "Статус провайдера \"{name}\" обновлен", + "unknownError": "Неизвестная ошибка", + "viewFullKey": "Просмотр полного API-ключа", + "viewFullKeyDesc": "Пожалуйста, храните его в безопасности и не делитесь с другими", + "weight": "Вес" +} diff --git a/messages/ru/settings/providers/schedulingDialog.json b/messages/ru/settings/providers/schedulingDialog.json new file mode 100644 index 000000000..2a0ffa6e6 --- /dev/null +++ b/messages/ru/settings/providers/schedulingDialog.json @@ -0,0 +1,9 @@ +{ + "after": "После:", + "before": "До:", + "decision": "Решение:", + "description": "Узнайте, как система интеллектуально выбирает вышестоящих провайдеров для высокой доступности и оптимизации затрат", + "step": "Шаг", + "title": "Правила планирования провайдеров", + "triggerButton": "Правила" +} diff --git a/messages/ru/settings/providers/search.json b/messages/ru/settings/providers/search.json new file mode 100644 index 000000000..84e3b6c5a --- /dev/null +++ b/messages/ru/settings/providers/search.json @@ -0,0 +1,7 @@ +{ + "clear": "Очистить поиск", + "found": "Найдено {count} совпадающих провайдеров", + "notFound": "Совпадающие провайдеры не найдены", + "placeholder": "Поиск по имени, URL, заметкам...", + "showing": "Показано {filtered} / {total} провайдеров" +} diff --git a/messages/ru/settings/providers/section.json b/messages/ru/settings/providers/section.json new file mode 100644 index 000000000..127af504f --- /dev/null +++ b/messages/ru/settings/providers/section.json @@ -0,0 +1,5 @@ +{ + "description": "Настройка ограничений по расходам и параллельным сеансам для вышестоящих поставщиков.", + "leaderboard": "Рейтинг", + "title": "Поставщики" +} diff --git a/messages/ru/settings/providers/sort.json b/messages/ru/settings/providers/sort.json new file mode 100644 index 000000000..b88d7e4cb --- /dev/null +++ b/messages/ru/settings/providers/sort.json @@ -0,0 +1,8 @@ +{ + "byActualPriority": "По фактическому приоритету выбора", + "byCreatedAt": "По дате создания (нов-стар)", + "byName": "По имени (A-Z)", + "byPriority": "По приоритету (выс-низ)", + "byWeight": "По весу (выс-низ)", + "placeholder": "Сортировать провайдеров" +} diff --git a/messages/ru/settings/providers/strings.json b/messages/ru/settings/providers/strings.json new file mode 100644 index 000000000..c9374633a --- /dev/null +++ b/messages/ru/settings/providers/strings.json @@ -0,0 +1,47 @@ +{ + "add": "Добавить поставщика", + "addFailed": "Ошибка добавления поставщика", + "addProvider": "Добавить провайдера", + "addSuccess": "Поставщик добавлен успешно", + "circuitBroken": "Цепь разомкнута", + "clone": "Дублировать поставщика", + "cloneFailed": "Ошибка копирования", + "confirmDelete": "Вы уверены, что хотите удалить этого поставщика?", + "confirmDeleteDesc": "Вы уверены, что хотите удалить провайдера \"{name}\"? Это действие не может быть отменено.", + "confirmDeleteProvider": "Подтвердить удаление провайдера?", + "confirmDeleteProviderDesc": "Вы уверены, что хотите удалить провайдера \"{name}\"? Это действие не может быть восстановлено.", + "createProvider": "Добавить провайдера", + "delete": "Удалить поставщика", + "deleteFailed": "Ошибка удаления поставщика", + "deleteSuccess": "Успешно удалено", + "description": "Настройка поставщиков API и контроль статуса доступности.", + "disabledStatus": "Отключено", + "displayCount": "Показано {filtered} / {total} провайдеров", + "edit": "Редактировать поставщика", + "editFailed": "Ошибка обновления поставщика", + "editProvider": "Редактировать провайдера", + "enabledStatus": "Включено", + "keyLoading": "Загрузка...", + "noProviders": "Нет настроенных поставщиков", + "noProvidersDesc": "Добавьте вашего первого поставщика API", + "notFound": "Поставщики не найдены", + "official": "Официальный сайт", + "resetCircuit": "Автоматический выключатель сброшен", + "resetCircuitDesc": "Состояние размыкания поставщика \"{name}\" снято", + "resetCircuitFailed": "Не удалось сбросить автоматический выключатель", + "scheduling": "Подробное объяснение политики планирования", + "schedulingDesc": "Понимание того, как работает выбор поставщика: приоритизация, переиспользование сеансов, балансировка нагрузки и отказоустойчивость", + "searchNoResults": "Поставщики не найдены", + "searchResults": "Найдено {count} поставщиков", + "subtitle": "Поставщики", + "subtitleDesc": "Настройка ограничений по расходам и параллельным сеансам.", + "title": "Поставщики", + "todayUsage": "Сегодня", + "todayUsageCount": "{count} раз", + "toggleFailed": "Не удалось переключить статус", + "toggleSuccess": "Поставщик {status}", + "toggleSuccessDesc": "Статус поставщика \"{name}\" обновлен", + "updateFailed": "Не удалось обновить поставщика", + "viewKey": "Просмотреть полный API ключ", + "viewKeyDesc": "Пожалуйста, храните бережно и не раскрывайте другим" +} diff --git a/messages/ru/settings/providers/types.json b/messages/ru/settings/providers/types.json new file mode 100644 index 000000000..3eae6f6f9 --- /dev/null +++ b/messages/ru/settings/providers/types.json @@ -0,0 +1,26 @@ +{ + "claude": { + "description": "Официальный API Anthropic", + "label": "Claude" + }, + "claudeAuth": { + "description": "Служба ретрансляции Claude", + "label": "Claude Auth" + }, + "codex": { + "description": "Codex CLI API", + "label": "Codex" + }, + "gemini": { + "description": "Google Gemini API", + "label": "Gemini" + }, + "geminiCli": { + "description": "Gemini CLI API", + "label": "Gemini CLI" + }, + "openaiCompatible": { + "description": "Совместимый с OpenAI API", + "label": "OpenAI Compatible" + } +} diff --git a/messages/ru/settings/requestFilters.json b/messages/ru/settings/requestFilters.json new file mode 100644 index 000000000..086ed266a --- /dev/null +++ b/messages/ru/settings/requestFilters.json @@ -0,0 +1,84 @@ +{ + "actionLabel": { + "json_path": "Замена по JSON пути", + "remove": "Удалить header", + "set": "Установить header", + "text_replace": "Замена текста" + }, + "add": "Добавить фильтр", + "addFailed": "Не удалось создать", + "addSuccess": "Создано", + "applyToAll": "Применяется ко всем запросам", + "confirmDelete": "Удалить фильтр \"{name}\"?", + "delete": "Удалить фильтр", + "deleteFailed": "Не удалось удалить", + "deleteSuccess": "Удалено", + "description": "Настройте удаление/замену заголовков и замену тела перед отправкой вверх по цепочке для маскировки запросов.", + "dialog": { + "action": "Действие", + "bindingGlobal": "Все провайдеры (глобально)", + "bindingGroups": "Группы провайдеров", + "bindingProviders": "Конкретные провайдеры", + "bindingType": "Применить к", + "clear": "Очистить", + "createTitle": "Добавить фильтр", + "description": "Описание (опционально)", + "editTitle": "Редактировать фильтр", + "groupsSelected": "Выбрано групп: {count}", + "jsonPathPlaceholder": "например: messages.0.content или data.items[0].token", + "loading": "Загрузка...", + "matchType": "Тип совпадения", + "matchTypeContains": "Содержит", + "matchTypeExact": "Точное", + "matchTypeRegex": "Регулярное выражение", + "name": "Название", + "noGroupsFound": "Группы не найдены", + "noProvidersFound": "Провайдеры не найдены", + "priority": "Приоритет", + "providersSelected": "Выбрано провайдеров: {count}", + "replacement": "Значение (опционально)", + "replacementPlaceholder": "Строка или JSON, пусто — удалить", + "save": "Сохранить", + "saving": "Сохранение...", + "scope": "Область", + "searchGroups": "Поиск групп...", + "searchProviders": "Поиск провайдеров...", + "selectAll": "Выбрать все", + "selectGroups": "Выберите группы...", + "selectProviders": "Выберите провайдеров...", + "target": "Поле/путь", + "targetPlaceholder": "Имя заголовка или текст/путь", + "validation": { + "fieldRequired": "Название и цель обязательны" + } + }, + "disable": "Отключено", + "edit": "Редактировать фильтр", + "editFailed": "Не удалось обновить", + "editSuccess": "Обновлено", + "empty": "Фильтров пока нет. Добавьте новый.", + "enable": "Включено", + "groups": "Группы", + "nav": "Фильтры запросов", + "providers": "Провайдеры", + "refreshCache": "Обновить кэш", + "refreshFailed": "Обновление не удалось", + "refreshSuccess": "Кэш обновлен, загружено {count} фильтров", + "scopeLabel": { + "body": "Body", + "header": "Header" + }, + "table": { + "action": "Действие", + "actions": "Действия", + "apply": "Область", + "createdAt": "Создано", + "name": "Название", + "priority": "Приоритет", + "replacement": "Значение", + "scope": "Область", + "status": "Статус", + "target": "Цель" + }, + "title": "Фильтры запросов" +} diff --git a/messages/ru/settings/sensitiveWords.json b/messages/ru/settings/sensitiveWords.json new file mode 100644 index 000000000..6ba2b14b9 --- /dev/null +++ b/messages/ru/settings/sensitiveWords.json @@ -0,0 +1,55 @@ +{ + "add": "Добавить чувствительное слово", + "addFailed": "Ошибка создания чувствительного слова", + "addSuccess": "Чувствительное слово создано успешно", + "cacheStats": "Статистика кэша: Содержит({containsCount}) Точное({exactCount}) Регулярное({regexCount})", + "confirmDelete": "Вы уверены, что хотите удалить чувствительное слово \"{word}\"?", + "delete": "Удалить чувствительное слово", + "deleteFailed": "Ошибка удаления", + "deleteSuccess": "Чувствительное слово удалено успешно", + "description": "Настройка фильтрации чувствительных слов для блокирования чувствительного контента.", + "dialog": { + "addDescription": "Настройте правила фильтрации чувствительных слов. Совпадающие запросы не будут пересылаться.", + "addTitle": "Добавить чувствительное слово", + "creating": "Создание...", + "descriptionLabel": "Описание", + "descriptionPlaceholder": "Необязательно: Добавить описание...", + "editDescription": "Изменить конфигурацию чувствительного слова. Изменения автоматически обновят кэш.", + "editTitle": "Редактировать чувствительное слово", + "matchTypeContains": "Частичное совпадение - Блокировать, если текст содержит это слово", + "matchTypeExact": "Точное совпадение - Блокировать только при точном совпадении", + "matchTypeLabel": "Тип совпадения *", + "matchTypeRegex": "Регулярное выражение - Поддержка сложного сопоставления шаблонов", + "saving": "Сохранение...", + "wordLabel": "Чувствительное слово *", + "wordPlaceholder": "Введите чувствительное слово...", + "wordRequired": "Пожалуйста, введите чувствительное слово" + }, + "disable": "Чувствительное слово отключено", + "edit": "Редактировать чувствительное слово", + "editFailed": "Ошибка обновления чувствительного слова", + "editSuccess": "Чувствительное слово обновлено успешно", + "emptyState": "Пока нет чувствительных слов. Нажмите 'Добавить чувствительное слово' в правом верхнем углу для начала настройки.", + "enable": "Чувствительное слово включено", + "refreshCache": "Обновить кэш", + "refreshCacheFailed": "Не удалось обновить кэш", + "refreshCacheSuccess": "Кэш успешно обновлен, загружено {count} чувствительных слов", + "section": { + "description": "Запросы, заблокированные чувствительными словами, не будут пересылаться и не будут тарифицироваться. Поддерживает частичное совпадение, точное совпадение и регулярные выражения.", + "title": "Список чувствительных слов" + }, + "table": { + "actions": "Действия", + "createdAt": "Создано", + "description": "Описание", + "matchType": "Тип совпадения", + "matchTypeContains": "Частичное", + "matchTypeExact": "Точное", + "matchTypeRegex": "Регулярное", + "status": "Статус", + "word": "Чувствительное слово" + }, + "title": "Управление чувствительными словами", + "toggleFailed": "Ошибка переключения", + "toggleFailedError": "Ошибка переключения:" +} diff --git a/messages/ru/settings/strings.json b/messages/ru/settings/strings.json new file mode 100644 index 000000000..8210a601a --- /dev/null +++ b/messages/ru/settings/strings.json @@ -0,0 +1,22 @@ +{ + "mcpPassthroughConfig": "Конфигурация сквозной передачи MCP", + "mcpPassthroughConfigCustom": "Пользовательский (Зарезервировано)", + "mcpPassthroughConfigGlm": "GLM", + "mcpPassthroughConfigMinimax": "Minimax", + "mcpPassthroughConfigNone": "Отключено", + "mcpPassthroughCustomDesc": "Сквозная передача в пользовательский сервис MCP (зарезервировано, не реализовано)", + "mcpPassthroughCustomLabel": "Пользовательский", + "mcpPassthroughDesc": "При включении передаёт вызовы инструментов MCP указанному AI-провайдеру (например, minimax для распознавания изображений, веб-поиска)", + "mcpPassthroughGlmDesc": "Сквозная передача в сервис GLM MCP (поддержка анализа изображений, видео и т.д.)", + "mcpPassthroughGlmLabel": "GLM", + "mcpPassthroughHint": "Подсказка: сквозная передача MCP позволяет клиенту Claude Code использовать возможности инструментов, предоставляемых сторонними AI-провайдерами (например, распознавание изображений, веб-поиск)", + "mcpPassthroughMinimaxDesc": "Сквозная передача в сервис minimax MCP (поддержка распознавания изображений, веб-поиска и т.д.)", + "mcpPassthroughMinimaxLabel": "Minimax", + "mcpPassthroughNoneDesc": "Не включать сквозную передачу MCP (по умолчанию)", + "mcpPassthroughNoneLabel": "Отключено", + "mcpPassthroughSelect": "Тип сквозной передачи", + "mcpPassthroughUrlAuto": "Автоматически извлечено: {url}", + "mcpPassthroughUrlDesc": "Базовый URL сервиса MCP. Оставьте пустым для автоматического извлечения из URL провайдера", + "mcpPassthroughUrlLabel": "URL сквозной передачи MCP", + "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com" +} diff --git a/messages/zh-CN/index.ts b/messages/zh-CN/index.ts index d5bad5de2..70f0e4ca7 100644 --- a/messages/zh-CN/index.ts +++ b/messages/zh-CN/index.ts @@ -11,7 +11,7 @@ import notifications from "./notifications.json"; import providerChain from "./provider-chain.json"; import providers from "./providers.json"; import quota from "./quota.json"; -import settings from "./settings.json"; +import settings from "./settings"; import ui from "./ui.json"; import usage from "./usage.json"; import users from "./users.json"; diff --git a/messages/zh-CN/provider-chain.json b/messages/zh-CN/provider-chain.json index a3c96cfb3..b4921a7f5 100644 --- a/messages/zh-CN/provider-chain.json +++ b/messages/zh-CN/provider-chain.json @@ -86,7 +86,7 @@ "remaining": "还有{count}次", "status": "状态", "alreadyBroken": "已触发熔断", - "circuitTriggered": "⚠️ 已触发熔断", + "circuitTriggered": "警告:已触发熔断", "errorDetails": "错误详情", "systemError": "网络/系统错误", "systemErrorFailed": "网络/系统错误(第 {attempt} 次尝试)", @@ -97,7 +97,7 @@ "errorMeaning": "含义: {meaning}", "meaning": "含义", "notCountedInCircuit": "此错误不计入供应商熔断器", - "systemErrorNote": "ℹ️ 此错误不计入供应商熔断器", + "systemErrorNote": "说明:此错误不计入供应商熔断器", "reselection": "重新选择供应商", "reselect": "重新选择供应商", "excluded": "已排除: {providers}", diff --git a/messages/zh-CN/settings.json b/messages/zh-CN/settings.json deleted file mode 100644 index 151e6cb7e..000000000 --- a/messages/zh-CN/settings.json +++ /dev/null @@ -1,2165 +0,0 @@ -{ - "nav": { - "config": "配置", - "prices": "价格表", - "providers": "供应商", - "sensitiveWords": "敏感词", - "requestFilters": "请求过滤", - "clientVersions": "客户端升级提醒", - "data": "数据管理", - "logs": "日志", - "notifications": "消息推送", - "apiDocs": "API 文档", - "errorRules": "错误规则", - "feedback": "反馈问题", - "docs": "使用文档" - }, - "common": { - "save": "保存", - "saving": "保存中...", - "create": "创建", - "creating": "创建中...", - "edit": "编辑", - "delete": "删除", - "update": "更新", - "updating": "更新中...", - "test": "测试", - "testing": "测试中...", - "confirm": "确认", - "cancel": "取消", - "submit": "提交", - "reset": "重置", - "refresh": "刷新", - "loading": "加载中...", - "completed": "完成", - "unlimited": "无限", - "unlimited_desc": "无限制", - "none": "无(暂无用户使用该版本)", - "enabled": "启用", - "disabled": "禁用", - "empty": "未找到匹配的结果", - "error": "未知错误", - "success": "成功", - "failed": "失败", - "copy": "复制", - "copied": "密钥已复制到剪贴板", - "copyFailed": "复制失败" - }, - "config": { - "title": "基础配置", - "description": "管理系统的基础参数,影响站点显示和统计行为。", - "siteSettings": "站点参数", - "siteSettingsDesc": "配置站点标题、货币显示单位与仪表盘统计展示策略。", - "autoCleanup": "自动日志清理", - "autoCleanupDesc": "定时自动清理历史日志数据,释放数据库存储空间。", - "section": { - "siteParams": { - "title": "站点参数", - "description": "配置站点标题、货币显示单位与仪表盘统计展示策略。" - }, - "autoCleanup": { - "title": "自动日志清理", - "description": "定时自动清理历史日志数据,释放数据库存储空间。" - } - }, - "form": { - "siteTitle": "站点标题", - "siteTitlePlaceholder": "例如:Claude Code Hub", - "siteTitleRequired": "站点标题不能为空", - "siteTitleDesc": "用于设置浏览器标签页标题以及系统默认显示名称。", - "currencyDisplay": "货币显示单位", - "currencyDisplayPlaceholder": "选择货币单位", - "currencyDisplayDesc": "修改后,系统所有页面和 API 接口的金额显示将使用对应的货币符号(仅修改符号,不进行汇率转换)。", - "billingModelSource": "计费模型来源", - "billingModelSourcePlaceholder": "选择计费模型来源", - "billingModelSourceDesc": "配置模型重定向时使用哪个模型进行计费。重定向前使用用户请求的原始模型计费,重定向后使用实际调用的模型计费。", - "billingModelSourceOptions": { - "original": "重定向前(原始模型)", - "redirected": "重定向后(实际模型)" - }, - "allowGlobalView": "允许查看全站使用量", - "allowGlobalViewDesc": "关闭后,普通用户在仪表盘仅能查看自己密钥的使用统计。", - "verboseProviderError": "详细供应商错误信息", - "verboseProviderErrorDesc": "开启后,当所有供应商不可用时返回详细错误信息(包含供应商数量、限流原因等);关闭后仅返回简洁错误码。", - "enableHttp2": "启用 HTTP/2", - "enableHttp2Desc": "启用后,代理请求将优先使用 HTTP/2 协议。如果 HTTP/2 失败,将自动降级到 HTTP/1.1。", - "interceptAnthropicWarmupRequests": "拦截 Warmup 请求(Anthropic)", - "interceptAnthropicWarmupRequestsDesc": "开启后,识别到 Claude Code 的 Warmup 探测请求将由 CCH 直接抢答短响应,避免访问上游供应商;该请求会记录在日志中,但不计费、不限流、不计入统计。", - "enableThinkingSignatureRectifier": "启用 thinking 签名整流器", - "enableThinkingSignatureRectifierDesc": "当 Anthropic 类型供应商返回 thinking 签名不兼容或非法请求等错误时,自动移除不兼容的 thinking 相关块并对同一供应商重试一次(默认开启)。", - "enableResponseFixer": "启用响应整流", - "enableResponseFixerDesc": "自动修复上游响应中常见的编码、SSE 与 JSON 格式问题(默认开启)。", - "responseFixerFixEncoding": "修复编码问题", - "responseFixerFixEncodingDesc": "移除 BOM 与空字节,并对无效 UTF-8 做兼容处理。", - "responseFixerFixSseFormat": "修复 SSE 格式", - "responseFixerFixSseFormatDesc": "补齐 data: 前缀、统一换行符,并修复常见字段格式。", - "responseFixerFixTruncatedJson": "修复截断的 JSON", - "responseFixerFixTruncatedJsonDesc": "补齐未闭合的括号/引号,移除尾随逗号,必要时补 null。", - "saveSettings": "保存设置", - "keepDays": "保留天数", - "keepDaysDesc": "清理超过此天数的历史日志", - "cleanupSchedule": "清理周期", - "cleanupScheduleDesc": "选择自动清理的执行周期", - "saveSuccess": "保存成功", - "saveFailed": "保存失败", - "saveError": "保存失败", - "configUpdated": "系统设置已更新,页面将刷新以应用货币显示变更", - "enableAutoCleanup": "启用自动清理", - "enableAutoCleanupDesc": "定时自动清理历史日志数据", - "cleanupRetentionDays": "保留天数", - "cleanupRetentionDaysRequired": "保留天数 *", - "cleanupRetentionDaysPlaceholder": "30", - "cleanupRetentionDaysDesc": "超过此天数的日志将被自动清理(范围:1-365 天)", - "cleanupScheduleLabel": "执行时间 (Cron)", - "cleanupScheduleRequired": "执行时间 (Cron) *", - "cleanupSchedulePlaceholder": "0 2 * * *", - "cleanupScheduleCronDesc": "Cron 表达式,默认:0 2 * * *(每天凌晨 2 点)", - "cleanupScheduleCronExample": "示例:0 3 * * 0(每周日凌晨 3 点)", - "cleanupBatchSize": "批量大小", - "cleanupBatchSizeRequired": "批量大小 *", - "cleanupBatchSizePlaceholder": "10000", - "cleanupBatchSizeDesc": "每批删除的记录数(范围:1000-100000,推荐 10000)", - "saveConfig": "保存配置", - "autoCleanupSaved": "自动清理配置已保存", - "currencies": { - "USD": "$ 美元 (USD)", - "CNY": "¥ 人民币 (CNY)", - "EUR": "€ 欧元 (EUR)", - "JPY": "¥ 日元 (JPY)", - "GBP": "£ 英镑 (GBP)", - "HKD": "HK$ 港币 (HKD)", - "TWD": "NT$ 新台币 (TWD)", - "KRW": "₩ 韩元 (KRW)", - "SGD": "S$ 新加坡元 (SGD)" - } - } - }, - "providers": { - "title": "供应商管理", - "description": "配置 API 服务商并维护可用状态。", - "subtitle": "服务商管理", - "subtitleDesc": "配置上游服务商的金额限流和并发限制,留空表示无限制。", - "add": "添加供应商", - "autoSort": { - "button": "自动排序优先级", - "dialogTitle": "自动排序供应商优先级", - "dialogDescription": "根据成本倍率自动分配优先级(低成本 = 高优先级)", - "changeCount": "{count} 个供应商将被更新", - "noChanges": "无需更改(已排序)", - "costMultiplierHeader": "成本倍率", - "priorityHeader": "优先级", - "providersHeader": "供应商", - "changesTitle": "变更详情", - "providerHeader": "供应商", - "priorityChangeHeader": "优先级变更", - "confirm": "应用变更", - "success": "已更新 {count} 个供应商的优先级", - "error": "更新优先级失败" - }, - "types": { - "claude": { - "label": "Claude", - "description": "Anthropic 官方 API" - }, - "claudeAuth": { - "label": "Claude Auth", - "description": "Claude 中转服务" - }, - "codex": { - "label": "Codex", - "description": "Codex CLI API" - }, - "gemini": { - "label": "Gemini", - "description": "Google Gemini API" - }, - "geminiCli": { - "label": "Gemini CLI", - "description": "Gemini CLI API" - }, - "openaiCompatible": { - "label": "OpenAI Compatible", - "description": "OpenAI 兼容 API" - } - }, - "list": { - "priority": "优先级", - "weight": "权重", - "costMultiplier": "成本倍数", - "todayUsageLabel": "今日用量", - "todayUsageCount": "{count} 次", - "circuitBroken": "熔断中", - "officialWebsite": "官网", - "viewFullKey": "查看完整 API Key", - "viewFullKeyDesc": "请妥善保管,不要泄露给他人", - "keyLoading": "加载中...", - "confirmDeleteTitle": "确认删除供应商?", - "confirmDeleteMessage": "确定要删除供应商 \"{name}\" 吗?此操作无法撤销。", - "deleteButton": "删除", - "cancelButton": "取消", - "deleteSuccess": "删除成功", - "deleteSuccessDesc": "供应商 \"{name}\" 已删除", - "deleteFailed": "删除失败", - "deleteError": "操作过程中出现异常", - "unknownError": "未知错误", - "getKeyFailed": "获取密钥失败", - "keyCopied": "密钥已复制到剪贴板", - "copyFailed": "复制失败", - "clipboardUnavailable": "当前环境无法访问剪贴板,请手动选择复制。", - "resetCircuitSuccess": "熔断器已重置", - "resetCircuitSuccessDesc": "供应商 \"{name}\" 的熔断状态已解除", - "resetCircuitFailed": "重置熔断器失败", - "resetUsageTitle": "重置总用量", - "resetUsageSuccess": "总用量已重置", - "resetUsageSuccessDesc": "供应商 \"{name}\" 的总用量已重置", - "resetUsageFailed": "重置总用量失败", - "toggleSuccess": "供应商已{status}", - "toggleSuccessDesc": "供应商 \"{name}\" 状态已更新", - "toggleFailed": "状态切换失败", - "statusEnabled": "启用", - "statusDisabled": "禁用" - }, - "inlineEdit": { - "save": "保存", - "cancel": "取消", - "saveSuccess": "保存成功", - "saveFailed": "保存失败", - "priorityLabel": "优先级", - "weightLabel": "权重", - "costMultiplierLabel": "成本倍数", - "priorityInvalid": "请输入大于等于 0 的整数", - "weightInvalid": "请输入 1-100 之间的整数", - "costMultiplierInvalid": "请输入大于等于 0 的数字" - }, - "schedulingDialog": { - "title": "供应商调度规则说明", - "description": "了解系统如何智能选择上游供应商,确保高可用性和成本优化", - "triggerButton": "调度规则说明", - "step": "步骤", - "before": "过滤前:", - "after": "过滤后:", - "decision": "决策:" - }, - "addSuccess": "添加服务商成功", - "addFailed": "添加服务商失败", - "edit": "编辑服务商", - "editFailed": "更新服务商失败", - "delete": "删除供应商", - "deleteFailed": "删除供应商失败", - "clone": "克隆服务商", - "cloneFailed": "复制失败", - "confirmDelete": "确定要删除此供应商吗?", - "notFound": "未找到匹配的供应商", - "form": { - "proxyTest": { - "fillUrlFirst": "请先填写供应商 URL", - "testFailed": "测试失败", - "testFailedRetry": "测试失败,请重试", - "noResult": "测试成功但未返回结果", - "connectionSuccess": "连接成功", - "connectionFailed": "连接失败", - "viaProxy": "(通过代理)", - "viaDirect": "(直连)", - "responseTime": "响应时间:", - "statusCode": "状态码:", - "connectionMethod": "连接方式:", - "proxy": "代理", - "direct": "直连", - "errorType": "错误类型:", - "testing": "测试中...", - "testConnection": "测试连接", - "timeoutError": "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确", - "proxyError": "代理错误:", - "networkError": "网络错误:" - }, - "apiTest": { - "fillUrlFirst": "请先填写供应商 URL", - "invalidUrl": "供应商 URL 无效,仅支持 http/https", - "fillKeyFirst": "请先填写 API 密钥", - "testFailed": "测试失败", - "testFailedRetry": "测试失败,请重试", - "noResult": "测试成功但未返回结果", - "testSuccess": "模型测试成功", - "testApi": "供应商模型测试", - "testing": "测试中...", - "apiFormat": "供应商类型", - "selectApiFormat": "选择要测试的供应商类型", - "apiFormatDesc": "默认同步路由配置中的供应商类型,除非手动修改", - "formatAnthropicMessages": "Claude (Anthropic Messages API)", - "formatOpenAIChat": "OpenAI Compatible", - "formatOpenAIResponses": "Codex (Response API)", - "testModel": "测试模型", - "testModelDesc": "可手动输入,不填写则使用默认模型", - "requestConfig": "请求配置", - "presetConfig": "预置配置", - "customConfig": "自定义", - "selectPreset": "选择预置模板", - "presetDesc": "预置模板包含真实 CLI 请求特征,用于通过中转服务验证", - "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", - "customPayloadDesc": "输入自定义 JSON payload,将覆盖默认请求体", - "successContains": "成功检测词", - "successContainsPlaceholder": "pong", - "successContainsDesc": "响应需包含此内容才算测试成功", - "model": "模型", - "responseModel": "响应模型", - "responseTime": "响应时间", - "usage": "Token 用量", - "response": "响应内容", - "error": "错误信息", - "unknown": "未知", - "viewDetails": "查看详情", - "copySuccess": "已复制到剪贴板", - "copyFailed": "复制失败", - "copyResult": "复制结果", - "close": "关闭", - "success": "成功", - "failed": "失败", - "streamInfo": "流式响应信息", - "chunksReceived": "接收到的数据块", - "streamFormat": "流式格式", - "streamResponse": "流式响应", - "chunksCount": "接收 {count} 个数据块 ({format})", - "truncatedPreview": "显示前 {length} 字符,完整内容请复制查看", - "truncatedBrief": "显示前 {length} 字符,完整内容请点击「查看详情」", - "timeout": { - "label": "超时时间(秒)", - "desc": "测试请求的最大等待时间(5-120 秒)", - "geminiHint": ",Gemini Thinking 模型建议 60 秒以上" - }, - "geminiAuthFallback": { - "warning": "Header 认证失败,使用了 URL 参数认证", - "desc": "实际代理转发仅使用 Header 认证,可能导致请求失败" - }, - "copyFormat": { - "testResult": "测试结果", - "message": "消息", - "errorDetails": "错误详情" - }, - "disclaimer": { - "title": "注意", - "resultReference": "【重要】因各家供应商情况不同,测试结果仅供参考,不代表实际调用效果", - "realRequest": "测试将向供应商发送真实请求,可能消耗少量额度", - "confirmConfig": "请确认供应商 URL、API 密钥及模型配置正确" - }, - "resultCard": { - "status": { - "green": "可用", - "yellow": "波动", - "red": "不可用" - }, - "dialogTitle": "供应商测试详情", - "validation": { - "title": "三层验证详情", - "http": { - "title": "Tier 1: HTTP 状态", - "statusCode": "状态码", - "passed": "2xx/3xx 成功", - "failed": "4xx/5xx 失败" - }, - "latency": { - "title": "Tier 2: 延迟阈值", - "actual": "实际延迟", - "passed": "在阈值内", - "failed": "超过阈值" - }, - "content": { - "title": "Tier 3: 内容验证", - "target": "目标", - "passed": "包含目标字符串", - "failed": "未找到目标" - }, - "passed": "通过", - "failed": "失败", - "timeout": "超时" - }, - "labels": { - "http": "HTTP", - "latency": "延迟", - "content": "内容", - "model": "模型", - "firstByte": "首字节", - "totalLatency": "总延迟", - "error": "错误", - "responsePreview": "响应预览" - }, - "timing": { - "title": "时间信息", - "totalLatency": "总延迟", - "firstByte": "首字节", - "testedAt": "测试时间" - }, - "tokenUsage": { - "title": "Token 用量", - "input": "输入", - "output": "输出", - "cacheCreation": "缓存创建", - "cacheRead": "缓存读取" - }, - "streamInfo": { - "title": "流式响应信息", - "isStreaming": "流式响应", - "chunksCount": "数据块数", - "yes": "是", - "no": "否" - }, - "rawResponse": { - "title": "完整响应体", - "hint": "此处显示原始响应内容,您可以在此检查关键词是否存在于响应中" - }, - "errorDetails": { - "title": "错误详情", - "type": "错误类型" - }, - "copyText": { - "status": "状态", - "message": "消息", - "latency": "延迟", - "httpStatus": "HTTP 状态", - "model": "模型", - "usage": "用量", - "inputOutput": "输入 {input} / 输出 {output} tokens", - "response": "响应", - "error": "错误", - "testedAt": "测试时间", - "validationDetails": "验证详情", - "httpCheck": "HTTP 检查", - "latencyCheck": "延迟检查", - "contentCheck": "内容验证" - }, - "judgment": "判定" - } - }, - "urlPreview": { - "title": "URL 拼接预览", - "invalidUrl": "无效的 URL 格式", - "invalidUrlDesc": "请输入有效的 HTTP/HTTPS 地址", - "duplicatePath": "检测到重复路径", - "copy": "复制", - "copySuccess": "已复制 {name} 到剪贴板", - "copyFailed": "复制失败" - }, - "modelSelect": { - "allowAllModels": "允许所有 {type} 模型", - "selectedCount": "已选择 {count} 个模型", - "searchPlaceholder": "搜索模型名称...", - "loading": "加载中...", - "notFound": "未找到模型", - "selectAll": "全选 ({count})", - "clear": "清空", - "manualAdd": "手动添加模型", - "manualPlaceholder": "输入模型名称(如 gpt-5-turbo)", - "manualDesc": "支持添加任意模型名称(不限于价格表中的模型)", - "claude": "Claude", - "openai": "OpenAI", - "gemini": "Gemini", - "sourceUpstream": "上游", - "sourceUpstreamDesc": "模型列表来自上游服务商 API", - "sourceFallback": "本地", - "sourceFallbackDesc": "使用本地价格表中的模型列表(上游获取失败或不支持)", - "refresh": "刷新模型列表" - }, - "modelRedirect": { - "currentRules": "当前规则 ({count})", - "addNewRule": "添加新规则", - "sourceModel": "用户请求的模型", - "targetModel": "实际转发的模型", - "sourcePlaceholder": "例如: claude-sonnet-4-5-20250929", - "targetPlaceholder": "例如: glm-4.6", - "add": "添加", - "sourceEmpty": "源模型名称不能为空", - "targetEmpty": "目标模型名称不能为空", - "alreadyExists": "模型 \"{model}\" 已存在重定向规则", - "description": "将 Claude Code 客户端请求的模型(如 claude-sonnet-4.5)重定向到上游供应商实际支持的模型(如 glm-4.6、gemini-pro)。用于成本优化或接入第三方 AI 服务。", - "emptyState": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。" - }, - "name": { - "label": "服务商名称 *", - "placeholder": "例如: 智谱" - }, - "namePlaceholder": "输入供应商名称", - "baseUrl": "基础 URL", - "baseUrlPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic", - "baseUrlRequired": "请先填写供应商 URL", - "apiKey": "API 密钥", - "apiKeyPlaceholder": "输入 API 密钥", - "apiKeyOptional": "留空则不更改密钥", - "weight": "权重", - "weightDesc": "加权随机概率。同优先级内,权重越高被选中概率越大。例如权重 1:2:3 的概率为 16%:33%:50%", - "priority": "优先级", - "priorityDesc": "数值越小优先级越高(0 最高)。系统只从最高优先级的供应商中选择。建议:主力=0,备用=1,紧急备份=2", - "enabled": "启用", - "costMultiplier": "成本倍数", - "costMultiplierDesc": "成本计算倍数。官方供应商=1.0,便宜 20%=0.8,贵 20%=1.2(支持最多 4 位小数)", - "limitConcurrent": "并发 Session 限制", - "limitConcurrentDesc": "例如: 供应商 C 并发限制 2,当前活跃 Session 数:2", - "limitAmount5h": "5 小时消费上限 (USD)", - "limitAmount5hDesc": "例如: 供应商 B 的 5 小时限额 $10,已消耗 $9.8", - "limitAmountWeekly": "周消费上限 (USD)", - "limitAmountMonthly": "月消费上限 (USD)", - "modelRedirects": "模型重定向", - "modelRedirectsDesc": "将 Claude Code 客户端请求的模型(如 claude-sonnet-4.5)重定向到上游供应商实际支持的模型(如 glm-4.6、gemini-pro)。用于成本优化或接入第三方 AI 服务。", - "sourceModel": "源模型名称", - "sourceModelPlaceholder": "例如: claude-sonnet-4-5-20250929", - "sourceModelRequired": "源模型名称不能为空", - "targetModel": "目标模型名称", - "targetModelPlaceholder": "例如: glm-4.6", - "targetModelRequired": "目标模型名称不能为空", - "addRedirect": "添加重定向", - "removeRedirect": "移除重定向", - "allowAllModels": "✓ 允许所有模型(推荐)", - "proxy": "代理", - "proxyUrl": "代理地址", - "proxyUrlPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", - "proxyFallback": "代理失败降级", - "proxyFallbackDesc": "启用后,代理连接失败时自动尝试直接连接供应商", - "testProxy": "测试连接", - "testProxySuccess": "代理连接成功", - "testProxyFailed": "测试代理连接失败", - "testProxyFailedError": "测试连接失败:", - "proxyConfigured": "已配置代理", - "proxyNotConfigured": "未配置", - "providerType": "供应商类型", - "selectProviderType": "选择供应商类型", - "codexInstructions": "Codex Instructions 策略", - "codexInstructionsDesc": "(决定调度策略)", - "codexInstructionsAuto": "自动 (推荐)", - "codexInstructionsForce": "强制官方", - "codexInstructionsKeep": "保留原值", - "group": "分组", - "groupPlaceholder": "例如: premium, economy", - "remark": "备注", - "remarkPlaceholder": "可选:添加说明...", - "sort": "排序供应商", - "sortByName": "按名称 (A-Z)", - "sortByWeight": "按权重 (高-低)", - "sortByPriority": "按优先级 (高-低)", - "sortByCreated": "按创建时间 (新-旧)", - "sortByCost": "按成本排序", - "filterByType": "筛选供应商类型", - "searchPlaceholder": "搜索供应商名称、URL、备注...", - "clearSearch": "清除搜索", - "limit0Means": "0 表示无限制", - "leaveEmpty": "留空表示无限制", - "providerName": "服务商名称", - "providerNameRequired": "服务商名称 *", - "providerNamePlaceholder": "例如: 智谱", - "apiAddress": "API 地址", - "apiAddressRequired": "API 地址 *", - "apiAddressPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic", - "apiKeyRequired": "API 密钥 *", - "apiKeyLeaveEmpty": "(留空不更改)", - "apiKeyLeaveEmptyDesc": "留空则不更改密钥", - "apiKeyCurrent": "当前密钥:", - "websiteUrl": { - "label": "供应商官网地址", - "placeholder": "https://example.com", - "desc": "供应商官网地址,用于快速跳转管理" - }, - "websiteUrlPlaceholder": "https://example.com", - "websiteUrlDesc": "供应商官网地址,用于快速跳转管理", - "websiteUrlInvalid": "请输入有效的供应商官网地址", - "expandAll": "展开全部高级配置", - "collapseAll": "折叠全部高级配置", - "routingConfig": "路由配置", - "routingConfigSummary": "{models} 个模型白名单, {redirects} 个重定向", - "routingConfigNone": "未配置", - "providerTypeDesc": "选择供应商的 API 格式类型。", - "providerTypeDisabledNote": "注:OpenAI Compatible 类型功能正在开发中,暂不可用", - "modelRedirectsLabel": "模型重定向配置", - "modelRedirectsOptional": "(可选)", - "modelRedirectsEmpty": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。", - "modelRedirectsCurrentRules": "当前规则 ({count})", - "modelRedirectsAddNew": "添加新规则", - "modelRedirectsSourceModel": "用户请求的模型", - "modelRedirectsSourcePlaceholder": "例如: claude-sonnet-4-5-20250929", - "modelRedirectsTargetModel": "实际转发的模型", - "modelRedirectsTargetPlaceholder": "例如: glm-4.6", - "modelRedirectsSourceRequired": "源模型名称不能为空", - "modelRedirectsTargetRequired": "目标模型名称不能为空", - "modelRedirectsExists": "模型 \"{model}\" 已存在重定向规则", - "joinClaudePool": "加入 Claude 调度池", - "joinClaudePoolDesc": "启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度", - "joinClaudePoolHelp": "仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用户请求 claude-* 模型时,此供应商也会参与调度选择。", - "modelWhitelist": "模型白名单", - "modelWhitelistDesc": "限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。", - "modelWhitelistLabel": "允许的模型", - "modelWhitelistSelected": "已选择 {count} 个模型", - "modelWhitelistLoading": "加载中...", - "modelWhitelistNotFound": "未找到模型", - "modelWhitelistSearchPlaceholder": "搜索模型名称...", - "modelWhitelistSelectAll": "全选 ({count})", - "modelWhitelistClear": "清空", - "modelWhitelistManualAdd": "手动添加模型", - "modelWhitelistManualPlaceholder": "输入模型名称(如 gpt-5-turbo)", - "modelWhitelistManualDesc": "支持添加任意模型名称(不限于价格表中的模型)", - "modelWhitelistAllowAll": "允许所有 {type} 模型", - "modelWhitelistAllowAllClause": "允许所有 Claude 模型", - "modelWhitelistAllowAllOpenAI": "允许所有 OpenAI 模型", - "modelWhitelistSelectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。", - "scheduleParams": "调度参数", - "priorityLabel": "优先级", - "priorityPlaceholder": "0", - "weightLabel": "权重", - "weightPlaceholder": "1", - "costMultiplierLabel": "成本倍率", - "costMultiplierPlaceholder": "1.0", - "providerGroupLabel": "供应商分组", - "providerGroupPlaceholder": "例如: premium, economy", - "providerGroupDesc": "供应商分组标签。只有用户的 providerGroup 与此值匹配时,该用户才能使用此供应商。示例:设置为 \"premium\" 表示只供 providerGroup=\"premium\" 的用户使用", - "rateLimitConfig": "限流配置", - "rateLimitConfigSummary": "5h: ${fiveHour}, 周: ${weekly}, 月: ${monthly}, 并发: {concurrent}", - "rateLimitConfigNone": "无限制", - "limit5hLabel": "5小时消费上限 (USD)", - "limitWeeklyLabel": "周消费上限 (USD)", - "limitMonthlyLabel": "月消费上限 (USD)", - "limitConcurrentLabel": "并发 Session 上限", - "limitPlaceholderUnlimited": "留空表示无限制", - "limitPlaceholder0": "0 表示无限制", - "circuitBreakerConfig": "熔断器配置", - "circuitBreakerConfigSummary": "{failureThreshold} 次失败 / {openDuration} 分钟熔断 / {successThreshold} 次成功恢复 / 每个供应商最多 {maxRetryAttempts} 次尝试", - "circuitBreakerDesc": "供应商连续失败时自动熔断,避免影响整体服务质量", - "failureThreshold": "失败阈值(次)", - "failureThresholdPlaceholder": "5", - "failureThresholdDesc": "连续失败多少次后触发熔断", - "openDuration": "熔断时长(分钟)", - "openDurationPlaceholder": "30", - "openDurationDesc": "熔断后多久自动进入半开状态", - "successThreshold": "恢复阈值(次)", - "successThresholdPlaceholder": "2", - "successThresholdDesc": "半开状态下成功多少次后完全恢复", - "maxRetryAttempts": { - "label": "单供应商最大尝试次数", - "placeholder": "2", - "desc": "包含首次调用在内,单个供应商最多尝试几次后切换。留空使用系统默认值。" - }, - "proxyConfig": "代理配置", - "proxyConfigSummary": "已配置代理", - "proxyConfigSummaryFallback": " (启用降级)", - "proxyConfigNone": "未配置", - "proxyConfigDesc": "配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)", - "proxyAddressLabel": "代理地址", - "proxyAddressOptional": "(可选)", - "proxyAddressPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", - "proxyAddressFormats": "支持格式:", - "proxyFallbackLabel": "代理失败时降级到直连", - "proxyTestLabel": "连接测试", - "proxyTestButton": "测试连接", - "proxyTestTesting": "测试中...", - "proxyTestSuccess": "连接成功", - "proxyTestFailed": "连接失败", - "proxyTestDesc": "测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)", - "proxyTestFillUrl": "请先填写供应商 URL", - "proxyTestResultSuccess": "连接成功 {via}", - "proxyTestViaProxy": "(通过代理)", - "proxyTestViaDirect": "(直连)", - "proxyTestResponseTime": "响应时间: {time}", - "proxyTestStatusCode": "| 状态码: {code}", - "proxyTestResultFailed": "连接失败", - "proxyTestTimeout": "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确", - "proxyTestProxyError": "代理错误: {error}", - "proxyTestNetworkError": "网络错误: {error}", - "proxyTestResultMessage": "{message}", - "proxyTestResultStatusCode": "状态码: {code}", - "proxyTestResultResponseTime": "响应时间: {time}ms", - "proxyTestResultConnectionMethod": "连接方式: {via}", - "proxyTestResultConnectionMethodProxy": "代理", - "proxyTestResultConnectionMethodDirect": "直连", - "proxyTestResultErrorType": "错误类型: {type}", - "codexStrategyConfig": "Codex Instructions 策略", - "codexStrategyConfigAuto": "自动 (推荐)", - "codexStrategyConfigForce": "强制官方", - "codexStrategyConfigKeep": "透传原样", - "codexStrategyDesc": "控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性", - "codexStrategySelect": "策略选择", - "codexStrategyAutoLabel": "自动 (推荐)", - "codexStrategyAutoDesc": "透传客户端 instructions,400 错误时自动重试官方 prompt", - "codexStrategyForceLabel": "强制官方", - "codexStrategyForceDesc": "始终使用官方 Codex CLI instructions(约 4000+ 字)", - "codexStrategyKeepLabel": "透传原样", - "codexStrategyKeepDesc": "始终透传客户端 instructions,不自动重试(适用于宽松中转站)", - "codexStrategyHint": "提示: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方 instructions,选择\"自动\"或\"强制官方\"策略", - "confirmAdd": "确认添加", - "confirmUpdate": "确认更新", - "confirmAddPending": "添加中...", - "confirmUpdatePending": "更新中...", - "deleteButton": "删除", - "validUrlRequired": "请输入有效的 API 地址", - "filterProvider": "筛选供应商类型", - "filterAllProviders": "全部供应商", - "searchClear": "清除搜索", - "title": { - "create": "新增服务商", - "edit": "编辑服务商" - }, - "dialogDescription": "配置供应商信息及高级设置。", - "url": { - "label": "API 地址 *", - "placeholder": "例如: https://open.bigmodel.cn/api/anthropic" - }, - "key": { - "label": "API 密钥", - "leaveEmpty": "(留空不更改)", - "placeholder": "输入 API 密钥", - "leaveEmptyDesc": "留空则不更改密钥", - "currentKey": "当前密钥: {key}" - }, - "buttons": { - "expandAll": "展开全部高级配置", - "collapseAll": "折叠全部高级配置", - "submit": "确认添加", - "submitting": "添加中...", - "update": "确认更新", - "updating": "更新中...", - "delete": "删除" - }, - "common": { - "core": "核心" - }, - "sections": { - "routing": { - "title": "路由配置", - "summary": { - "models": "{count} 个模型白名单", - "redirects": "{count} 个重定向", - "none": "未配置" - }, - "providerType": { - "label": "供应商类型", - "desc": "(决定调度策略)", - "placeholder": "选择供应商类型" - }, - "providerTypeDesc": "选择供应商的 API 格式类型。", - "providerTypeDisabledNote": "注:Gemini CLI 和 OpenAI Compatible 类型功能正在开发中,暂不可用", - "modelRedirects": { - "label": "模型重定向配置", - "optional": "(可选)" - }, - "joinClaudePool": { - "label": "加入 Claude 调度池", - "desc": "启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度", - "help": "仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用户请求 claude-* 模型时,此供应商也会参与调度选择。" - }, - "preserveClientIp": { - "label": "透传客户端 IP", - "desc": "向上游转发 x-forwarded-for / x-real-ip,可能暴露真实来源 IP", - "help": "默认关闭以保护隐私;仅在需要上游感知终端 IP 时开启。" - }, - "modelWhitelist": { - "title": "模型白名单", - "desc": "限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。", - "label": "允许的模型", - "optional": "(可选)", - "allowAll": "✓ 允许所有模型(推荐)", - "selectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。", - "moreModels": "+{count} 更多" - }, - "scheduleParams": { - "title": "调度参数", - "priority": { - "label": "优先级", - "placeholder": "0", - "desc": "数值越小优先级越高(0 最高)。系统只从最高优先级的供应商中选择。建议:主力=0,备用=1,紧急备份=2" - }, - "weight": { - "label": "权重", - "placeholder": "1", - "desc": "加权随机概率。同优先级内,权重越高被选中概率越大。例如权重 1:2:3 的概率为 16%:33%:50%" - }, - "costMultiplier": { - "label": "成本倍率", - "placeholder": "1.0", - "desc": "成本计算倍数。官方供应商=1.0,便宜 20%=0.8,贵 20%=1.2(支持最多 4 位小数)" - }, - "group": { - "label": "供应商分组", - "placeholder": "输入分组标签", - "desc": "供应商分组标签(支持多个,逗号分隔)。只有用户的 providerGroup 与此值匹配时,该用户才能使用此供应商。留空=对所有用户开放" - } - }, - "cacheTtl": { - "label": "Cache TTL 覆写", - "options": { - "inherit": "不覆写(跟随客户端)", - "5m": "5 分钟", - "1h": "1 小时" - }, - "desc": "强制设置 prompt cache TTL;仅影响包含 cache_control 的请求。" - }, - "context1m": { - "label": "1M 上下文窗口", - "options": { - "inherit": "跟随客户端", - "forceEnable": "强制启用", - "disabled": "禁用" - }, - "desc": "配置 1M 上下文窗口支持。仅对 Sonnet 模型生效(claude-sonnet-4-5、claude-sonnet-4)。启用后将应用阶梯定价。" - }, - "codexOverrides": { - "reasoningEffort": { - "label": "推理等级覆写", - "help": "控制模型在输出前用于推理的强度(推理 token 数量)。选择“跟随客户端”表示不改写请求;选择其他值则强制覆写 reasoning.effort。注意:none 仅 GPT-5.1 系列支持;xhigh 仅 GPT-5.1-Codex-Max 支持,模型不支持会返回错误。", - "options": { - "inherit": "不覆写(跟随客户端)", - "minimal": "minimal(更省、更快)", - "low": "low", - "medium": "medium(默认)", - "high": "high(更强推理)", - "xhigh": "xhigh(仅 GPT-5.1-Codex-Max)", - "none": "none(仅 GPT-5.1)" - } - }, - "reasoningSummary": { - "label": "推理摘要覆写", - "help": "控制是否返回推理摘要。auto 返回精简摘要,detailed 返回更详细摘要;“跟随客户端”不改写 reasoning.summary。", - "options": { - "inherit": "不覆写(跟随客户端)", - "auto": "auto(精简)", - "detailed": "detailed(更详细)" - } - }, - "textVerbosity": { - "label": "输出冗长度覆写", - "help": "控制模型输出的详细程度。low 更简洁,high 更详细;“跟随客户端”不改写 text.verbosity。", - "options": { - "inherit": "不覆写(跟随客户端)", - "low": "low(简洁)", - "medium": "medium(默认)", - "high": "high(更详细)" - } - }, - "parallelToolCalls": { - "label": "并行工具调用覆写", - "help": "控制是否允许并行 tool calls。关闭可能降低工具调用并发能力;“跟随客户端”不改写 parallel_tool_calls。", - "options": { - "inherit": "不覆写(跟随客户端)", - "true": "强制开启", - "false": "强制关闭" - } - } - } - }, - "rateLimit": { - "title": "限流配置", - "summary": { - "fiveHour": "5h: {amount}", - "daily": "日: {amount} (重置 {resetTime})", - "weekly": "周: {amount}", - "monthly": "月: {amount}", - "total": "总: {amount}", - "concurrent": "并发: {count}", - "none": "无限制" - }, - "limit5h": { - "label": "5小时消费上限 (USD)", - "placeholder": "留空表示无限制" - }, - "limitDaily": { - "label": "每日消费上限 (USD)", - "placeholder": "留空表示无限制" - }, - "dailyResetMode": { - "label": "每日重置模式", - "options": { - "fixed": "固定时间重置", - "rolling": "滚动窗口(24小时)" - }, - "desc": { - "fixed": "每天固定时间点重置配额", - "rolling": "从首次调用开始计算,24小时后重置" - } - }, - "dailyResetTime": { - "label": "每日重置时间 (HH:mm)" - }, - "limitWeekly": { - "label": "周消费上限 (USD)", - "placeholder": "留空表示无限制" - }, - "limitMonthly": { - "label": "月消费上限 (USD)", - "placeholder": "留空表示无限制" - }, - "limitTotal": { - "label": "总消费上限 (USD)", - "placeholder": "留空表示无限制" - }, - "limitConcurrent": { - "label": "并发 Session 上限", - "placeholder": "0 表示无限制" - } - }, - "circuitBreaker": { - "title": "熔断器配置", - "summary": "{failureThreshold} 次失败 / {openDuration} 分钟熔断 / {successThreshold} 次成功恢复 / 每个供应商最多 {maxRetryAttempts} 次尝试", - "desc": "供应商连续失败时自动熔断,避免影响整体服务质量", - "failureThreshold": { - "label": "失败阈值(次)", - "placeholder": "5", - "desc": "连续失败多少次后触发熔断" - }, - "openDuration": { - "label": "熔断时长(分钟)", - "placeholder": "30", - "desc": "熔断后多久自动进入半开状态" - }, - "successThreshold": { - "label": "恢复阈值(次)", - "placeholder": "2", - "desc": "半开状态下成功多少次后完全恢复" - }, - "maxRetryAttempts": { - "label": "单供应商最大尝试次数", - "placeholder": "2", - "desc": "包含首次调用在内,单个供应商最多尝试次数,超过后切换其他供应商。留空使用系统默认值。" - } - }, - "proxy": { - "title": "代理配置", - "summary": { - "configured": "已配置代理", - "fallback": " (启用降级)", - "none": "未配置" - }, - "desc": "配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)", - "url": { - "label": "代理地址", - "optional": "(可选)", - "placeholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", - "formats": "支持格式:" - }, - "fallback": { - "label": "代理失败时降级到直连", - "desc": "启用后,代理连接失败时自动尝试直接连接供应商" - }, - "test": { - "label": "连接测试", - "desc": "测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)" - } - }, - "timeout": { - "title": "超时配置", - "summary": "首字: {streaming}s | 流式间隔: {idle}s | 非流式: {nonStreaming}s", - "desc": "配置请求超时时间,0 表示禁用超时", - "streamingFirstByte": { - "label": "流式首字节超时(秒)", - "placeholder": "30", - "desc": "流式请求首字节超时,范围 1-120 秒,默认 30 秒", - "core": "true" - }, - "streamingIdle": { - "label": "流式静默期超时(秒)", - "placeholder": "60", - "desc": "流式请求静默期超时,范围 60-600 秒,填 0 禁用(防止中途卡住)", - "core": "true" - }, - "nonStreamingTotal": { - "label": "非流式总超时(秒)", - "placeholder": "600", - "desc": "非流式请求总超时,范围 60-1200 秒,默认 600 秒(10 分钟)", - "core": "true" - }, - "disableHint": "设为 0 表示禁用该超时(仅用于灰度回退场景,不推荐)" - }, - "apiTest": { - "title": "供应商模型测试", - "summary": "验证供应商与模型连通性", - "desc": "测试供应商模型是否可用,默认与路由配置中选择的供应商类型保持一致。", - "testLabel": "供应商模型测试" - }, - "codexStrategy": { - "title": "Codex Instructions 策略", - "summary": { - "auto": "自动 (推荐)", - "force": "强制官方", - "keep": "透传原样" - }, - "desc": "控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性", - "select": { - "label": "策略选择", - "auto": { - "label": "自动 (推荐)", - "desc": "透传客户端 instructions,400 错误时自动重试官方 prompt" - }, - "force": { - "label": "强制官方", - "desc": "始终使用官方 Codex CLI instructions(约 4000+ 字)" - }, - "keep": { - "label": "透传原样", - "desc": "始终透传客户端 instructions,不自动重试(适用于宽松中转站)" - }, - "placeholder": "选择策略" - }, - "hint": "提示: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方 instructions,选择\"自动\"或\"强制官方\"策略" - }, - "mcpPassthrough": { - "title": "MCP 透传配置", - "summary": { - "none": "不启用", - "minimax": "Minimax", - "glm": "智谱 GLM", - "custom": "自定义 (预留)" - }, - "desc": "启用后,将 MCP 工具调用透传到指定的 AI 服务商(如 minimax 的图片识别、联网搜索)", - "select": { - "label": "透传类型", - "none": { - "label": "不启用", - "desc": "不启用 MCP 透传功能(默认)" - }, - "minimax": { - "label": "Minimax", - "desc": "透传到 minimax MCP 服务(支持图片识别、联网搜索等工具)" - }, - "glm": { - "label": "智谱 GLM", - "desc": "透传到智谱 MCP 服务(支持图片分析、视频分析等工具)" - }, - "custom": { - "label": "自定义", - "desc": "透传到自定义 MCP 服务(预留,暂未实现)" - }, - "placeholder": "选择透传类型" - }, - "hint": "提示: MCP 透传功能允许 Claude Code 客户端使用第三方 AI 服务商提供的工具能力(如图片识别、联网搜索)", - "urlLabel": "MCP 透传 URL", - "urlPlaceholder": "https://api.minimaxi.com", - "urlDesc": "MCP 服务的基础 URL。留空则自动从提供商 URL 提取基础域名", - "urlAuto": "自动提取: {url}" - } - }, - "providerTypes": { - "claude": "Claude (Anthropic Messages API)", - "claudeAuth": "Claude (Anthropic Auth Token)", - "codex": "Codex (Response API)", - "gemini": "Gemini (Google Gemini API)", - "geminiCli": "Gemini CLI", - "geminiCliDisabled": " - 功能开发中", - "openaiCompatible": "OpenAI Compatible", - "openaiCompatibleDisabled": " - 功能开发中" - }, - "deleteDialog": { - "title": "删除服务商", - "description": "确定要删除服务商 \"{name}\" 吗?此操作不可恢复。", - "cancel": "取消", - "confirm": "确认删除" - }, - "failureThresholdConfirmDialog": { - "title": "确认特殊配置", - "descriptionDisabledPrefix": "您将熔断失败阈值设置为 ", - "descriptionDisabledValue": "0", - "descriptionDisabledMiddle": ",这表示", - "descriptionDisabledAction": "禁用熔断器", - "descriptionDisabledSuffix": ",供应商将不会因为连续失败而被熔断。", - "descriptionHighValuePrefix": "您将熔断失败阈值设置为 ", - "descriptionHighValueSuffix": ",这是一个较高的值,可能会导致供应商在大量失败后才被熔断。", - "confirmQuestion": "是否确认保存此配置?", - "cancel": "取消", - "confirm": "确认保存" - }, - "errors": { - "invalidUrl": "请输入有效的 API 地址", - "invalidWebsiteUrl": "请输入有效的供应商官网地址", - "groupTagTooLong": "分组标签总长度不能超过 {max} 个字符", - "addFailed": "添加服务商失败", - "updateFailed": "更新服务商失败", - "deleteFailed": "删除服务商失败" - }, - "success": { - "created": "添加服务商成功", - "createdDesc": "服务商 \"{name}\" 已添加" - } - }, - "scheduling": "调度策略详解", - "schedulingDesc": "了解供应商选择如何进行优先级分层、会话复用、负载均衡和故障转移", - "guide": { - "priority": "优先级分层选择", - "priorityExample": "有 4 个已启用的供应商,优先级各不相同", - "priorityStep": "系统首先按优先级过滤,只从最高优先级的供应商中选择", - "priorityResult": "筛选出最高优先级(0)的供应商:A, C", - "weight": "使用权重进行随机选择,权重越高被选中概率越大", - "weightExample": "C (权重 3), A (权重 1)", - "weightCalc": "C 被选中概率 75%, A 被选中概率 25%", - "costSort": "成本排序降级", - "costSortExample": "所有供应商:A (default), B (premium), C (premium), D (economy)", - "costSortResult": "排序后:C (0.8x), A (1.0x)", - "costSortProb": "成本更低的 C 有更高的被选中概率", - "session": "会话复用机制", - "sessionDesc": "如果上次使用的供应商不可用,则重新选择", - "sessionExample": "最近一次请求使用了供应商 B", - "sessionLastUsed": "B 可用,直接复用,跳过随机选择", - "sessionExpired": "Session 过期(5 分钟)后自动释放", - "sessionUnavailable": "上次使用的供应商 B 已被禁用或熔断", - "sessionFallback": "从其他可用供应商中选择", - "group": "用户分组过滤", - "groupDesc": "如果用户指定了供应商组,系统会优先从该组中选择", - "groupExample": "用户配置了 providerGroup = 'premium'", - "groupFiltered": "只从 A 和 C 中选择,B 和 D 被过滤", - "groupFallback": "如果用户组内没有可用供应商,降级到所有供应商", - "groupUnavailable": "用户组 'vip' 内的供应商全部禁用或超限", - "groupDowngrade": "记录警告并从全局供应商池中选择", - "health": "健康度过滤(熔断器 + 限流)", - "healthCheck": "检查 B 是否启用且健康", - "healthCheckCircuit": "供应商 A 连续失败 5 次,熔断器状态:open", - "healthCheckConcurrent": "检查当前活跃 Session 数是否超过配置的并发限制", - "healthCheckConcurrentExample": "供应商 C 并发限制 2,当前活跃 Session 数:2", - "healthCheckAmountLimit": "检查 5 小时、7 天、30 天的消费额度是否超限", - "healthCheckAmountLimitExample": "供应商 B 的 5 小时限额 $10,已消耗 $9.8", - "healthFiltered": "B 被过滤(接近限额),剩余:C, D", - "healthFiltered2": "C 被过滤(已满),剩余:D", - "randomSelect": "加权随机", - "randomResult": "最终随机选择了 C", - "history": "检查历史请求", - "historyDesc": "查询该 API Key 最近 10 秒内使用的供应商", - "circuitBreaker": "熔断器检查", - "circuitBreakerOpen": "A 被过滤,剩余:B, C, D", - "circuitBreakerRecovery": "A 在 60 秒后自动恢复到半开状态", - "circuitBreakerRecovery5h": "5 小时窗口滑动后自动恢复", - "reset": "手动解除熔断", - "resetSuccess": "熔断器已重置", - "title": "核心原则", - "priorityFirst": "1️⃣ 优先级优先:只从最高优先级(数值最小)的供应商中选择", - "costOptimize": "2️⃣ 成本优化:同优先级内,成本倍率低的供应商有更高概率", - "healthFilter": "3️⃣ 健康过滤:自动跳过熔断或超限的供应商", - "sessionReuse": "4️⃣ 会话复用:连续对话复用同一供应商,节省上下文成本", - "scenariosTitle": "交互式场景演示", - "bestPracticesTitle": "最佳实践建议", - "bestPracticesPriority": "• 优先级设置:核心供应商设为 0,备用供应商设为 1-3", - "bestPracticesWeight": "• 权重配置:根据供应商容量设置权重(容量大 = 权重高)", - "bestPracticesCost": "• 成本倍率:官方倍率为 1.0,自建服务可设置为 0.8-1.2", - "bestPracticesLimit": "• 限额设置:根据预算设置 5 小时、7 天、30 天限额", - "bestPracticesConcurrent": "• 并发控制:根据供应商 API 限制设置 Session 并发数", - "scenario1Title": "优先级分层选择", - "scenario1Desc": "系统首先按优先级过滤,只从最高优先级的供应商中选择", - "scenario1Step1": "初始状态", - "scenario1Step1Desc": "有 4 个已启用的供应商,优先级各不相同", - "scenario1Step1Before": "供应商 A (优先级 0), B (优先级 1), C (优先级 0), D (优先级 2)", - "scenario1Step1After": "筛选出最高优先级(0)的供应商:A, C", - "scenario1Step1Decision": "只从 A 和 C 中选择,B 和 D 被过滤", - "scenario1Step2": "成本排序", - "scenario1Step2Desc": "在同优先级内,按成本倍率从低到高排序", - "scenario1Step2Before": "A (成本 1.0x), C (成本 0.8x)", - "scenario1Step2After": "排序后:C (0.8x), A (1.0x)", - "scenario1Step2Decision": "成本更低的 C 有更高的被选中概率", - "scenario1Step3": "加权随机", - "scenario1Step3Desc": "使用权重进行随机选择,权重越高被选中概率越大", - "scenario1Step3Before": "C (权重 3), A (权重 1)", - "scenario1Step3After": "C 被选中概率 75%, A 被选中概率 25%", - "scenario1Step3Decision": "最终随机选择了 C", - "scenario2Title": "用户分组过滤", - "scenario2Desc": "如果用户指定了供应商组,系统会优先从该组中选择", - "scenario2Step1": "检查用户分组", - "scenario2Step1Desc": "用户配置了 providerGroup = 'premium'", - "scenario2Step1Before": "所有供应商:A (default), B (premium), C (premium), D (economy)", - "scenario2Step1After": "过滤出 'premium' 组:B, C", - "scenario2Step1Decision": "只从 B 和 C 中选择", - "scenario2Step2": "分组降级", - "scenario2Step2Desc": "如果用户组内没有可用供应商,降级到所有供应商", - "scenario2Step2Before": "用户组 'vip' 内的供应商全部禁用或超限", - "scenario2Step2After": "降级到所有启用的供应商:A, B, C, D", - "scenario2Step2Decision": "记录警告并从全局供应商池中选择", - "scenario3Title": "健康度过滤(熔断器 + 限流)", - "scenario3Desc": "系统自动过滤掉熔断或超限的供应商", - "scenario3Step1": "熔断器检查", - "scenario3Step1Desc": "连续失败 5 次后熔断器打开,60 秒内不可用", - "scenario3Step1Before": "供应商 A 连续失败 5 次,熔断器状态:open", - "scenario3Step1After": "A 被过滤,剩余:B, C, D", - "scenario3Step1Decision": "A 在 60 秒后自动恢复到半开状态", - "scenario3Step2": "金额限流", - "scenario3Step2Desc": "检查 5 小时、7 天、30 天的消费额度是否超限", - "scenario3Step2Before": "供应商 B 的 5 小时限额 $10,已消耗 $9.8", - "scenario3Step2After": "B 被过滤(接近限额),剩余:C, D", - "scenario3Step2Decision": "5 小时窗口滑动后自动恢复", - "scenario3Step3": "并发 Session 限制", - "scenario3Step3Desc": "检查当前活跃 Session 数是否超过配置的并发限制", - "scenario3Step3Before": "供应商 C 并发限制 2,当前活跃 Session 数:2", - "scenario3Step3After": "C 被过滤(已满),剩余:D", - "scenario3Step3Decision": "Session 过期(5 分钟)后自动释放", - "scenario4Title": "会话复用机制", - "scenario4Desc": "连续对话优先使用同一供应商,利用 Claude 的上下文缓存", - "scenario4Step1": "检查历史请求", - "scenario4Step1Desc": "查询该 API Key 最近 10 秒内使用的供应商", - "scenario4Step1Before": "最近一次请求使用了供应商 B", - "scenario4Step1After": "检查 B 是否启用且健康", - "scenario4Step1Decision": "B 可用,直接复用,跳过随机选择", - "scenario4Step2": "复用失效", - "scenario4Step2Desc": "如果上次使用的供应商不可用,则重新选择", - "scenario4Step2Before": "上次使用的供应商 B 已被禁用或熔断", - "scenario4Step2After": "进入正常选择流程", - "scenario4Step2Decision": "从其他可用供应商中选择", - "step": "步骤", - "before": "过滤前:", - "after": "过滤后:", - "decision": "决策:" - }, - "section": { - "title": "服务商管理", - "leaderboard": "供应商排行榜", - "description": "配置上游服务商的金额限流和并发限制,留空表示无限制。" - }, - "addProvider": "新增服务商", - "filter": { - "status": { - "all": "全部状态", - "active": "已启用", - "inactive": "已禁用" - }, - "groups": { - "label": "分组:", - "all": "全部", - "default": "default" - }, - "circuitBroken": "熔断" - }, - "editProvider": "编辑服务商", - "createProvider": "新增服务商", - "updateFailed": "更新服务商失败", - "deleteSuccess": "删除成功", - "confirmDeleteDesc": "确定要删除供应商 \"{name}\" 吗?此操作无法撤销。", - "confirmDeleteProvider": "确认删除供应商?", - "confirmDeleteProviderDesc": "确定要删除服务商\"{name}\"吗?此操作不可恢复。", - "noProviders": "暂无服务商配置", - "noProvidersDesc": "添加你的第一个 API 服务商", - "toggleSuccess": "供应商已{status}", - "toggleSuccessDesc": "供应商 \"{name}\" 状态已更新", - "toggleFailed": "状态切换失败", - "enabledStatus": "启用", - "disabledStatus": "禁用", - "searchResults": "找到 {count} 个匹配的供应商", - "searchNoResults": "未找到匹配的供应商", - "displayCount": "显示 {filtered} / {total} 个供应商", - "todayUsage": "今日用量", - "todayUsageCount": "{count} 次", - "circuitBroken": "熔断中", - "official": "官网", - "viewKey": "查看完整 API Key", - "viewKeyDesc": "请妥善保管,不要泄露给他人", - "keyLoading": "加载中...", - "resetCircuit": "熔断器已重置", - "resetCircuitDesc": "供应商 \"{name}\" 的熔断状态已解除", - "resetCircuitFailed": "重置熔断器失败", - "sort": { - "byName": "按名称 (A-Z)", - "byPriority": "按优先级 (高-低)", - "byWeight": "按权重 (高-低)", - "byActualPriority": "按实际选取顺序", - "byCreatedAt": "按创建时间 (新-旧)", - "placeholder": "排序供应商" - }, - "search": { - "placeholder": "搜索供应商名称、URL、备注...", - "clear": "清除搜索", - "found": "找到 {count} 个匹配的供应商", - "notFound": "未找到匹配的供应商", - "showing": "显示 {filtered} / {total} 个供应商" - } - }, - "prices": { - "title": "价格表", - "description": "管理平台基础配置与模型价格", - "section": { - "title": "模型价格", - "description": "管理 AI 模型的价格配置" - }, - "searchPlaceholder": "搜索模型名称...", - "filters": { - "all": "全部", - "local": "本地", - "anthropic": "Anthropic", - "openai": "OpenAI", - "vertex": "Vertex" - }, - "badges": { - "local": "本地" - }, - "capabilities": { - "assistantPrefill": "助手预填充", - "computerUse": "电脑使用", - "functionCalling": "函数调用", - "pdfInput": "PDF 输入", - "promptCaching": "Prompt 缓存", - "reasoning": "推理", - "responseSchema": "响应 Schema", - "toolChoice": "工具选择", - "vision": "视觉", - "statusSupported": "支持", - "statusUnsupported": "不支持", - "tooltip": "{label}: {status}" - }, - "sync": { - "button": "同步云端价格表", - "syncing": "同步中...", - "checking": "检查冲突...", - "successWithChanges": "价格表更新: 新增 {added} 个,更新 {updated} 个,未变化 {unchanged} 个", - "successNoChanges": "价格表已是最新,无需更新", - "failed": "同步失败", - "failedError": "同步失败: {error}", - "failedNoResult": "价格表更新成功但未返回处理结果", - "noModels": "未找到支持的模型价格", - "partialFailure": "部分更新成功,但有 {failed} 个模型失败", - "failedModels": "失败模型: {models}", - "skippedConflicts": "跳过 {count} 个手动模型" - }, - "conflict": { - "title": "选择要覆盖的冲突项", - "description": "以下模型存在手动维护的价格,勾选后将用 LiteLLM 价格覆盖,未勾选的保持本地不变", - "searchPlaceholder": "搜索模型...", - "table": { - "modelName": "模型", - "manualPrice": "手动价格", - "litellmPrice": "LiteLLM 价格", - "action": "操作" - }, - "viewDiff": "查看差异", - "diffTitle": "价格差异对比", - "diff": { - "field": "字段", - "manual": "手动", - "litellm": "LiteLLM", - "inputPrice": "输入价格", - "outputPrice": "输出价格", - "imagePrice": "图片价格", - "provider": "供应商", - "mode": "类型" - }, - "pagination": { - "showing": "显示 {from}-{to} 条,共 {total} 条" - }, - "selectedCount": "已选择 {count}/{total} 个模型", - "noMatch": "未找到匹配的模型", - "noConflicts": "无冲突项", - "applyOverwrite": "应用覆盖", - "applying": "应用中..." - }, - "table": { - "modelName": "模型名称", - "provider": "提供商", - "capabilities": "能力", - "price": "价格", - "inputPrice": "输入价格 ($/M)", - "outputPrice": "输出价格 ($/M)", - "priceInput": "输入", - "priceOutput": "输出", - "pricePerRequest": "按次", - "cacheReadPrice": "缓存读取 ($/M)", - "cacheCreationPrice": "缓存创建 ($/M)", - "cache5m": "5m", - "cache1h": "1h+", - "copyModelId": "复制模型 ID", - "updatedAt": "更新时间", - "actions": "操作", - "typeChat": "对话", - "typeImage": "图像生成", - "typeCompletion": "补全", - "typeUnknown": "未知", - "loading": "加载中...", - "noMatch": "未找到匹配的模型", - "noDataTitle": "暂无价格数据", - "noDataHint": "系统已内置价格表,请通过上方按钮同步或更新" - }, - "pagination": { - "showing": "显示 {from}-{to} 条,共 {total} 条", - "previous": "上一页", - "next": "下一页", - "perPageLabel": "每页", - "perPage": "每页 {size} 条" - }, - "stats": { - "totalModels": "共 {count} 个模型", - "searchResults": "搜索到 {count} 个结果", - "lastUpdated": "最后更新: {time}" - }, - "dialog": { - "title": "更新模型价格表", - "description": "选择包含模型价格数据的 JSON 或 TOML 文件并上传", - "selectFile": "点击选择 JSON/TOML 文件或拖拽到此处", - "fileSizeLimit": "文件大小不能超过 10MB", - "fileSizeLimitSmall": "文件大小不超过 10MB", - "invalidFileType": "请选择 JSON 或 TOML 格式的文件", - "fileTooLarge": "文件大小超过 10MB 限制", - "upload": "上传并更新", - "uploading": "上传中...", - "updatePriceTable": "更新价格表", - "updating": "更新中...", - "selectJson": "选择文件", - "updateSuccess": "价格表更新成功,共更新 {count} 个模型", - "updateFailed": "更新失败", - "systemHasBuiltIn": "系统已内置价格表", - "manualDownload": "你也可以手动下载", - "latestPriceTable": "云端价格表", - "andUploadViaButton": ",并通过上方按钮上传", - "cloudModelCountLoading": "云端模型数量加载中...", - "cloudModelCountFailed": "云端模型数量加载失败", - "supportedModels": "当前支持 {count} 个模型", - "results": { - "title": "更新结果", - "total": "总计: {total} 个模型", - "success": "成功: {success}", - "failed": "失败: {failed}", - "skipped": "跳过: {skipped}", - "more": " (+{count})", - "details": "详细信息", - "viewDetails": "查看详细日志" - } - }, - "addModel": "添加模型", - "editModel": "编辑模型", - "deleteModel": "删除模型", - "addModelDescription": "手动添加新的模型价格配置", - "editModelDescription": "编辑模型的价格配置", - "deleteConfirm": "确定要删除模型 {name} 吗?此操作不可撤销。", - "form": { - "modelName": "模型 ID", - "modelNamePlaceholder": "例如: gpt-5.2-codex", - "modelNameRequired": "模型 ID 不能为空", - "displayName": "展示名称(可选)", - "displayNamePlaceholder": "例如: GPT-5.2 Codex", - "type": "类型", - "provider": "供应商", - "providerPlaceholder": "例如: openai", - "requestPrice": "按次调用价格 ($/request)", - "inputPrice": "输入价格 ($/M tokens)", - "outputPrice": "输出价格 ($/M tokens)", - "outputPriceImage": "输出价格 ($/image)", - "cacheReadPrice": "缓存读取价格 ($/M tokens)", - "cacheCreationPrice5m": "缓存创建价格(5m,$/M tokens)", - "cacheCreationPrice1h": "缓存创建价格(1h+,$/M tokens)" - }, - "drawer": { - "prefillLabel": "搜索现有模型并预填充", - "prefillEmpty": "未找到匹配的模型", - "prefillFailed": "搜索失败", - "promptCachingHint": "仅当模型支持缓存时开启,并配置下方缓存价格", - "cachePricingTitle": "缓存价格" - }, - "actions": { - "edit": "编辑", - "more": "更多操作", - "delete": "删除" - }, - "toast": { - "createSuccess": "模型已添加", - "updateSuccess": "模型已更新", - "deleteSuccess": "模型已删除", - "saveFailed": "保存失败", - "deleteFailed": "删除失败" - } - }, - "sensitiveWords": { - "title": "敏感词管理", - "description": "配置敏感词过滤规则,拦截包含敏感内容的请求。", - "section": { - "title": "敏感词列表", - "description": "被敏感词拦截的请求不会转发到上游,也不会计费。支持包含匹配、精确匹配和正则表达式三种模式。" - }, - "add": "添加敏感词", - "addSuccess": "敏感词创建成功", - "addFailed": "创建敏感词失败", - "edit": "编辑敏感词", - "editSuccess": "敏感词更新成功", - "editFailed": "更新敏感词失败", - "delete": "删除敏感词", - "deleteSuccess": "敏感词删除成功", - "deleteFailed": "删除失败", - "enable": "敏感词已启用", - "disable": "敏感词已禁用", - "toggleFailed": "状态切换失败", - "toggleFailedError": "状态切换失败:", - "refreshCache": "刷新缓存", - "refreshCacheSuccess": "缓存刷新成功,已加载 {count} 个敏感词", - "refreshCacheFailed": "刷新缓存失败", - "cacheStats": "缓存统计: 包含({containsCount}) 精确({exactCount}) 正则({regexCount})", - "emptyState": "暂无敏感词,点击右上角\"添加敏感词\"开始配置。", - "confirmDelete": "确定要删除敏感词\"{word}\"吗?", - "dialog": { - "addTitle": "添加敏感词", - "addDescription": "配置敏感词过滤规则,被命中的请求将不会转发到上游。", - "editTitle": "编辑敏感词", - "editDescription": "修改敏感词配置,更改后将自动刷新缓存。", - "wordLabel": "敏感词 *", - "wordPlaceholder": "输入敏感词...", - "wordRequired": "请输入敏感词", - "matchTypeLabel": "匹配类型 *", - "matchTypeContains": "包含匹配 - 文本中包含该词即拦截", - "matchTypeExact": "精确匹配 - 完全匹配该词才拦截", - "matchTypeRegex": "正则表达式 - 支持复杂模式匹配", - "descriptionLabel": "说明", - "descriptionPlaceholder": "可选:添加说明...", - "creating": "创建中...", - "saving": "保存中..." - }, - "table": { - "word": "敏感词", - "matchType": "匹配类型", - "matchTypeContains": "包含匹配", - "matchTypeExact": "精确匹配", - "matchTypeRegex": "正则表达式", - "description": "说明", - "status": "状态", - "createdAt": "创建时间", - "actions": "操作" - } - }, - "requestFilters": { - "nav": "请求过滤", - "title": "请求过滤器", - "description": "配置 Header 删除/覆盖以及 Body 替换规则,在转发到上游前自动脱敏或规范化请求。", - "add": "新增过滤器", - "addSuccess": "创建成功", - "addFailed": "创建失败", - "edit": "编辑过滤器", - "editSuccess": "更新成功", - "editFailed": "更新失败", - "delete": "删除过滤器", - "deleteSuccess": "删除成功", - "deleteFailed": "删除失败", - "enable": "已启用", - "disable": "已禁用", - "confirmDelete": "确定删除过滤器\"{name}\"?", - "empty": "暂无过滤器,点击右上角新增。", - "refreshCache": "刷新缓存", - "refreshSuccess": "缓存已刷新,加载 {count} 条过滤器", - "refreshFailed": "刷新失败", - "dialog": { - "createTitle": "新增过滤器", - "editTitle": "编辑过滤器", - "name": "名称", - "scope": "作用域", - "action": "动作", - "target": "目标字段/路径", - "replacement": "替换值 (可选)", - "description": "描述 (可选)", - "priority": "优先级", - "matchType": "匹配类型", - "matchTypeContains": "包含", - "matchTypeExact": "精确匹配", - "matchTypeRegex": "正则", - "jsonPathPlaceholder": "例如: messages.0.content 或 data.items[0].token", - "targetPlaceholder": "Header 名称或文本/路径", - "replacementPlaceholder": "字符串或 JSON,留空表示删除", - "save": "保存", - "saving": "保存中...", - "validation": { - "fieldRequired": "名称和目标为必填项" - }, - "bindingType": "应用范围", - "bindingGlobal": "所有Provider(全局)", - "bindingProviders": "指定Provider", - "bindingGroups": "Provider分组", - "selectProviders": "选择Provider...", - "selectGroups": "选择分组...", - "searchProviders": "搜索Provider...", - "searchGroups": "搜索分组...", - "noProvidersFound": "未找到Provider", - "noGroupsFound": "未找到分组", - "providersSelected": "已选 {count} 个Provider", - "groupsSelected": "已选 {count} 个分组", - "loading": "加载中...", - "clear": "清空", - "selectAll": "全选" - }, - "table": { - "name": "名称", - "scope": "作用域", - "action": "动作", - "target": "目标", - "replacement": "替换值", - "priority": "优先级", - "apply": "范围", - "status": "状态", - "createdAt": "创建时间", - "actions": "操作" - }, - "scopeLabel": { - "header": "Header", - "body": "Body" - }, - "actionLabel": { - "remove": "删除 Header", - "set": "设置 Header", - "json_path": "JSON 路径替换", - "text_replace": "文本替换" - }, - "applyToAll": "应用于所有请求", - "providers": "供应商", - "groups": "组" - }, - "logs": { - "title": "日志管理", - "description": "动态调整系统日志级别,实时控制日志输出详细程度。", - "subtitle": "日志级别控制", - "subtitleDesc": "调整后立即生效,无需重启服务。适合生产环境排查问题时使用。", - "section": { - "title": "日志级别控制", - "description": "调整后立即生效,无需重启服务。" - }, - "levels": { - "fatal": { - "label": "Fatal", - "description": "仅致命错误" - }, - "error": { - "label": "Error", - "description": "错误信息" - }, - "warn": { - "label": "Warn", - "description": "警告 + 错误" - }, - "info": { - "label": "Info", - "description": "关键业务事件 + 警告 + 错误(推荐生产)" - }, - "debug": { - "label": "Debug", - "description": "调试信息 + 所有级别(推荐开发)" - }, - "trace": { - "label": "Trace", - "description": "极详细追踪 + 所有级别" - } - }, - "form": { - "currentLevel": "当前日志级别", - "selectLevel": "选择日志级别", - "save": "保存设置", - "saving": "保存中...", - "success": "日志级别已设置为: {level}", - "failed": "设置失败", - "failedError": "设置日志级别失败", - "fetchFailed": "获取日志级别失败", - "effectiveImmediately": "调整日志级别后立即生效,无需重启服务。", - "levelGuideTitle": "日志级别说明", - "levelGuideFatal": "Fatal/Error: 仅显示错误,日志最少,适合高负载生产环境", - "levelGuideWarn": "Warn: 包含警告(限流触发、熔断器打开等)+ 错误", - "levelGuideInfo": "Info(推荐生产): 显示关键业务事件(供应商选择、Session 复用、价格同步)+ 警告 + 错误", - "levelGuideDebug": "Debug(推荐开发): 包含详细调试信息,适合排查问题时使用", - "levelGuideTrace": "Trace: 极详细的追踪信息,包含所有细节", - "changeNotice": "当前级别为 {current},点击保存后将切换到 {selected}" - } - }, - "data": { - "title": "数据管理", - "description": "管理数据库的备份与恢复,支持完整数据导入导出和日志清理。", - "status": { - "loading": "加载中...", - "error": "获取数据库状态失败", - "retry": "重试", - "connected": "数据库连接正常", - "unavailable": "数据库不可用", - "tables": "{count} 个表" - }, - "cleanup": { - "rangeLabel": "清理范围", - "range": { - "7days": "一周前的日志 (7 天)", - "30days": "一个月前的日志 (30 天)", - "90days": "三个月前的日志 (90 天)", - "180days": "六个月前的日志 (180 天)" - }, - "rangeDescription": { - "7days": "一周前", - "30days": "一个月前", - "90days": "三个月前", - "180days": "六个月前", - "default": "{days} 天前" - }, - "willClean": "将清理 {range} 的所有日志记录", - "button": "清理日志", - "confirmTitle": "确认清理日志", - "confirmWarning": "此操作将永久删除 {range}的所有日志记录,且无法恢复。", - "previewLoading": "正在统计...", - "previewCount": "将删除 {count} 条日志记录", - "previewError": "无法获取预览信息", - "statisticsRetained": "✓ 统计数据将被保留(用于趋势分析)", - "logsDeleted": "✗ 日志详情将被删除(请求/响应内容、错误信息等)", - "backupRecommendation": "建议:在清理前先导出数据库备份,以防需要恢复数据。", - "cancel": "取消", - "confirm": "确认清理", - "cleaning": "正在清理...", - "successMessage": "成功清理 {count} 条日志记录({batches} 批次,耗时 {duration}s)", - "failed": "清理失败", - "error": "清理日志失败", - "descriptionWarning": "清理历史日志数据以释放数据库存储空间。注意:统计数据将被保留,但日志详情将被永久删除。" - }, - "export": { - "button": "导出数据库", - "exporting": "正在导出...", - "successMessage": "数据库导出成功!", - "failed": "导出失败", - "error": "导出数据库失败", - "descriptionFull": "导出完整的数据库备份文件(.dump 格式),可用于数据迁移或恢复。备份文件使用 PostgreSQL custom format,自动压缩且兼容不同版本的数据库结构。" - }, - "import": { - "selectFileLabel": "选择备份文件", - "fileSelected": "已选择: {name} ({size} MB)", - "fileError": "请选择 .dump 格式的备份文件", - "noFileSelected": "请先选择备份文件", - "cleanFirstLabel": "清除现有数据(覆盖模式)", - "cleanFirstDescription": "导入前删除所有现有数据,确保数据库与备份文件完全一致。如果不勾选,将尝试合并数据,但可能因主键冲突而失败。", - "button": "导入数据库", - "importing": "正在导入...", - "progressTitle": "导入进度", - "confirmTitle": "确认导入数据库", - "confirmOverwrite": "您选择了「覆盖模式」,这将会删除所有现有数据后导入备份。", - "confirmMerge": "您选择了「合并模式」,这将尝试在保留现有数据的基础上导入备份。", - "warningOverwrite": "⚠️ 警告:此操作不可逆,所有当前数据将被永久删除!", - "warningMerge": "⚠️ 注意:如果存在主键冲突,导入可能会失败。", - "backupFile": "备份文件:", - "backupRecommendation": "建议在执行此操作前,先导出当前数据库作为备份。", - "cancel": "取消", - "confirm": "确认导入", - "successMessage": "数据导入完成!", - "successCleanModeDesc": "所有数据已成功恢复。如果页面显示异常,请刷新浏览器。", - "successMergeModeDesc": "数据已成功导入并合并。如果页面显示异常,请刷新浏览器。", - "successWithWarnings": "数据导入完成(有警告)", - "successWithWarningsDesc": "数据已成功导入,但跳过了一些已存在的对象。如果页面显示异常,请刷新浏览器或重启应用。", - "failedMessage": "数据导入失败", - "error": "导入数据库失败", - "streamError": "无法读取响应流", - "streamInterrupted": "数据流意外中断", - "streamInterruptedDesc": "导入进度未正常完成,请检查日志并验证数据完整性。如有问题,请重新导入。", - "parseError": "解析响应数据失败", - "errorUnknown": "未知错误", - "descriptionFull": "从备份文件恢复数据库。支持 PostgreSQL custom format (.dump) 格式的备份文件。" - }, - "guide": { - "title": "使用说明与注意事项", - "items": { - "cleanup": { - "title": "日志清理", - "description": "物理删除历史日志数据,不可恢复。统计数据(statistics 表)将被保留。建议清理前先导出数据库备份。" - }, - "format": { - "title": "备份格式", - "description": "使用 PostgreSQL custom format (.dump),自动压缩且能够兼容不同版本的数据库结构。" - }, - "overwrite": { - "title": "覆盖模式", - "description": "导入前会删除所有现有数据,确保数据库与备份文件完全一致。适合完整恢复场景。" - }, - "merge": { - "title": "合并模式", - "description": "保留现有数据,尝试插入备份中的数据。如果存在主键冲突可能导致导入失败。" - }, - "safety": { - "title": "安全建议", - "description": "在执行导入操作前,建议先导出当前数据库作为备份,避免数据丢失。" - }, - "environment": { - "title": "环境要求", - "description": "此功能需要 Docker Compose 部署环境。本地开发环境可能无法使用。" - } - } - }, - "section": { - "status": { - "title": "数据库状态", - "description": "查看当前数据库的连接状态和基本信息。" - }, - "cleanup": { - "title": "日志清理", - "description": "清理历史日志数据以释放数据库存储空间。注意:统计数据将被保留,但日志详情将被永久删除。" - }, - "export": { - "title": "数据导出", - "description": "导出完整的数据库备份文件(.dump 格式),可用于数据迁移或恢复。" - }, - "import": { - "title": "数据导入", - "description": "从备份文件恢复数据库。支持 PostgreSQL custom format (.dump) 格式的备份文件。" - } - } - }, - "clientVersions": { - "title": "客户端升级提醒", - "description": "管理客户端版本要求,确保用户使用最新的稳定版本。VSCode 插件和 CLI 作为独立客户端分别管理版本。", - "section": { - "settings": { - "title": "升级提醒设置", - "description": "启用后,系统将自动检测客户端版本并拦截旧版本用户的请求。" - }, - "distribution": { - "title": "客户端版本分布", - "description": "显示过去 7 天内活跃用户的客户端版本信息。每种客户端类型独立统计 GA 版本。" - } - }, - "empty": { - "title": "暂无客户端数据", - "description": "过去 7 天内没有活跃用户使用可识别的客户端" - }, - "toggle": { - "enable": "启用升级提醒", - "description": "启用后,系统将拦截使用旧版本客户端的请求", - "enableSuccess": "已启用客户端版本检查", - "disableSuccess": "已关闭客户端版本检查", - "toggleFailed": "更新失败" - }, - "features": { - "title": "功能说明", - "whatHappens": "启用后会发生什么:", - "autoDetect": "系统会自动检测每种客户端的最新稳定版本(GA 版本)", - "gaRule": "判定规则:", - "gaRuleDesc": "当某个版本被 1 个以上用户使用时,视为 GA 版本", - "activeWindow": "活跃窗口:", - "activeWindowDesc": "仅统计过去 7 天内有请求的用户", - "blockOldVersion": "使用旧版本的用户将收到 HTTP 400 错误,无法继续使用服务", - "errorMessage": "错误提示中包含当前版本和需要升级的版本号", - "recommendation": "推荐做法:", - "recommendationDesc": "先观察下方的版本分布,确认新版本稳定后再启用。" - }, - "table": { - "internalType": "内部类型:", - "currentGA": "当前 GA 版本:", - "usersCount": "{count} 位用户", - "user": "用户", - "version": "当前版本", - "lastActive": "最后活跃时间", - "status": "状态", - "noUsers": "暂无用户数据", - "latest": "最新", - "needsUpgrade": "需升级", - "unknown": "未知" - } - }, - "notifications": { - "title": "消息推送", - "description": "配置 Webhook 消息推送", - "global": { - "title": "通知总开关", - "description": "启用或禁用所有消息推送功能", - "enable": "启用消息推送", - "legacyModeTitle": "兼容模式", - "legacyModeDescription": "当前使用旧版单 URL 推送配置。创建推送目标后将自动切换到多目标模式。" - }, - "targets": { - "title": "推送目标", - "description": "管理推送目标,支持企业微信、飞书、钉钉、Telegram、自定义 Webhook", - "add": "添加目标", - "update": "保存目标", - "edit": "编辑", - "delete": "删除", - "deleteConfirmTitle": "删除推送目标", - "deleteConfirm": "确定要删除该目标吗?相关绑定也会被移除。", - "enable": "启用目标", - "statusEnabled": "已启用", - "statusDisabled": "已禁用", - "lastTestAt": "上次测试", - "lastTestNever": "从未测试", - "lastTestSuccess": "测试成功", - "lastTestFailed": "测试失败", - "test": "测试", - "testSelectType": "选择测试类型", - "emptyHint": "暂无推送目标,点击“添加目标”创建。", - "created": "目标已创建", - "updated": "目标已更新", - "deleted": "目标已删除", - "bindingsSaved": "绑定已保存" - }, - "targetDialog": { - "createTitle": "添加推送目标", - "editTitle": "编辑推送目标", - "name": "目标名称", - "namePlaceholder": "例如:运维群", - "type": "平台类型", - "selectType": "请选择平台类型", - "enable": "启用", - "webhookUrl": "Webhook URL", - "webhookUrlPlaceholder": "https://example.com/webhook", - "telegramBotToken": "Telegram Bot Token", - "telegramBotTokenPlaceholder": "例如:123456:ABCDEF...", - "telegramChatId": "Telegram Chat ID", - "telegramChatIdPlaceholder": "例如:-1001234567890", - "dingtalkSecret": "钉钉签名密钥", - "dingtalkSecretPlaceholder": "可选,用于签名", - "customHeaders": "自定义 Headers(JSON)", - "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", - "errors": { - "headersInvalidJson": "Headers 不是有效 JSON", - "headersMustBeObject": "Headers 必须是 JSON 对象", - "headersValueMustBeString": "Headers 的值必须为字符串" - }, - "types": { - "wechat": "企业微信", - "feishu": "飞书", - "dingtalk": "钉钉", - "telegram": "Telegram", - "custom": "自定义 Webhook" - }, - "proxy": { - "title": "代理设置", - "toggle": "展开/收起代理设置", - "url": "代理地址", - "urlPlaceholder": "http://127.0.0.1:7890", - "fallbackToDirect": "代理失败时回退直连" - } - }, - "bindings": { - "title": "绑定", - "noTargets": "暂无可用推送目标", - "bindTarget": "绑定目标", - "enable": "启用", - "enableType": "启用该通知", - "advanced": "高级", - "scheduleCron": "Cron 表达式", - "scheduleCronPlaceholder": "例如:0 9 * * *", - "scheduleTimezone": "时区", - "templateOverride": "模板覆盖", - "editTemplateOverride": "编辑覆盖", - "templateOverrideTitle": "编辑模板覆盖", - "boundCount": "已绑定:{count}", - "enabledCount": "已启用:{count}" - }, - "templateEditor": { - "title": "模板(JSON)", - "placeholder": "请输入 JSON 模板...", - "jsonInvalid": "JSON 格式不正确", - "placeholders": "占位符", - "insert": "插入" - }, - "circuitBreaker": { - "title": "熔断器告警", - "description": "供应商完全熔断时立即推送告警消息", - "enable": "启用熔断器告警", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "test": "测试连接" - }, - "dailyLeaderboard": { - "title": "每日用户消费排行榜", - "description": "每天定时发送用户消费 Top N 排行榜", - "enable": "启用每日排行榜", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "time": "发送时间", - "timePlaceholder": "09:00", - "timeError": "时间格式错误,应为 HH:mm", - "topN": "显示前 N 名", - "test": "测试连接" - }, - "costAlert": { - "title": "成本预警", - "description": "检测用户/供应商消费超过配额阈值时触发告警", - "enable": "启用成本预警", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "webhookTypeWeCom": "企业微信", - "webhookTypeFeishu": "飞书", - "webhookTypeUnknown": "未知平台,请使用企业微信或飞书的 Webhook URL", - "threshold": "预警阈值", - "thresholdLabel": "预警阈值: {percent}%", - "thresholdHelp": "当消费达到配额的 {percent}% 时触发告警", - "interval": "检查间隔(分钟)", - "test": "测试连接" - }, - "form": { - "save": "保存设置", - "saving": "保存中...", - "loading": "加载中...", - "success": "通知设置已保存并重新调度任务", - "saveFailed": "保存失败", - "saveError": "保存设置失败", - "loadError": "加载通知设置失败", - "webhookRequired": "请先填写 Webhook URL", - "testSuccess": "测试消息已发送", - "testFailed": "测试失败", - "testFailedRetry": "测试失败,请重试", - "testError": "测试连接失败", - "testNoResult": "测试成功但未返回结果" - } - }, - "errors": { - "saveSuccess": "保存成功", - "saveFailed": "保存失败", - "saveFailed_error": "保存设置失败", - "addSuccess": "添加成功", - "addFailed": "添加服务商失败", - "editSuccess": "更新成功", - "editFailed": "更新服务商失败", - "deleteSuccess": "删除成功", - "deleteFailed": "删除供应商失败", - "syncSuccess": "同步成功", - "syncFailed": "同步失败", - "testFailed": "测试失败", - "testFailedRetry": "测试失败,请重试", - "loadFailed": "加载通知设置失败", - "unknownError": "操作过程中出现异常" - }, - "errorRules": { - "nav": "错误规则", - "title": "错误规则管理", - "description": "管理不需要自动重试的客户端错误规则。配置后,命中规则的报错将直接返回给用户,不会触发重试,也不会计入供应商熔断。", - "section": { - "title": "错误规则列表" - }, - "tester": { - "title": "错误规则测试", - "description": "输入错误消息,检查是否命中已配置的规则以及最终返回给用户的内容。", - "inputLabel": "测试错误消息", - "inputPlaceholder": "输入要检测的错误消息...", - "testButton": "开始测试", - "testing": "测试中...", - "matched": "已命中错误规则", - "notMatched": "未命中任何规则", - "finalResponse": "覆写响应", - "ruleInfo": "匹配的规则", - "noRule": "未匹配到任何规则", - "category": "规则类别", - "pattern": "匹配模式", - "matchType": "匹配类型", - "overrideStatusCode": "覆写状态码", - "testFailed": "测试失败,请稍后重试", - "messageRequired": "请输入要测试的错误消息", - "warnings": "配置警告", - "statusCodeOnlyOverride": "仅覆写状态码,响应体将使用上游错误消息" - }, - "add": "添加错误规则", - "addSuccess": "错误规则创建成功", - "addFailed": "创建错误规则失败", - "edit": "编辑错误规则", - "editSuccess": "错误规则更新成功", - "editFailed": "更新错误规则失败", - "delete": "删除错误规则", - "deleteSuccess": "错误规则删除成功", - "deleteFailed": "删除失败", - "enable": "错误规则已启用", - "disable": "错误规则已禁用", - "toggleFailed": "状态切换失败", - "toggleFailedError": "状态切换失败:", - "refreshCache": "同步规则", - "refreshCacheSuccess": "规则同步成功,已加载 {count} 个错误规则", - "refreshCacheFailed": "同步规则失败", - "cacheStats": "缓存: 共 {totalCount} 条规则", - "emptyState": "暂无错误规则,点击右上角\"添加错误规则\"开始配置。", - "confirmDelete": "确定要删除错误规则\"{pattern}\"吗?", - "dialog": { - "addTitle": "添加错误规则", - "addDescription": "配置错误消息正则模式,匹配的错误将被识别为不可重试的客户端错误。", - "editTitle": "编辑错误规则", - "editDescription": "修改错误规则配置,更改后将自动刷新缓存。", - "patternLabel": "正则模式 *", - "patternPlaceholder": "输入正则表达式...", - "patternRequired": "请输入正则模式", - "patternHint": "支持 JavaScript 正则表达式语法,例如:prompt is too long|invalid.*request", - "categoryLabel": "规则类别 *", - "categoryPlaceholder": "选择规则类别", - "categoryRequired": "请选择规则类别", - "categoryHint": "选择规则所属的错误类别,用于分类管理和统计", - "descriptionLabel": "说明", - "descriptionPlaceholder": "可选:添加说明...", - "invalidRegex": "正则表达式语法错误", - "regexTester": "正则测试器", - "testMessageLabel": "测试消息", - "testMessagePlaceholder": "输入要测试的错误消息...", - "matchSuccess": "匹配成功", - "matchFailed": "未匹配", - "invalidPattern": "正则无效", - "matchedText": "匹配内容", - "defaultRuleHint": "默认规则的模式不可修改", - "enableOverride": "启用错误覆写", - "enableOverrideHint": "启用后可自定义返回给客户端的错误响应和状态码,原始错误仍会记录到数据库。当前仅支持 Claude API 错误格式。", - "overrideResponseLabel": "覆写响应(JSON 格式)", - "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"您的自定义消息\"\n }\n}", - "overrideResponseHint": "留空则仅覆写状态码。", - "overrideStatusCodeLabel": "覆写状态码(可选)", - "overrideStatusCodePlaceholder": "例如 400", - "overrideStatusCodeHint": "留空则使用上游状态码。范围:400-599。", - "useTemplate": "Claude Error 模板", - "useTemplateConfirm": "输入框已有内容,确定覆盖为模板示例?", - "validJson": "JSON 格式正确", - "invalidJson": "JSON 格式无效", - "invalidStatusCode": "状态码必须在 400-599 范围内", - "creating": "创建中...", - "saving": "保存中..." - }, - "table": { - "pattern": "正则模式", - "category": "规则类别", - "description": "说明", - "status": "状态", - "default": "默认", - "isEnabled": "启用状态", - "isDefault": "默认规则", - "createdAt": "创建时间", - "actions": "操作" - }, - "form": { - "fields": { - "pattern": "正则模式", - "category": "规则类别", - "description": "说明" - }, - "placeholders": { - "pattern": "例如: prompt is too long", - "category": "选择类别", - "description": "可选:添加说明..." - }, - "labels": { - "pattern": "正则模式 *", - "category": "规则类别 *", - "description": "说明", - "isEnabled": "启用状态" - } - }, - "actions": { - "add": "添加", - "edit": "编辑", - "delete": "删除", - "refresh": "刷新", - "test": "测试", - "messages": { - "success": "操作成功", - "error": "操作失败" - } - }, - "validation": { - "patternRequired": "请输入正则模式", - "categoryRequired": "请选择规则类别", - "patternInvalid": "正则表达式语法错误", - "redosRisk": "正则表达式存在 ReDoS 风险,请简化模式", - "patternTooComplex": "正则表达式过于复杂" - }, - "categories": { - "prompt_limit": "Prompt 长度限制", - "content_filter": "内容过滤", - "pdf_limit": "PDF 页数限制", - "thinking_error": "Thinking 格式错误", - "parameter_error": "参数验证失败", - "invalid_request": "非法请求", - "cache_limit": "缓存控制限制" - }, - "regexTester": { - "title": "正则测试器", - "testMessage": "测试消息", - "testMessagePlaceholder": "输入要测试的错误消息...", - "matchResult": "匹配结果", - "matched": "匹配成功", - "notMatched": "未匹配", - "test": "测试" - }, - "defaultRules": { - "cannotDelete": "默认规则无法删除", - "cannotDisable": "建议保留默认规则启用状态" - } - }, - "mcpPassthroughConfig": "MCP 透传配置", - "mcpPassthroughConfigNone": "不启用", - "mcpPassthroughConfigMinimax": "Minimax", - "mcpPassthroughConfigGlm": "智谱 GLM", - "mcpPassthroughConfigCustom": "自定义 (预留)", - "mcpPassthroughDesc": "启用后,将 MCP 工具调用透传到指定的 AI 服务商(如 minimax 的图片识别、联网搜索)", - "mcpPassthroughSelect": "透传类型", - "mcpPassthroughNoneLabel": "不启用", - "mcpPassthroughNoneDesc": "不启用 MCP 透传功能(默认)", - "mcpPassthroughMinimaxLabel": "Minimax", - "mcpPassthroughMinimaxDesc": "透传到 minimax MCP 服务(支持图片识别、联网搜索等工具)", - "mcpPassthroughGlmLabel": "智谱 GLM", - "mcpPassthroughGlmDesc": "透传到智谱 GLM MCP 服务(支持图片分析、视频分析等工具)", - "mcpPassthroughCustomLabel": "自定义", - "mcpPassthroughCustomDesc": "透传到自定义 MCP 服务(预留,暂未实现)", - "mcpPassthroughHint": "提示: MCP 透传功能允许 Claude Code 客户端使用第三方 AI 服务商提供的工具能力(如图片识别、联网搜索)", - "mcpPassthroughUrlLabel": "MCP 透传 URL", - "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com", - "mcpPassthroughUrlDesc": "MCP 服务的基础 URL。留空则自动从提供商 URL 提取基础域名", - "mcpPassthroughUrlAuto": "自动提取: {url}" -} diff --git a/messages/zh-CN/settings/clientVersions.json b/messages/zh-CN/settings/clientVersions.json new file mode 100644 index 000000000..b13c0e3cb --- /dev/null +++ b/messages/zh-CN/settings/clientVersions.json @@ -0,0 +1,51 @@ +{ + "title": "客户端升级提醒", + "description": "管理客户端版本要求,确保用户使用最新的稳定版本。VSCode 插件和 CLI 作为独立客户端分别管理版本。", + "section": { + "settings": { + "title": "升级提醒设置", + "description": "启用后,系统将自动检测客户端版本并拦截旧版本用户的请求。" + }, + "distribution": { + "title": "客户端版本分布", + "description": "显示过去 7 天内活跃用户的客户端版本信息。每种客户端类型独立统计 GA 版本。" + } + }, + "empty": { + "title": "暂无客户端数据", + "description": "过去 7 天内没有活跃用户使用可识别的客户端" + }, + "toggle": { + "enable": "启用升级提醒", + "description": "启用后,系统将拦截使用旧版本客户端的请求", + "enableSuccess": "已启用客户端版本检查", + "disableSuccess": "已关闭客户端版本检查", + "toggleFailed": "更新失败" + }, + "features": { + "title": "功能说明", + "whatHappens": "启用后会发生什么:", + "autoDetect": "系统会自动检测每种客户端的最新稳定版本(GA 版本)", + "gaRule": "判定规则:", + "gaRuleDesc": "当某个版本被 1 个以上用户使用时,视为 GA 版本", + "activeWindow": "活跃窗口:", + "activeWindowDesc": "仅统计过去 7 天内有请求的用户", + "blockOldVersion": "使用旧版本的用户将收到 HTTP 400 错误,无法继续使用服务", + "errorMessage": "错误提示中包含当前版本和需要升级的版本号", + "recommendation": "推荐做法:", + "recommendationDesc": "先观察下方的版本分布,确认新版本稳定后再启用。" + }, + "table": { + "internalType": "内部类型:", + "currentGA": "当前 GA 版本:", + "usersCount": "{count} 位用户", + "user": "用户", + "version": "当前版本", + "lastActive": "最后活跃时间", + "status": "状态", + "noUsers": "暂无用户数据", + "latest": "最新", + "needsUpgrade": "需升级", + "unknown": "未知" + } +} diff --git a/messages/zh-CN/settings/common.json b/messages/zh-CN/settings/common.json new file mode 100644 index 000000000..fb31b8dc0 --- /dev/null +++ b/messages/zh-CN/settings/common.json @@ -0,0 +1,31 @@ +{ + "save": "保存", + "saving": "保存中...", + "create": "创建", + "creating": "创建中...", + "edit": "编辑", + "delete": "删除", + "update": "更新", + "updating": "更新中...", + "test": "测试", + "testing": "测试中...", + "confirm": "确认", + "cancel": "取消", + "submit": "提交", + "reset": "重置", + "refresh": "刷新", + "loading": "加载中...", + "completed": "完成", + "unlimited": "无限", + "unlimited_desc": "无限制", + "none": "无(暂无用户使用该版本)", + "enabled": "启用", + "disabled": "禁用", + "empty": "未找到匹配的结果", + "error": "未知错误", + "success": "成功", + "failed": "失败", + "copy": "复制", + "copied": "密钥已复制到剪贴板", + "copyFailed": "复制失败" +} diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json new file mode 100644 index 000000000..f5605ed00 --- /dev/null +++ b/messages/zh-CN/settings/config.json @@ -0,0 +1,89 @@ +{ + "title": "基础配置", + "description": "管理系统的基础参数,影响站点显示和统计行为。", + "siteSettings": "站点参数", + "siteSettingsDesc": "配置站点标题、货币显示单位与仪表盘统计展示策略。", + "autoCleanup": "自动日志清理", + "autoCleanupDesc": "定时自动清理历史日志数据,释放数据库存储空间。", + "section": { + "siteParams": { + "title": "站点参数", + "description": "配置站点标题、货币显示单位与仪表盘统计展示策略。" + }, + "autoCleanup": { + "title": "自动日志清理", + "description": "定时自动清理历史日志数据,释放数据库存储空间。" + } + }, + "form": { + "siteTitle": "站点标题", + "siteTitlePlaceholder": "例如:Claude Code Hub", + "siteTitleRequired": "站点标题不能为空", + "siteTitleDesc": "用于设置浏览器标签页标题以及系统默认显示名称。", + "currencyDisplay": "货币显示单位", + "currencyDisplayPlaceholder": "选择货币单位", + "currencyDisplayDesc": "修改后,系统所有页面和 API 接口的金额显示将使用对应的货币符号(仅修改符号,不进行汇率转换)。", + "billingModelSource": "计费模型来源", + "billingModelSourcePlaceholder": "选择计费模型来源", + "billingModelSourceDesc": "配置模型重定向时使用哪个模型进行计费。重定向前使用用户请求的原始模型计费,重定向后使用实际调用的模型计费。", + "billingModelSourceOptions": { + "original": "重定向前(原始模型)", + "redirected": "重定向后(实际模型)" + }, + "allowGlobalView": "允许查看全站使用量", + "allowGlobalViewDesc": "关闭后,普通用户在仪表盘仅能查看自己密钥的使用统计。", + "verboseProviderError": "详细供应商错误信息", + "verboseProviderErrorDesc": "开启后,当所有供应商不可用时返回详细错误信息(包含供应商数量、限流原因等);关闭后仅返回简洁错误码。", + "enableHttp2": "启用 HTTP/2", + "enableHttp2Desc": "启用后,代理请求将优先使用 HTTP/2 协议。如果 HTTP/2 失败,将自动降级到 HTTP/1.1。", + "interceptAnthropicWarmupRequests": "拦截 Warmup 请求(Anthropic)", + "interceptAnthropicWarmupRequestsDesc": "开启后,识别到 Claude Code 的 Warmup 探测请求将由 CCH 直接抢答短响应,避免访问上游供应商;该请求会记录在日志中,但不计费、不限流、不计入统计。", + "enableThinkingSignatureRectifier": "启用 thinking 签名整流器", + "enableThinkingSignatureRectifierDesc": "当 Anthropic 类型供应商返回 thinking 签名不兼容或非法请求等错误时,自动移除不兼容的 thinking 相关块并对同一供应商重试一次(默认开启)。", + "enableResponseFixer": "启用响应整流", + "enableResponseFixerDesc": "自动修复上游响应中常见的编码、SSE 与 JSON 格式问题(默认开启)。", + "responseFixerFixEncoding": "修复编码问题", + "responseFixerFixEncodingDesc": "移除 BOM 与空字节,并对无效 UTF-8 做兼容处理。", + "responseFixerFixSseFormat": "修复 SSE 格式", + "responseFixerFixSseFormatDesc": "补齐 data: 前缀、统一换行符,并修复常见字段格式。", + "responseFixerFixTruncatedJson": "修复截断的 JSON", + "responseFixerFixTruncatedJsonDesc": "补齐未闭合的括号/引号,移除尾随逗号,必要时补 null。", + "saveSettings": "保存设置", + "keepDays": "保留天数", + "keepDaysDesc": "清理超过此天数的历史日志", + "cleanupSchedule": "清理周期", + "cleanupScheduleDesc": "选择自动清理的执行周期", + "saveSuccess": "保存成功", + "saveFailed": "保存失败", + "saveError": "保存失败", + "configUpdated": "系统设置已更新,页面将刷新以应用货币显示变更", + "enableAutoCleanup": "启用自动清理", + "enableAutoCleanupDesc": "定时自动清理历史日志数据", + "cleanupRetentionDays": "保留天数", + "cleanupRetentionDaysRequired": "保留天数 *", + "cleanupRetentionDaysPlaceholder": "30", + "cleanupRetentionDaysDesc": "超过此天数的日志将被自动清理(范围:1-365 天)", + "cleanupScheduleLabel": "执行时间 (Cron)", + "cleanupScheduleRequired": "执行时间 (Cron) *", + "cleanupSchedulePlaceholder": "0 2 * * *", + "cleanupScheduleCronDesc": "Cron 表达式,默认:0 2 * * *(每天凌晨 2 点)", + "cleanupScheduleCronExample": "示例:0 3 * * 0(每周日凌晨 3 点)", + "cleanupBatchSize": "批量大小", + "cleanupBatchSizeRequired": "批量大小 *", + "cleanupBatchSizePlaceholder": "10000", + "cleanupBatchSizeDesc": "每批删除的记录数(范围:1000-100000,推荐 10000)", + "saveConfig": "保存配置", + "autoCleanupSaved": "自动清理配置已保存", + "currencies": { + "USD": "$ 美元 (USD)", + "CNY": "¥ 人民币 (CNY)", + "EUR": "€ 欧元 (EUR)", + "JPY": "¥ 日元 (JPY)", + "GBP": "£ 英镑 (GBP)", + "HKD": "HK$ 港币 (HKD)", + "TWD": "NT$ 新台币 (TWD)", + "KRW": "₩ 韩元 (KRW)", + "SGD": "S$ 新加坡元 (SGD)" + } + } +} diff --git a/messages/zh-CN/settings/data.json b/messages/zh-CN/settings/data.json new file mode 100644 index 000000000..147ce7927 --- /dev/null +++ b/messages/zh-CN/settings/data.json @@ -0,0 +1,133 @@ +{ + "title": "数据管理", + "description": "管理数据库的备份与恢复,支持完整数据导入导出和日志清理。", + "status": { + "loading": "加载中...", + "error": "获取数据库状态失败", + "retry": "重试", + "connected": "数据库连接正常", + "unavailable": "数据库不可用", + "tables": "{count} 个表" + }, + "cleanup": { + "rangeLabel": "清理范围", + "range": { + "7days": "一周前的日志 (7 天)", + "30days": "一个月前的日志 (30 天)", + "90days": "三个月前的日志 (90 天)", + "180days": "六个月前的日志 (180 天)" + }, + "rangeDescription": { + "7days": "一周前", + "30days": "一个月前", + "90days": "三个月前", + "180days": "六个月前", + "default": "{days} 天前" + }, + "willClean": "将清理 {range} 的所有日志记录", + "button": "清理日志", + "confirmTitle": "确认清理日志", + "confirmWarning": "此操作将永久删除 {range}的所有日志记录,且无法恢复。", + "previewLoading": "正在统计...", + "previewCount": "将删除 {count} 条日志记录", + "previewError": "无法获取预览信息", + "statisticsRetained": "✓ 统计数据将被保留(用于趋势分析)", + "logsDeleted": "✗ 日志详情将被删除(请求/响应内容、错误信息等)", + "backupRecommendation": "建议:在清理前先导出数据库备份,以防需要恢复数据。", + "cancel": "取消", + "confirm": "确认清理", + "cleaning": "正在清理...", + "successMessage": "成功清理 {count} 条日志记录({batches} 批次,耗时 {duration}s)", + "failed": "清理失败", + "error": "清理日志失败", + "descriptionWarning": "清理历史日志数据以释放数据库存储空间。注意:统计数据将被保留,但日志详情将被永久删除。" + }, + "export": { + "button": "导出数据库", + "exporting": "正在导出...", + "successMessage": "数据库导出成功!", + "failed": "导出失败", + "error": "导出数据库失败", + "descriptionFull": "导出完整的数据库备份文件(.dump 格式),可用于数据迁移或恢复。备份文件使用 PostgreSQL custom format,自动压缩且兼容不同版本的数据库结构。" + }, + "import": { + "selectFileLabel": "选择备份文件", + "fileSelected": "已选择: {name} ({size} MB)", + "fileError": "请选择 .dump 格式的备份文件", + "noFileSelected": "请先选择备份文件", + "cleanFirstLabel": "清除现有数据(覆盖模式)", + "cleanFirstDescription": "导入前删除所有现有数据,确保数据库与备份文件完全一致。如果不勾选,将尝试合并数据,但可能因主键冲突而失败。", + "button": "导入数据库", + "importing": "正在导入...", + "progressTitle": "导入进度", + "confirmTitle": "确认导入数据库", + "confirmOverwrite": "您选择了「覆盖模式」,这将会删除所有现有数据后导入备份。", + "confirmMerge": "您选择了「合并模式」,这将尝试在保留现有数据的基础上导入备份。", + "warningOverwrite": "警告:此操作不可逆,所有当前数据将被永久删除!", + "warningMerge": "注意:如果存在主键冲突,导入可能会失败。", + "backupFile": "备份文件:", + "backupRecommendation": "建议在执行此操作前,先导出当前数据库作为备份。", + "cancel": "取消", + "confirm": "确认导入", + "successMessage": "数据导入完成!", + "successCleanModeDesc": "所有数据已成功恢复。如果页面显示异常,请刷新浏览器。", + "successMergeModeDesc": "数据已成功导入并合并。如果页面显示异常,请刷新浏览器。", + "successWithWarnings": "数据导入完成(有警告)", + "successWithWarningsDesc": "数据已成功导入,但跳过了一些已存在的对象。如果页面显示异常,请刷新浏览器或重启应用。", + "failedMessage": "数据导入失败", + "error": "导入数据库失败", + "streamError": "无法读取响应流", + "streamInterrupted": "数据流意外中断", + "streamInterruptedDesc": "导入进度未正常完成,请检查日志并验证数据完整性。如有问题,请重新导入。", + "parseError": "解析响应数据失败", + "errorUnknown": "未知错误", + "descriptionFull": "从备份文件恢复数据库。支持 PostgreSQL custom format (.dump) 格式的备份文件。" + }, + "guide": { + "title": "使用说明与注意事项", + "items": { + "cleanup": { + "title": "日志清理", + "description": "物理删除历史日志数据,不可恢复。统计数据(statistics 表)将被保留。建议清理前先导出数据库备份。" + }, + "format": { + "title": "备份格式", + "description": "使用 PostgreSQL custom format (.dump),自动压缩且能够兼容不同版本的数据库结构。" + }, + "overwrite": { + "title": "覆盖模式", + "description": "导入前会删除所有现有数据,确保数据库与备份文件完全一致。适合完整恢复场景。" + }, + "merge": { + "title": "合并模式", + "description": "保留现有数据,尝试插入备份中的数据。如果存在主键冲突可能导致导入失败。" + }, + "safety": { + "title": "安全建议", + "description": "在执行导入操作前,建议先导出当前数据库作为备份,避免数据丢失。" + }, + "environment": { + "title": "环境要求", + "description": "此功能需要 Docker Compose 部署环境。本地开发环境可能无法使用。" + } + } + }, + "section": { + "status": { + "title": "数据库状态", + "description": "查看当前数据库的连接状态和基本信息。" + }, + "cleanup": { + "title": "日志清理", + "description": "清理历史日志数据以释放数据库存储空间。注意:统计数据将被保留,但日志详情将被永久删除。" + }, + "export": { + "title": "数据导出", + "description": "导出完整的数据库备份文件(.dump 格式),可用于数据迁移或恢复。" + }, + "import": { + "title": "数据导入", + "description": "从备份文件恢复数据库。支持 PostgreSQL custom format (.dump) 格式的备份文件。" + } + } +} diff --git a/messages/zh-CN/settings/errorRules.json b/messages/zh-CN/settings/errorRules.json new file mode 100644 index 000000000..63dd41f74 --- /dev/null +++ b/messages/zh-CN/settings/errorRules.json @@ -0,0 +1,157 @@ +{ + "nav": "错误规则", + "title": "错误规则管理", + "description": "管理不需要自动重试的客户端错误规则。配置后,命中规则的报错将直接返回给用户,不会触发重试,也不会计入供应商熔断。", + "section": { + "title": "错误规则列表" + }, + "tester": { + "title": "错误规则测试", + "description": "输入错误消息,检查是否命中已配置的规则以及最终返回给用户的内容。", + "inputLabel": "测试错误消息", + "inputPlaceholder": "输入要检测的错误消息...", + "testButton": "开始测试", + "testing": "测试中...", + "matched": "已命中错误规则", + "notMatched": "未命中任何规则", + "finalResponse": "覆写响应", + "ruleInfo": "匹配的规则", + "noRule": "未匹配到任何规则", + "category": "规则类别", + "pattern": "匹配模式", + "matchType": "匹配类型", + "overrideStatusCode": "覆写状态码", + "testFailed": "测试失败,请稍后重试", + "messageRequired": "请输入要测试的错误消息", + "warnings": "配置警告", + "statusCodeOnlyOverride": "仅覆写状态码,响应体将使用上游错误消息" + }, + "add": "添加错误规则", + "addSuccess": "错误规则创建成功", + "addFailed": "创建错误规则失败", + "edit": "编辑错误规则", + "editSuccess": "错误规则更新成功", + "editFailed": "更新错误规则失败", + "delete": "删除错误规则", + "deleteSuccess": "错误规则删除成功", + "deleteFailed": "删除失败", + "enable": "错误规则已启用", + "disable": "错误规则已禁用", + "toggleFailed": "状态切换失败", + "toggleFailedError": "状态切换失败:", + "refreshCache": "同步规则", + "refreshCacheSuccess": "规则同步成功,已加载 {count} 个错误规则", + "refreshCacheFailed": "同步规则失败", + "cacheStats": "缓存: 共 {totalCount} 条规则", + "emptyState": "暂无错误规则,点击右上角\"添加错误规则\"开始配置。", + "confirmDelete": "确定要删除错误规则\"{pattern}\"吗?", + "dialog": { + "addTitle": "添加错误规则", + "addDescription": "配置错误消息正则模式,匹配的错误将被识别为不可重试的客户端错误。", + "editTitle": "编辑错误规则", + "editDescription": "修改错误规则配置,更改后将自动刷新缓存。", + "patternLabel": "正则模式 *", + "patternPlaceholder": "输入正则表达式...", + "patternRequired": "请输入正则模式", + "patternHint": "支持 JavaScript 正则表达式语法,例如:prompt is too long|invalid.*request", + "categoryLabel": "规则类别 *", + "categoryPlaceholder": "选择规则类别", + "categoryRequired": "请选择规则类别", + "categoryHint": "选择规则所属的错误类别,用于分类管理和统计", + "descriptionLabel": "说明", + "descriptionPlaceholder": "可选:添加说明...", + "invalidRegex": "正则表达式语法错误", + "regexTester": "正则测试器", + "testMessageLabel": "测试消息", + "testMessagePlaceholder": "输入要测试的错误消息...", + "matchSuccess": "匹配成功", + "matchFailed": "未匹配", + "invalidPattern": "正则无效", + "matchedText": "匹配内容", + "defaultRuleHint": "默认规则的模式不可修改", + "enableOverride": "启用错误覆写", + "enableOverrideHint": "启用后可自定义返回给客户端的错误响应和状态码,原始错误仍会记录到数据库。当前仅支持 Claude API 错误格式。", + "overrideResponseLabel": "覆写响应(JSON 格式)", + "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"您的自定义消息\"\n }\n}", + "overrideResponseHint": "留空则仅覆写状态码。", + "overrideStatusCodeLabel": "覆写状态码(可选)", + "overrideStatusCodePlaceholder": "例如 400", + "overrideStatusCodeHint": "留空则使用上游状态码。范围:400-599。", + "useTemplate": "Claude Error 模板", + "useTemplateConfirm": "输入框已有内容,确定覆盖为模板示例?", + "validJson": "JSON 格式正确", + "invalidJson": "JSON 格式无效", + "invalidStatusCode": "状态码必须在 400-599 范围内", + "creating": "创建中...", + "saving": "保存中..." + }, + "table": { + "pattern": "正则模式", + "category": "规则类别", + "description": "说明", + "status": "状态", + "default": "默认", + "isEnabled": "启用状态", + "isDefault": "默认规则", + "createdAt": "创建时间", + "actions": "操作" + }, + "form": { + "fields": { + "pattern": "正则模式", + "category": "规则类别", + "description": "说明" + }, + "placeholders": { + "pattern": "例如: prompt is too long", + "category": "选择类别", + "description": "可选:添加说明..." + }, + "labels": { + "pattern": "正则模式 *", + "category": "规则类别 *", + "description": "说明", + "isEnabled": "启用状态" + } + }, + "actions": { + "add": "添加", + "edit": "编辑", + "delete": "删除", + "refresh": "刷新", + "test": "测试", + "messages": { + "success": "操作成功", + "error": "操作失败" + } + }, + "validation": { + "patternRequired": "请输入正则模式", + "categoryRequired": "请选择规则类别", + "patternInvalid": "正则表达式语法错误", + "redosRisk": "正则表达式存在 ReDoS 风险,请简化模式", + "patternTooComplex": "正则表达式过于复杂" + }, + "categories": { + "prompt_limit": "Prompt 长度限制", + "content_filter": "内容过滤", + "pdf_limit": "PDF 页数限制", + "thinking_error": "Thinking 格式错误", + "parameter_error": "参数验证失败", + "invalid_request": "非法请求", + "cache_limit": "缓存控制限制" + }, + "regexTester": { + "title": "正则测试器", + "testMessage": "测试消息", + "testMessagePlaceholder": "输入要测试的错误消息...", + "matchResult": "匹配结果", + "matched": "匹配成功", + "notMatched": "未匹配", + "test": "测试" + }, + "defaultRules": { + "cannotDelete": "默认规则无法删除", + "cannotDisable": "建议保留默认规则启用状态" + } +} diff --git a/messages/zh-CN/settings/errors.json b/messages/zh-CN/settings/errors.json new file mode 100644 index 000000000..4a5844464 --- /dev/null +++ b/messages/zh-CN/settings/errors.json @@ -0,0 +1,17 @@ +{ + "saveSuccess": "保存成功", + "saveFailed": "保存失败", + "saveFailed_error": "保存设置失败", + "addSuccess": "添加成功", + "addFailed": "添加服务商失败", + "editSuccess": "更新成功", + "editFailed": "更新服务商失败", + "deleteSuccess": "删除成功", + "deleteFailed": "删除供应商失败", + "syncSuccess": "同步成功", + "syncFailed": "同步失败", + "testFailed": "测试失败", + "testFailedRetry": "测试失败,请重试", + "loadFailed": "加载通知设置失败", + "unknownError": "操作过程中出现异常" +} diff --git a/messages/zh-CN/settings/index.ts b/messages/zh-CN/settings/index.ts new file mode 100644 index 000000000..414b5daa4 --- /dev/null +++ b/messages/zh-CN/settings/index.ts @@ -0,0 +1,101 @@ +import clientVersions from "./clientVersions.json"; +import common from "./common.json"; +import config from "./config.json"; +import data from "./data.json"; +import errorRules from "./errorRules.json"; +import errors from "./errors.json"; +import logs from "./logs.json"; +import nav from "./nav.json"; +import notifications from "./notifications.json"; +import prices from "./prices.json"; +import requestFilters from "./requestFilters.json"; +import sensitiveWords from "./sensitiveWords.json"; +import strings from "./strings.json"; + +import providersAutoSort from "./providers/autoSort.json"; +import providersFilter from "./providers/filter.json"; +import providersGuide from "./providers/guide.json"; +import providersInlineEdit from "./providers/inlineEdit.json"; +import providersList from "./providers/list.json"; +import providersSchedulingDialog from "./providers/schedulingDialog.json"; +import providersSearch from "./providers/search.json"; +import providersSection from "./providers/section.json"; +import providersSort from "./providers/sort.json"; +import providersStrings from "./providers/strings.json"; +import providersTypes from "./providers/types.json"; + +import providersFormApiTest from "./providers/form/apiTest.json"; +import providersFormButtons from "./providers/form/buttons.json"; +import providersFormCommon from "./providers/form/common.json"; +import providersFormDeleteDialog from "./providers/form/deleteDialog.json"; +import providersFormErrors from "./providers/form/errors.json"; +import providersFormFailureThresholdConfirmDialog from "./providers/form/failureThresholdConfirmDialog.json"; +import providersFormKey from "./providers/form/key.json"; +import providersFormMaxRetryAttempts from "./providers/form/maxRetryAttempts.json"; +import providersFormModelRedirect from "./providers/form/modelRedirect.json"; +import providersFormModelSelect from "./providers/form/modelSelect.json"; +import providersFormName from "./providers/form/name.json"; +import providersFormProviderTypes from "./providers/form/providerTypes.json"; +import providersFormProxyTest from "./providers/form/proxyTest.json"; +import providersFormSections from "./providers/form/sections.json"; +import providersFormStrings from "./providers/form/strings.json"; +import providersFormSuccess from "./providers/form/success.json"; +import providersFormTitle from "./providers/form/title.json"; +import providersFormUrl from "./providers/form/url.json"; +import providersFormUrlPreview from "./providers/form/urlPreview.json"; +import providersFormWebsiteUrl from "./providers/form/websiteUrl.json"; + +const providersForm = { + ...providersFormStrings, + apiTest: providersFormApiTest, + buttons: providersFormButtons, + common: providersFormCommon, + deleteDialog: providersFormDeleteDialog, + errors: providersFormErrors, + failureThresholdConfirmDialog: providersFormFailureThresholdConfirmDialog, + key: providersFormKey, + maxRetryAttempts: providersFormMaxRetryAttempts, + modelRedirect: providersFormModelRedirect, + modelSelect: providersFormModelSelect, + name: providersFormName, + providerTypes: providersFormProviderTypes, + proxyTest: providersFormProxyTest, + sections: providersFormSections, + success: providersFormSuccess, + title: providersFormTitle, + url: providersFormUrl, + urlPreview: providersFormUrlPreview, + websiteUrl: providersFormWebsiteUrl, +}; + +const providers = { + ...providersStrings, + autoSort: providersAutoSort, + filter: providersFilter, + form: providersForm, + guide: providersGuide, + inlineEdit: providersInlineEdit, + list: providersList, + schedulingDialog: providersSchedulingDialog, + search: providersSearch, + section: providersSection, + sort: providersSort, + types: providersTypes, +}; + +export default { + nav, + common, + config, + providers, + prices, + sensitiveWords, + requestFilters, + logs, + data, + clientVersions, + notifications, + errors, + errorRules, + ...strings, +}; diff --git a/messages/zh-CN/settings/logs.json b/messages/zh-CN/settings/logs.json new file mode 100644 index 000000000..31a395d1c --- /dev/null +++ b/messages/zh-CN/settings/logs.json @@ -0,0 +1,54 @@ +{ + "title": "日志管理", + "description": "动态调整系统日志级别,实时控制日志输出详细程度。", + "subtitle": "日志级别控制", + "subtitleDesc": "调整后立即生效,无需重启服务。适合生产环境排查问题时使用。", + "section": { + "title": "日志级别控制", + "description": "调整后立即生效,无需重启服务。" + }, + "levels": { + "fatal": { + "label": "Fatal", + "description": "仅致命错误" + }, + "error": { + "label": "Error", + "description": "错误信息" + }, + "warn": { + "label": "Warn", + "description": "警告 + 错误" + }, + "info": { + "label": "Info", + "description": "关键业务事件 + 警告 + 错误(推荐生产)" + }, + "debug": { + "label": "Debug", + "description": "调试信息 + 所有级别(推荐开发)" + }, + "trace": { + "label": "Trace", + "description": "极详细追踪 + 所有级别" + } + }, + "form": { + "currentLevel": "当前日志级别", + "selectLevel": "选择日志级别", + "save": "保存设置", + "saving": "保存中...", + "success": "日志级别已设置为: {level}", + "failed": "设置失败", + "failedError": "设置日志级别失败", + "fetchFailed": "获取日志级别失败", + "effectiveImmediately": "调整日志级别后立即生效,无需重启服务。", + "levelGuideTitle": "日志级别说明", + "levelGuideFatal": "Fatal/Error: 仅显示错误,日志最少,适合高负载生产环境", + "levelGuideWarn": "Warn: 包含警告(限流触发、熔断器打开等)+ 错误", + "levelGuideInfo": "Info(推荐生产): 显示关键业务事件(供应商选择、Session 复用、价格同步)+ 警告 + 错误", + "levelGuideDebug": "Debug(推荐开发): 包含详细调试信息,适合排查问题时使用", + "levelGuideTrace": "Trace: 极详细的追踪信息,包含所有细节", + "changeNotice": "当前级别为 {current},点击保存后将切换到 {selected}" + } +} diff --git a/messages/zh-CN/settings/nav.json b/messages/zh-CN/settings/nav.json new file mode 100644 index 000000000..5d125a2fb --- /dev/null +++ b/messages/zh-CN/settings/nav.json @@ -0,0 +1,15 @@ +{ + "config": "配置", + "prices": "价格表", + "providers": "供应商", + "sensitiveWords": "敏感词", + "requestFilters": "请求过滤", + "clientVersions": "客户端升级提醒", + "data": "数据管理", + "logs": "日志", + "notifications": "消息推送", + "apiDocs": "API 文档", + "errorRules": "错误规则", + "feedback": "反馈问题", + "docs": "使用文档" +} diff --git a/messages/zh-CN/settings/notifications.json b/messages/zh-CN/settings/notifications.json new file mode 100644 index 000000000..33f388fe8 --- /dev/null +++ b/messages/zh-CN/settings/notifications.json @@ -0,0 +1,146 @@ +{ + "title": "消息推送", + "description": "配置 Webhook 消息推送", + "global": { + "title": "通知总开关", + "description": "启用或禁用所有消息推送功能", + "enable": "启用消息推送", + "legacyModeTitle": "兼容模式", + "legacyModeDescription": "当前使用旧版单 URL 推送配置。创建推送目标后将自动切换到多目标模式。" + }, + "targets": { + "title": "推送目标", + "description": "管理推送目标,支持企业微信、飞书、钉钉、Telegram、自定义 Webhook", + "add": "添加目标", + "update": "保存目标", + "edit": "编辑", + "delete": "删除", + "deleteConfirmTitle": "删除推送目标", + "deleteConfirm": "确定要删除该目标吗?相关绑定也会被移除。", + "enable": "启用目标", + "statusEnabled": "已启用", + "statusDisabled": "已禁用", + "lastTestAt": "上次测试", + "lastTestNever": "从未测试", + "lastTestSuccess": "测试成功", + "lastTestFailed": "测试失败", + "test": "测试", + "testSelectType": "选择测试类型", + "emptyHint": "暂无推送目标,点击“添加目标”创建。", + "created": "目标已创建", + "updated": "目标已更新", + "deleted": "目标已删除", + "bindingsSaved": "绑定已保存" + }, + "targetDialog": { + "createTitle": "添加推送目标", + "editTitle": "编辑推送目标", + "name": "目标名称", + "namePlaceholder": "例如:运维群", + "type": "平台类型", + "selectType": "请选择平台类型", + "enable": "启用", + "webhookUrl": "Webhook URL", + "webhookUrlPlaceholder": "https://example.com/webhook", + "telegramBotToken": "Telegram Bot Token", + "telegramBotTokenPlaceholder": "例如:123456:ABCDEF...", + "telegramChatId": "Telegram Chat ID", + "telegramChatIdPlaceholder": "例如:-1001234567890", + "dingtalkSecret": "钉钉签名密钥", + "dingtalkSecretPlaceholder": "可选,用于签名", + "customHeaders": "自定义 Headers(JSON)", + "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", + "errors": { + "headersInvalidJson": "Headers 不是有效 JSON", + "headersMustBeObject": "Headers 必须是 JSON 对象", + "headersValueMustBeString": "Headers 的值必须为字符串" + }, + "types": { + "wechat": "企业微信", + "feishu": "飞书", + "dingtalk": "钉钉", + "telegram": "Telegram", + "custom": "自定义 Webhook" + }, + "proxy": { + "title": "代理设置", + "toggle": "展开/收起代理设置", + "url": "代理地址", + "urlPlaceholder": "http://127.0.0.1:7890", + "fallbackToDirect": "代理失败时回退直连" + } + }, + "bindings": { + "title": "绑定", + "noTargets": "暂无可用推送目标", + "bindTarget": "绑定目标", + "enable": "启用", + "enableType": "启用该通知", + "advanced": "高级", + "scheduleCron": "Cron 表达式", + "scheduleCronPlaceholder": "例如:0 9 * * *", + "scheduleTimezone": "时区", + "templateOverride": "模板覆盖", + "editTemplateOverride": "编辑覆盖", + "templateOverrideTitle": "编辑模板覆盖", + "boundCount": "已绑定:{count}", + "enabledCount": "已启用:{count}" + }, + "templateEditor": { + "title": "模板(JSON)", + "placeholder": "请输入 JSON 模板...", + "jsonInvalid": "JSON 格式不正确", + "placeholders": "占位符", + "insert": "插入" + }, + "circuitBreaker": { + "title": "熔断器告警", + "description": "供应商完全熔断时立即推送告警消息", + "enable": "启用熔断器告警", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "test": "测试连接" + }, + "dailyLeaderboard": { + "title": "每日用户消费排行榜", + "description": "每天定时发送用户消费 Top N 排行榜", + "enable": "启用每日排行榜", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "time": "发送时间", + "timePlaceholder": "09:00", + "timeError": "时间格式错误,应为 HH:mm", + "topN": "显示前 N 名", + "test": "测试连接" + }, + "costAlert": { + "title": "成本预警", + "description": "检测用户/供应商消费超过配额阈值时触发告警", + "enable": "启用成本预警", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeWeCom": "企业微信", + "webhookTypeFeishu": "飞书", + "webhookTypeUnknown": "未知平台,请使用企业微信或飞书的 Webhook URL", + "threshold": "预警阈值", + "thresholdLabel": "预警阈值: {percent}%", + "thresholdHelp": "当消费达到配额的 {percent}% 时触发告警", + "interval": "检查间隔(分钟)", + "test": "测试连接" + }, + "form": { + "save": "保存设置", + "saving": "保存中...", + "loading": "加载中...", + "success": "通知设置已保存并重新调度任务", + "saveFailed": "保存失败", + "saveError": "保存设置失败", + "loadError": "加载通知设置失败", + "webhookRequired": "请先填写 Webhook URL", + "testSuccess": "测试消息已发送", + "testFailed": "测试失败", + "testFailedRetry": "测试失败,请重试", + "testError": "测试连接失败", + "testNoResult": "测试成功但未返回结果" + } +} diff --git a/messages/zh-CN/settings/prices.json b/messages/zh-CN/settings/prices.json new file mode 100644 index 000000000..68ef8d9ee --- /dev/null +++ b/messages/zh-CN/settings/prices.json @@ -0,0 +1,191 @@ +{ + "title": "价格表", + "description": "管理平台基础配置与模型价格", + "section": { + "title": "模型价格", + "description": "管理 AI 模型的价格配置" + }, + "searchPlaceholder": "搜索模型名称...", + "filters": { + "all": "全部", + "local": "本地", + "anthropic": "Anthropic", + "openai": "OpenAI", + "vertex": "Vertex" + }, + "badges": { + "local": "本地" + }, + "capabilities": { + "assistantPrefill": "助手预填充", + "computerUse": "电脑使用", + "functionCalling": "函数调用", + "pdfInput": "PDF 输入", + "promptCaching": "Prompt 缓存", + "reasoning": "推理", + "responseSchema": "响应 Schema", + "toolChoice": "工具选择", + "vision": "视觉", + "statusSupported": "支持", + "statusUnsupported": "不支持", + "tooltip": "{label}: {status}" + }, + "sync": { + "button": "同步云端价格表", + "syncing": "同步中...", + "checking": "检查冲突...", + "successWithChanges": "价格表更新: 新增 {added} 个,更新 {updated} 个,未变化 {unchanged} 个", + "successNoChanges": "价格表已是最新,无需更新", + "failed": "同步失败", + "failedError": "同步失败: {error}", + "failedNoResult": "价格表更新成功但未返回处理结果", + "noModels": "未找到支持的模型价格", + "partialFailure": "部分更新成功,但有 {failed} 个模型失败", + "failedModels": "失败模型: {models}", + "skippedConflicts": "跳过 {count} 个手动模型" + }, + "conflict": { + "title": "选择要覆盖的冲突项", + "description": "以下模型存在手动维护的价格,勾选后将用 LiteLLM 价格覆盖,未勾选的保持本地不变", + "searchPlaceholder": "搜索模型...", + "table": { + "modelName": "模型", + "manualPrice": "手动价格", + "litellmPrice": "LiteLLM 价格", + "action": "操作" + }, + "viewDiff": "查看差异", + "diffTitle": "价格差异对比", + "diff": { + "field": "字段", + "manual": "手动", + "litellm": "LiteLLM", + "inputPrice": "输入价格", + "outputPrice": "输出价格", + "imagePrice": "图片价格", + "provider": "供应商", + "mode": "类型" + }, + "pagination": { + "showing": "显示 {from}-{to} 条,共 {total} 条" + }, + "selectedCount": "已选择 {count}/{total} 个模型", + "noMatch": "未找到匹配的模型", + "noConflicts": "无冲突项", + "applyOverwrite": "应用覆盖", + "applying": "应用中..." + }, + "table": { + "modelName": "模型名称", + "provider": "提供商", + "capabilities": "能力", + "price": "价格", + "inputPrice": "输入价格 ($/M)", + "outputPrice": "输出价格 ($/M)", + "priceInput": "输入", + "priceOutput": "输出", + "pricePerRequest": "按次", + "cacheReadPrice": "缓存读取 ($/M)", + "cacheCreationPrice": "缓存创建 ($/M)", + "cache5m": "5m", + "cache1h": "1h+", + "copyModelId": "复制模型 ID", + "updatedAt": "更新时间", + "actions": "操作", + "typeChat": "对话", + "typeImage": "图像生成", + "typeCompletion": "补全", + "typeUnknown": "未知", + "loading": "加载中...", + "noMatch": "未找到匹配的模型", + "noDataTitle": "暂无价格数据", + "noDataHint": "系统已内置价格表,请通过上方按钮同步或更新" + }, + "pagination": { + "showing": "显示 {from}-{to} 条,共 {total} 条", + "previous": "上一页", + "next": "下一页", + "perPageLabel": "每页", + "perPage": "每页 {size} 条" + }, + "stats": { + "totalModels": "共 {count} 个模型", + "searchResults": "搜索到 {count} 个结果", + "lastUpdated": "最后更新: {time}" + }, + "dialog": { + "title": "更新模型价格表", + "description": "选择包含模型价格数据的 JSON 或 TOML 文件并上传", + "selectFile": "点击选择 JSON/TOML 文件或拖拽到此处", + "fileSizeLimit": "文件大小不能超过 10MB", + "fileSizeLimitSmall": "文件大小不超过 10MB", + "invalidFileType": "请选择 JSON 或 TOML 格式的文件", + "fileTooLarge": "文件大小超过 10MB 限制", + "upload": "上传并更新", + "uploading": "上传中...", + "updatePriceTable": "更新价格表", + "updating": "更新中...", + "selectJson": "选择文件", + "updateSuccess": "价格表更新成功,共更新 {count} 个模型", + "updateFailed": "更新失败", + "systemHasBuiltIn": "系统已内置价格表", + "manualDownload": "你也可以手动下载", + "latestPriceTable": "云端价格表", + "andUploadViaButton": ",并通过上方按钮上传", + "cloudModelCountLoading": "云端模型数量加载中...", + "cloudModelCountFailed": "云端模型数量加载失败", + "supportedModels": "当前支持 {count} 个模型", + "results": { + "title": "更新结果", + "total": "总计: {total} 个模型", + "success": "成功: {success}", + "failed": "失败: {failed}", + "skipped": "跳过: {skipped}", + "more": " (+{count})", + "details": "详细信息", + "viewDetails": "查看详细日志" + } + }, + "addModel": "添加模型", + "editModel": "编辑模型", + "deleteModel": "删除模型", + "addModelDescription": "手动添加新的模型价格配置", + "editModelDescription": "编辑模型的价格配置", + "deleteConfirm": "确定要删除模型 {name} 吗?此操作不可撤销。", + "form": { + "modelName": "模型 ID", + "modelNamePlaceholder": "例如: gpt-5.2-codex", + "modelNameRequired": "模型 ID 不能为空", + "displayName": "展示名称(可选)", + "displayNamePlaceholder": "例如: GPT-5.2 Codex", + "type": "类型", + "provider": "供应商", + "providerPlaceholder": "例如: openai", + "requestPrice": "按次调用价格 ($/request)", + "inputPrice": "输入价格 ($/M tokens)", + "outputPrice": "输出价格 ($/M tokens)", + "outputPriceImage": "输出价格 ($/image)", + "cacheReadPrice": "缓存读取价格 ($/M tokens)", + "cacheCreationPrice5m": "缓存创建价格(5m,$/M tokens)", + "cacheCreationPrice1h": "缓存创建价格(1h+,$/M tokens)" + }, + "drawer": { + "prefillLabel": "搜索现有模型并预填充", + "prefillEmpty": "未找到匹配的模型", + "prefillFailed": "搜索失败", + "promptCachingHint": "仅当模型支持缓存时开启,并配置下方缓存价格", + "cachePricingTitle": "缓存价格" + }, + "actions": { + "edit": "编辑", + "more": "更多操作", + "delete": "删除" + }, + "toast": { + "createSuccess": "模型已添加", + "updateSuccess": "模型已更新", + "deleteSuccess": "模型已删除", + "saveFailed": "保存失败", + "deleteFailed": "删除失败" + } +} diff --git a/messages/zh-CN/settings/providers/autoSort.json b/messages/zh-CN/settings/providers/autoSort.json new file mode 100644 index 000000000..07fa9a71f --- /dev/null +++ b/messages/zh-CN/settings/providers/autoSort.json @@ -0,0 +1,16 @@ +{ + "button": "自动排序优先级", + "dialogTitle": "自动排序供应商优先级", + "dialogDescription": "根据成本倍率自动分配优先级(低成本 = 高优先级)", + "changeCount": "{count} 个供应商将被更新", + "noChanges": "无需更改(已排序)", + "costMultiplierHeader": "成本倍率", + "priorityHeader": "优先级", + "providersHeader": "供应商", + "changesTitle": "变更详情", + "providerHeader": "供应商", + "priorityChangeHeader": "优先级变更", + "confirm": "应用变更", + "success": "已更新 {count} 个供应商的优先级", + "error": "更新优先级失败" +} diff --git a/messages/zh-CN/settings/providers/filter.json b/messages/zh-CN/settings/providers/filter.json new file mode 100644 index 000000000..b1d3dcf97 --- /dev/null +++ b/messages/zh-CN/settings/providers/filter.json @@ -0,0 +1,13 @@ +{ + "status": { + "all": "全部状态", + "active": "已启用", + "inactive": "已禁用" + }, + "groups": { + "label": "分组:", + "all": "全部", + "default": "default" + }, + "circuitBroken": "熔断" +} diff --git a/messages/zh-CN/settings/providers/form/apiTest.json b/messages/zh-CN/settings/providers/form/apiTest.json new file mode 100644 index 000000000..fc43eee41 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/apiTest.json @@ -0,0 +1,157 @@ +{ + "fillUrlFirst": "请先填写供应商 URL", + "invalidUrl": "供应商 URL 无效,仅支持 http/https", + "fillKeyFirst": "请先填写 API 密钥", + "testFailed": "测试失败", + "testFailedRetry": "测试失败,请重试", + "noResult": "测试成功但未返回结果", + "testSuccess": "模型测试成功", + "testApi": "供应商模型测试", + "testing": "测试中...", + "apiFormat": "供应商类型", + "selectApiFormat": "选择要测试的供应商类型", + "apiFormatDesc": "默认同步路由配置中的供应商类型,除非手动修改", + "formatAnthropicMessages": "Claude (Anthropic Messages API)", + "formatOpenAIChat": "OpenAI Compatible", + "formatOpenAIResponses": "Codex (Response API)", + "testModel": "测试模型", + "testModelDesc": "可手动输入,不填写则使用默认模型", + "requestConfig": "请求配置", + "presetConfig": "预置配置", + "customConfig": "自定义", + "selectPreset": "选择预置模板", + "presetDesc": "预置模板包含真实 CLI 请求特征,用于通过中转服务验证", + "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", + "customPayloadDesc": "输入自定义 JSON payload,将覆盖默认请求体", + "successContains": "成功检测词", + "successContainsPlaceholder": "pong", + "successContainsDesc": "响应需包含此内容才算测试成功", + "model": "模型", + "responseModel": "响应模型", + "responseTime": "响应时间", + "usage": "Token 用量", + "response": "响应内容", + "error": "错误信息", + "unknown": "未知", + "viewDetails": "查看详情", + "copySuccess": "已复制到剪贴板", + "copyFailed": "复制失败", + "copyResult": "复制结果", + "close": "关闭", + "success": "成功", + "failed": "失败", + "streamInfo": "流式响应信息", + "chunksReceived": "接收到的数据块", + "streamFormat": "流式格式", + "streamResponse": "流式响应", + "chunksCount": "接收 {count} 个数据块 ({format})", + "truncatedPreview": "显示前 {length} 字符,完整内容请复制查看", + "truncatedBrief": "显示前 {length} 字符,完整内容请点击「查看详情」", + "timeout": { + "label": "超时时间(秒)", + "desc": "测试请求的最大等待时间(5-120 秒)", + "geminiHint": ",Gemini Thinking 模型建议 60 秒以上" + }, + "geminiAuthFallback": { + "warning": "Header 认证失败,使用了 URL 参数认证", + "desc": "实际代理转发仅使用 Header 认证,可能导致请求失败" + }, + "copyFormat": { + "testResult": "测试结果", + "message": "消息", + "errorDetails": "错误详情" + }, + "disclaimer": { + "title": "注意", + "resultReference": "【重要】因各家供应商情况不同,测试结果仅供参考,不代表实际调用效果", + "realRequest": "测试将向供应商发送真实请求,可能消耗少量额度", + "confirmConfig": "请确认供应商 URL、API 密钥及模型配置正确" + }, + "resultCard": { + "status": { + "green": "可用", + "yellow": "波动", + "red": "不可用" + }, + "dialogTitle": "供应商测试详情", + "validation": { + "title": "三层验证详情", + "http": { + "title": "Tier 1: HTTP 状态", + "statusCode": "状态码", + "passed": "2xx/3xx 成功", + "failed": "4xx/5xx 失败" + }, + "latency": { + "title": "Tier 2: 延迟阈值", + "actual": "实际延迟", + "passed": "在阈值内", + "failed": "超过阈值" + }, + "content": { + "title": "Tier 3: 内容验证", + "target": "目标", + "passed": "包含目标字符串", + "failed": "未找到目标" + }, + "passed": "通过", + "failed": "失败", + "timeout": "超时" + }, + "labels": { + "http": "HTTP", + "latency": "延迟", + "content": "内容", + "model": "模型", + "firstByte": "首字节", + "totalLatency": "总延迟", + "error": "错误", + "responsePreview": "响应预览" + }, + "timing": { + "title": "时间信息", + "totalLatency": "总延迟", + "firstByte": "首字节", + "testedAt": "测试时间" + }, + "tokenUsage": { + "title": "Token 用量", + "input": "输入", + "output": "输出", + "cacheCreation": "缓存创建", + "cacheRead": "缓存读取" + }, + "streamInfo": { + "title": "流式响应信息", + "isStreaming": "流式响应", + "chunksCount": "数据块数", + "yes": "是", + "no": "否" + }, + "rawResponse": { + "title": "完整响应体", + "hint": "此处显示原始响应内容,您可以在此检查关键词是否存在于响应中" + }, + "errorDetails": { + "title": "错误详情", + "type": "错误类型" + }, + "copyText": { + "status": "状态", + "message": "消息", + "latency": "延迟", + "httpStatus": "HTTP 状态", + "model": "模型", + "usage": "用量", + "inputOutput": "输入 {input} / 输出 {output} tokens", + "response": "响应", + "error": "错误", + "testedAt": "测试时间", + "validationDetails": "验证详情", + "httpCheck": "HTTP 检查", + "latencyCheck": "延迟检查", + "contentCheck": "内容验证" + }, + "judgment": "判定" + } +} diff --git a/messages/zh-CN/settings/providers/form/buttons.json b/messages/zh-CN/settings/providers/form/buttons.json new file mode 100644 index 000000000..60b0337b7 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/buttons.json @@ -0,0 +1,9 @@ +{ + "expandAll": "展开全部高级配置", + "collapseAll": "折叠全部高级配置", + "submit": "确认添加", + "submitting": "添加中...", + "update": "确认更新", + "updating": "更新中...", + "delete": "删除" +} diff --git a/messages/zh-CN/settings/providers/form/common.json b/messages/zh-CN/settings/providers/form/common.json new file mode 100644 index 000000000..c394b7c11 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/common.json @@ -0,0 +1,3 @@ +{ + "core": "核心" +} diff --git a/messages/zh-CN/settings/providers/form/deleteDialog.json b/messages/zh-CN/settings/providers/form/deleteDialog.json new file mode 100644 index 000000000..bf89fc4c8 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/deleteDialog.json @@ -0,0 +1,6 @@ +{ + "title": "删除服务商", + "description": "确定要删除服务商 \"{name}\" 吗?此操作不可恢复。", + "cancel": "取消", + "confirm": "确认删除" +} diff --git a/messages/zh-CN/settings/providers/form/errors.json b/messages/zh-CN/settings/providers/form/errors.json new file mode 100644 index 000000000..9b38331ba --- /dev/null +++ b/messages/zh-CN/settings/providers/form/errors.json @@ -0,0 +1,8 @@ +{ + "invalidUrl": "请输入有效的 API 地址", + "invalidWebsiteUrl": "请输入有效的供应商官网地址", + "groupTagTooLong": "分组标签总长度不能超过 {max} 个字符", + "addFailed": "添加服务商失败", + "updateFailed": "更新服务商失败", + "deleteFailed": "删除服务商失败" +} diff --git a/messages/zh-CN/settings/providers/form/failureThresholdConfirmDialog.json b/messages/zh-CN/settings/providers/form/failureThresholdConfirmDialog.json new file mode 100644 index 000000000..942ac5737 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/failureThresholdConfirmDialog.json @@ -0,0 +1,13 @@ +{ + "title": "确认特殊配置", + "descriptionDisabledPrefix": "您将熔断失败阈值设置为 ", + "descriptionDisabledValue": "0", + "descriptionDisabledMiddle": ",这表示", + "descriptionDisabledAction": "禁用熔断器", + "descriptionDisabledSuffix": ",供应商将不会因为连续失败而被熔断。", + "descriptionHighValuePrefix": "您将熔断失败阈值设置为 ", + "descriptionHighValueSuffix": ",这是一个较高的值,可能会导致供应商在大量失败后才被熔断。", + "confirmQuestion": "是否确认保存此配置?", + "cancel": "取消", + "confirm": "确认保存" +} diff --git a/messages/zh-CN/settings/providers/form/key.json b/messages/zh-CN/settings/providers/form/key.json new file mode 100644 index 000000000..bcb728144 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/key.json @@ -0,0 +1,7 @@ +{ + "label": "API 密钥", + "leaveEmpty": "(留空不更改)", + "placeholder": "输入 API 密钥", + "leaveEmptyDesc": "留空则不更改密钥", + "currentKey": "当前密钥: {key}" +} diff --git a/messages/zh-CN/settings/providers/form/maxRetryAttempts.json b/messages/zh-CN/settings/providers/form/maxRetryAttempts.json new file mode 100644 index 000000000..edc10da5b --- /dev/null +++ b/messages/zh-CN/settings/providers/form/maxRetryAttempts.json @@ -0,0 +1,5 @@ +{ + "label": "单供应商最大尝试次数", + "placeholder": "2", + "desc": "包含首次调用在内,单个供应商最多尝试几次后切换。留空使用系统默认值。" +} diff --git a/messages/zh-CN/settings/providers/form/modelRedirect.json b/messages/zh-CN/settings/providers/form/modelRedirect.json new file mode 100644 index 000000000..fc00c1e20 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/modelRedirect.json @@ -0,0 +1,14 @@ +{ + "currentRules": "当前规则 ({count})", + "addNewRule": "添加新规则", + "sourceModel": "用户请求的模型", + "targetModel": "实际转发的模型", + "sourcePlaceholder": "例如: claude-sonnet-4-5-20250929", + "targetPlaceholder": "例如: glm-4.6", + "add": "添加", + "sourceEmpty": "源模型名称不能为空", + "targetEmpty": "目标模型名称不能为空", + "alreadyExists": "模型 \"{model}\" 已存在重定向规则", + "description": "将 Claude Code 客户端请求的模型(如 claude-sonnet-4.5)重定向到上游供应商实际支持的模型(如 glm-4.6、gemini-pro)。用于成本优化或接入第三方 AI 服务。", + "emptyState": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。" +} diff --git a/messages/zh-CN/settings/providers/form/modelSelect.json b/messages/zh-CN/settings/providers/form/modelSelect.json new file mode 100644 index 000000000..663c2e8d2 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/modelSelect.json @@ -0,0 +1,20 @@ +{ + "allowAllModels": "允许所有 {type} 模型", + "selectedCount": "已选择 {count} 个模型", + "searchPlaceholder": "搜索模型名称...", + "loading": "加载中...", + "notFound": "未找到模型", + "selectAll": "全选 ({count})", + "clear": "清空", + "manualAdd": "手动添加模型", + "manualPlaceholder": "输入模型名称(如 gpt-5-turbo)", + "manualDesc": "支持添加任意模型名称(不限于价格表中的模型)", + "claude": "Claude", + "openai": "OpenAI", + "gemini": "Gemini", + "sourceUpstream": "上游", + "sourceUpstreamDesc": "模型列表来自上游服务商 API", + "sourceFallback": "本地", + "sourceFallbackDesc": "使用本地价格表中的模型列表(上游获取失败或不支持)", + "refresh": "刷新模型列表" +} diff --git a/messages/zh-CN/settings/providers/form/name.json b/messages/zh-CN/settings/providers/form/name.json new file mode 100644 index 000000000..5d8ff44e6 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/name.json @@ -0,0 +1,4 @@ +{ + "label": "服务商名称 *", + "placeholder": "例如: 智谱" +} diff --git a/messages/zh-CN/settings/providers/form/providerTypes.json b/messages/zh-CN/settings/providers/form/providerTypes.json new file mode 100644 index 000000000..5475f4233 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/providerTypes.json @@ -0,0 +1,10 @@ +{ + "claude": "Claude (Anthropic Messages API)", + "claudeAuth": "Claude (Anthropic Auth Token)", + "codex": "Codex (Response API)", + "gemini": "Gemini (Google Gemini API)", + "geminiCli": "Gemini CLI", + "geminiCliDisabled": " - 功能开发中", + "openaiCompatible": "OpenAI Compatible", + "openaiCompatibleDisabled": " - 功能开发中" +} diff --git a/messages/zh-CN/settings/providers/form/proxyTest.json b/messages/zh-CN/settings/providers/form/proxyTest.json new file mode 100644 index 000000000..f50c8c86e --- /dev/null +++ b/messages/zh-CN/settings/providers/form/proxyTest.json @@ -0,0 +1,21 @@ +{ + "fillUrlFirst": "请先填写供应商 URL", + "testFailed": "测试失败", + "testFailedRetry": "测试失败,请重试", + "noResult": "测试成功但未返回结果", + "connectionSuccess": "连接成功", + "connectionFailed": "连接失败", + "viaProxy": "(通过代理)", + "viaDirect": "(直连)", + "responseTime": "响应时间:", + "statusCode": "状态码:", + "connectionMethod": "连接方式:", + "proxy": "代理", + "direct": "直连", + "errorType": "错误类型:", + "testing": "测试中...", + "testConnection": "测试连接", + "timeoutError": "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确", + "proxyError": "代理错误:", + "networkError": "网络错误:" +} diff --git a/messages/zh-CN/settings/providers/form/sections.json b/messages/zh-CN/settings/providers/form/sections.json new file mode 100644 index 000000000..709ee56ec --- /dev/null +++ b/messages/zh-CN/settings/providers/form/sections.json @@ -0,0 +1,313 @@ +{ + "routing": { + "title": "路由配置", + "summary": { + "models": "{count} 个模型白名单", + "redirects": "{count} 个重定向", + "none": "未配置" + }, + "providerType": { + "label": "供应商类型", + "desc": "(决定调度策略)", + "placeholder": "选择供应商类型" + }, + "providerTypeDesc": "选择供应商的 API 格式类型。", + "providerTypeDisabledNote": "注:Gemini CLI 和 OpenAI Compatible 类型功能正在开发中,暂不可用", + "modelRedirects": { + "label": "模型重定向配置", + "optional": "(可选)" + }, + "joinClaudePool": { + "label": "加入 Claude 调度池", + "desc": "启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度", + "help": "仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用户请求 claude-* 模型时,此供应商也会参与调度选择。" + }, + "preserveClientIp": { + "label": "透传客户端 IP", + "desc": "向上游转发 x-forwarded-for / x-real-ip,可能暴露真实来源 IP", + "help": "默认关闭以保护隐私;仅在需要上游感知终端 IP 时开启。" + }, + "modelWhitelist": { + "title": "模型白名单", + "desc": "限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。", + "label": "允许的模型", + "optional": "(可选)", + "allowAll": "✓ 允许所有模型(推荐)", + "selectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。", + "moreModels": "+{count} 更多" + }, + "scheduleParams": { + "title": "调度参数", + "priority": { + "label": "优先级", + "placeholder": "0", + "desc": "数值越小优先级越高(0 最高)。系统只从最高优先级的供应商中选择。建议:主力=0,备用=1,紧急备份=2" + }, + "weight": { + "label": "权重", + "placeholder": "1", + "desc": "加权随机概率。同优先级内,权重越高被选中概率越大。例如权重 1:2:3 的概率为 16%:33%:50%" + }, + "costMultiplier": { + "label": "成本倍率", + "placeholder": "1.0", + "desc": "成本计算倍数。官方供应商=1.0,便宜 20%=0.8,贵 20%=1.2(支持最多 4 位小数)" + }, + "group": { + "label": "供应商分组", + "placeholder": "输入分组标签", + "desc": "供应商分组标签(支持多个,逗号分隔)。只有用户的 providerGroup 与此值匹配时,该用户才能使用此供应商。留空=对所有用户开放" + } + }, + "cacheTtl": { + "label": "Cache TTL 覆写", + "options": { + "inherit": "不覆写(跟随客户端)", + "5m": "5 分钟", + "1h": "1 小时" + }, + "desc": "强制设置 prompt cache TTL;仅影响包含 cache_control 的请求。" + }, + "context1m": { + "label": "1M 上下文窗口", + "options": { + "inherit": "跟随客户端", + "forceEnable": "强制启用", + "disabled": "禁用" + }, + "desc": "配置 1M 上下文窗口支持。仅对 Sonnet 模型生效(claude-sonnet-4-5、claude-sonnet-4)。启用后将应用阶梯定价。" + }, + "codexOverrides": { + "reasoningEffort": { + "label": "推理等级覆写", + "help": "控制模型在输出前用于推理的强度(推理 token 数量)。选择“跟随客户端”表示不改写请求;选择其他值则强制覆写 reasoning.effort。注意:none 仅 GPT-5.1 系列支持;xhigh 仅 GPT-5.1-Codex-Max 支持,模型不支持会返回错误。", + "options": { + "inherit": "不覆写(跟随客户端)", + "minimal": "minimal(更省、更快)", + "low": "low", + "medium": "medium(默认)", + "high": "high(更强推理)", + "xhigh": "xhigh(仅 GPT-5.1-Codex-Max)", + "none": "none(仅 GPT-5.1)" + } + }, + "reasoningSummary": { + "label": "推理摘要覆写", + "help": "控制是否返回推理摘要。auto 返回精简摘要,detailed 返回更详细摘要;“跟随客户端”不改写 reasoning.summary。", + "options": { + "inherit": "不覆写(跟随客户端)", + "auto": "auto(精简)", + "detailed": "detailed(更详细)" + } + }, + "textVerbosity": { + "label": "输出冗长度覆写", + "help": "控制模型输出的详细程度。low 更简洁,high 更详细;“跟随客户端”不改写 text.verbosity。", + "options": { + "inherit": "不覆写(跟随客户端)", + "low": "low(简洁)", + "medium": "medium(默认)", + "high": "high(更详细)" + } + }, + "parallelToolCalls": { + "label": "并行工具调用覆写", + "help": "控制是否允许并行 tool calls。关闭可能降低工具调用并发能力;“跟随客户端”不改写 parallel_tool_calls。", + "options": { + "inherit": "不覆写(跟随客户端)", + "true": "强制开启", + "false": "强制关闭" + } + } + } + }, + "rateLimit": { + "title": "限流配置", + "summary": { + "fiveHour": "5h: {amount}", + "daily": "日: {amount} (重置 {resetTime})", + "weekly": "周: {amount}", + "monthly": "月: {amount}", + "total": "总: {amount}", + "concurrent": "并发: {count}", + "none": "无限制" + }, + "limit5h": { + "label": "5小时消费上限 (USD)", + "placeholder": "留空表示无限制" + }, + "limitDaily": { + "label": "每日消费上限 (USD)", + "placeholder": "留空表示无限制" + }, + "dailyResetMode": { + "label": "每日重置模式", + "options": { + "fixed": "固定时间重置", + "rolling": "滚动窗口(24小时)" + }, + "desc": { + "fixed": "每天固定时间点重置配额", + "rolling": "从首次调用开始计算,24小时后重置" + } + }, + "dailyResetTime": { + "label": "每日重置时间 (HH:mm)" + }, + "limitWeekly": { + "label": "周消费上限 (USD)", + "placeholder": "留空表示无限制" + }, + "limitMonthly": { + "label": "月消费上限 (USD)", + "placeholder": "留空表示无限制" + }, + "limitTotal": { + "label": "总消费上限 (USD)", + "placeholder": "留空表示无限制" + }, + "limitConcurrent": { + "label": "并发 Session 上限", + "placeholder": "0 表示无限制" + } + }, + "circuitBreaker": { + "title": "熔断器配置", + "summary": "{failureThreshold} 次失败 / {openDuration} 分钟熔断 / {successThreshold} 次成功恢复 / 每个供应商最多 {maxRetryAttempts} 次尝试", + "desc": "供应商连续失败时自动熔断,避免影响整体服务质量", + "failureThreshold": { + "label": "失败阈值(次)", + "placeholder": "5", + "desc": "连续失败多少次后触发熔断" + }, + "openDuration": { + "label": "熔断时长(分钟)", + "placeholder": "30", + "desc": "熔断后多久自动进入半开状态" + }, + "successThreshold": { + "label": "恢复阈值(次)", + "placeholder": "2", + "desc": "半开状态下成功多少次后完全恢复" + }, + "maxRetryAttempts": { + "label": "单供应商最大尝试次数", + "placeholder": "2", + "desc": "包含首次调用在内,单个供应商最多尝试次数,超过后切换其他供应商。留空使用系统默认值。" + } + }, + "proxy": { + "title": "代理配置", + "summary": { + "configured": "已配置代理", + "fallback": " (启用降级)", + "none": "未配置" + }, + "desc": "配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)", + "url": { + "label": "代理地址", + "optional": "(可选)", + "placeholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", + "formats": "支持格式:" + }, + "fallback": { + "label": "代理失败时降级到直连", + "desc": "启用后,代理连接失败时自动尝试直接连接供应商" + }, + "test": { + "label": "连接测试", + "desc": "测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)" + } + }, + "timeout": { + "title": "超时配置", + "summary": "首字: {streaming}s | 流式间隔: {idle}s | 非流式: {nonStreaming}s", + "desc": "配置请求超时时间,0 表示禁用超时", + "streamingFirstByte": { + "label": "流式首字节超时(秒)", + "placeholder": "30", + "desc": "流式请求首字节超时,范围 1-120 秒,默认 30 秒", + "core": "true" + }, + "streamingIdle": { + "label": "流式静默期超时(秒)", + "placeholder": "60", + "desc": "流式请求静默期超时,范围 60-600 秒,填 0 禁用(防止中途卡住)", + "core": "true" + }, + "nonStreamingTotal": { + "label": "非流式总超时(秒)", + "placeholder": "600", + "desc": "非流式请求总超时,范围 60-1200 秒,默认 600 秒(10 分钟)", + "core": "true" + }, + "disableHint": "设为 0 表示禁用该超时(仅用于灰度回退场景,不推荐)" + }, + "apiTest": { + "title": "供应商模型测试", + "summary": "验证供应商与模型连通性", + "desc": "测试供应商模型是否可用,默认与路由配置中选择的供应商类型保持一致。", + "testLabel": "供应商模型测试" + }, + "codexStrategy": { + "title": "Codex Instructions 策略", + "summary": { + "auto": "自动 (推荐)", + "force": "强制官方", + "keep": "透传原样" + }, + "desc": "控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性", + "select": { + "label": "策略选择", + "auto": { + "label": "自动 (推荐)", + "desc": "透传客户端 instructions,400 错误时自动重试官方 prompt" + }, + "force": { + "label": "强制官方", + "desc": "始终使用官方 Codex CLI instructions(约 4000+ 字)" + }, + "keep": { + "label": "透传原样", + "desc": "始终透传客户端 instructions,不自动重试(适用于宽松中转站)" + }, + "placeholder": "选择策略" + }, + "hint": "提示: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方 instructions,选择\"自动\"或\"强制官方\"策略" + }, + "mcpPassthrough": { + "title": "MCP 透传配置", + "summary": { + "none": "不启用", + "minimax": "Minimax", + "glm": "智谱 GLM", + "custom": "自定义 (预留)" + }, + "desc": "启用后,将 MCP 工具调用透传到指定的 AI 服务商(如 minimax 的图片识别、联网搜索)", + "select": { + "label": "透传类型", + "none": { + "label": "不启用", + "desc": "不启用 MCP 透传功能(默认)" + }, + "minimax": { + "label": "Minimax", + "desc": "透传到 minimax MCP 服务(支持图片识别、联网搜索等工具)" + }, + "glm": { + "label": "智谱 GLM", + "desc": "透传到智谱 MCP 服务(支持图片分析、视频分析等工具)" + }, + "custom": { + "label": "自定义", + "desc": "透传到自定义 MCP 服务(预留,暂未实现)" + }, + "placeholder": "选择透传类型" + }, + "hint": "提示: MCP 透传功能允许 Claude Code 客户端使用第三方 AI 服务商提供的工具能力(如图片识别、联网搜索)", + "urlLabel": "MCP 透传 URL", + "urlPlaceholder": "https://api.minimaxi.com", + "urlDesc": "MCP 服务的基础 URL。留空则自动从提供商 URL 提取基础域名", + "urlAuto": "自动提取: {url}" + } +} diff --git a/messages/zh-CN/settings/providers/form/strings.json b/messages/zh-CN/settings/providers/form/strings.json new file mode 100644 index 000000000..895b5c493 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/strings.json @@ -0,0 +1,204 @@ +{ + "namePlaceholder": "输入供应商名称", + "baseUrl": "基础 URL", + "baseUrlPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic", + "baseUrlRequired": "请先填写供应商 URL", + "apiKey": "API 密钥", + "apiKeyPlaceholder": "输入 API 密钥", + "apiKeyOptional": "留空则不更改密钥", + "weight": "权重", + "weightDesc": "加权随机概率。同优先级内,权重越高被选中概率越大。例如权重 1:2:3 的概率为 16%:33%:50%", + "priority": "优先级", + "priorityDesc": "数值越小优先级越高(0 最高)。系统只从最高优先级的供应商中选择。建议:主力=0,备用=1,紧急备份=2", + "enabled": "启用", + "costMultiplier": "成本倍数", + "costMultiplierDesc": "成本计算倍数。官方供应商=1.0,便宜 20%=0.8,贵 20%=1.2(支持最多 4 位小数)", + "limitConcurrent": "并发 Session 限制", + "limitConcurrentDesc": "例如: 供应商 C 并发限制 2,当前活跃 Session 数:2", + "limitAmount5h": "5 小时消费上限 (USD)", + "limitAmount5hDesc": "例如: 供应商 B 的 5 小时限额 $10,已消耗 $9.8", + "limitAmountWeekly": "周消费上限 (USD)", + "limitAmountMonthly": "月消费上限 (USD)", + "modelRedirects": "模型重定向", + "modelRedirectsDesc": "将 Claude Code 客户端请求的模型(如 claude-sonnet-4.5)重定向到上游供应商实际支持的模型(如 glm-4.6、gemini-pro)。用于成本优化或接入第三方 AI 服务。", + "sourceModel": "源模型名称", + "sourceModelPlaceholder": "例如: claude-sonnet-4-5-20250929", + "sourceModelRequired": "源模型名称不能为空", + "targetModel": "目标模型名称", + "targetModelPlaceholder": "例如: glm-4.6", + "targetModelRequired": "目标模型名称不能为空", + "addRedirect": "添加重定向", + "removeRedirect": "移除重定向", + "allowAllModels": "✓ 允许所有模型(推荐)", + "proxy": "代理", + "proxyUrl": "代理地址", + "proxyUrlPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", + "proxyFallback": "代理失败降级", + "proxyFallbackDesc": "启用后,代理连接失败时自动尝试直接连接供应商", + "testProxy": "测试连接", + "testProxySuccess": "代理连接成功", + "testProxyFailed": "测试代理连接失败", + "testProxyFailedError": "测试连接失败:", + "proxyConfigured": "已配置代理", + "proxyNotConfigured": "未配置", + "providerType": "供应商类型", + "selectProviderType": "选择供应商类型", + "codexInstructions": "Codex Instructions 策略", + "codexInstructionsDesc": "(决定调度策略)", + "codexInstructionsAuto": "自动 (推荐)", + "codexInstructionsForce": "强制官方", + "codexInstructionsKeep": "保留原值", + "group": "分组", + "groupPlaceholder": "例如: premium, economy", + "remark": "备注", + "remarkPlaceholder": "可选:添加说明...", + "sort": "排序供应商", + "sortByName": "按名称 (A-Z)", + "sortByWeight": "按权重 (高-低)", + "sortByPriority": "按优先级 (高-低)", + "sortByCreated": "按创建时间 (新-旧)", + "sortByCost": "按成本排序", + "filterByType": "筛选供应商类型", + "searchPlaceholder": "搜索供应商名称、URL、备注...", + "clearSearch": "清除搜索", + "limit0Means": "0 表示无限制", + "leaveEmpty": "留空表示无限制", + "providerName": "服务商名称", + "providerNameRequired": "服务商名称 *", + "providerNamePlaceholder": "例如: 智谱", + "apiAddress": "API 地址", + "apiAddressRequired": "API 地址 *", + "apiAddressPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic", + "apiKeyRequired": "API 密钥 *", + "apiKeyLeaveEmpty": "(留空不更改)", + "apiKeyLeaveEmptyDesc": "留空则不更改密钥", + "apiKeyCurrent": "当前密钥:", + "websiteUrlPlaceholder": "https://example.com", + "websiteUrlDesc": "供应商官网地址,用于快速跳转管理", + "websiteUrlInvalid": "请输入有效的供应商官网地址", + "expandAll": "展开全部高级配置", + "collapseAll": "折叠全部高级配置", + "routingConfig": "路由配置", + "routingConfigSummary": "{models} 个模型白名单, {redirects} 个重定向", + "routingConfigNone": "未配置", + "providerTypeDesc": "选择供应商的 API 格式类型。", + "providerTypeDisabledNote": "注:OpenAI Compatible 类型功能正在开发中,暂不可用", + "modelRedirectsLabel": "模型重定向配置", + "modelRedirectsOptional": "(可选)", + "modelRedirectsEmpty": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。", + "modelRedirectsCurrentRules": "当前规则 ({count})", + "modelRedirectsAddNew": "添加新规则", + "modelRedirectsSourceModel": "用户请求的模型", + "modelRedirectsSourcePlaceholder": "例如: claude-sonnet-4-5-20250929", + "modelRedirectsTargetModel": "实际转发的模型", + "modelRedirectsTargetPlaceholder": "例如: glm-4.6", + "modelRedirectsSourceRequired": "源模型名称不能为空", + "modelRedirectsTargetRequired": "目标模型名称不能为空", + "modelRedirectsExists": "模型 \"{model}\" 已存在重定向规则", + "joinClaudePool": "加入 Claude 调度池", + "joinClaudePoolDesc": "启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度", + "joinClaudePoolHelp": "仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用户请求 claude-* 模型时,此供应商也会参与调度选择。", + "modelWhitelist": "模型白名单", + "modelWhitelistDesc": "限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。", + "modelWhitelistLabel": "允许的模型", + "modelWhitelistSelected": "已选择 {count} 个模型", + "modelWhitelistLoading": "加载中...", + "modelWhitelistNotFound": "未找到模型", + "modelWhitelistSearchPlaceholder": "搜索模型名称...", + "modelWhitelistSelectAll": "全选 ({count})", + "modelWhitelistClear": "清空", + "modelWhitelistManualAdd": "手动添加模型", + "modelWhitelistManualPlaceholder": "输入模型名称(如 gpt-5-turbo)", + "modelWhitelistManualDesc": "支持添加任意模型名称(不限于价格表中的模型)", + "modelWhitelistAllowAll": "允许所有 {type} 模型", + "modelWhitelistAllowAllClause": "允许所有 Claude 模型", + "modelWhitelistAllowAllOpenAI": "允许所有 OpenAI 模型", + "modelWhitelistSelectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。", + "scheduleParams": "调度参数", + "priorityLabel": "优先级", + "priorityPlaceholder": "0", + "weightLabel": "权重", + "weightPlaceholder": "1", + "costMultiplierLabel": "成本倍率", + "costMultiplierPlaceholder": "1.0", + "providerGroupLabel": "供应商分组", + "providerGroupPlaceholder": "例如: premium, economy", + "providerGroupDesc": "供应商分组标签。只有用户的 providerGroup 与此值匹配时,该用户才能使用此供应商。示例:设置为 \"premium\" 表示只供 providerGroup=\"premium\" 的用户使用", + "rateLimitConfig": "限流配置", + "rateLimitConfigSummary": "5h: ${fiveHour}, 周: ${weekly}, 月: ${monthly}, 并发: {concurrent}", + "rateLimitConfigNone": "无限制", + "limit5hLabel": "5小时消费上限 (USD)", + "limitWeeklyLabel": "周消费上限 (USD)", + "limitMonthlyLabel": "月消费上限 (USD)", + "limitConcurrentLabel": "并发 Session 上限", + "limitPlaceholderUnlimited": "留空表示无限制", + "limitPlaceholder0": "0 表示无限制", + "circuitBreakerConfig": "熔断器配置", + "circuitBreakerConfigSummary": "{failureThreshold} 次失败 / {openDuration} 分钟熔断 / {successThreshold} 次成功恢复 / 每个供应商最多 {maxRetryAttempts} 次尝试", + "circuitBreakerDesc": "供应商连续失败时自动熔断,避免影响整体服务质量", + "failureThreshold": "失败阈值(次)", + "failureThresholdPlaceholder": "5", + "failureThresholdDesc": "连续失败多少次后触发熔断", + "openDuration": "熔断时长(分钟)", + "openDurationPlaceholder": "30", + "openDurationDesc": "熔断后多久自动进入半开状态", + "successThreshold": "恢复阈值(次)", + "successThresholdPlaceholder": "2", + "successThresholdDesc": "半开状态下成功多少次后完全恢复", + "proxyConfig": "代理配置", + "proxyConfigSummary": "已配置代理", + "proxyConfigSummaryFallback": " (启用降级)", + "proxyConfigNone": "未配置", + "proxyConfigDesc": "配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)", + "proxyAddressLabel": "代理地址", + "proxyAddressOptional": "(可选)", + "proxyAddressPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", + "proxyAddressFormats": "支持格式:", + "proxyFallbackLabel": "代理失败时降级到直连", + "proxyTestLabel": "连接测试", + "proxyTestButton": "测试连接", + "proxyTestTesting": "测试中...", + "proxyTestSuccess": "连接成功", + "proxyTestFailed": "连接失败", + "proxyTestDesc": "测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)", + "proxyTestFillUrl": "请先填写供应商 URL", + "proxyTestResultSuccess": "连接成功 {via}", + "proxyTestViaProxy": "(通过代理)", + "proxyTestViaDirect": "(直连)", + "proxyTestResponseTime": "响应时间: {time}", + "proxyTestStatusCode": "| 状态码: {code}", + "proxyTestResultFailed": "连接失败", + "proxyTestTimeout": "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确", + "proxyTestProxyError": "代理错误: {error}", + "proxyTestNetworkError": "网络错误: {error}", + "proxyTestResultMessage": "{message}", + "proxyTestResultStatusCode": "状态码: {code}", + "proxyTestResultResponseTime": "响应时间: {time}ms", + "proxyTestResultConnectionMethod": "连接方式: {via}", + "proxyTestResultConnectionMethodProxy": "代理", + "proxyTestResultConnectionMethodDirect": "直连", + "proxyTestResultErrorType": "错误类型: {type}", + "codexStrategyConfig": "Codex Instructions 策略", + "codexStrategyConfigAuto": "自动 (推荐)", + "codexStrategyConfigForce": "强制官方", + "codexStrategyConfigKeep": "透传原样", + "codexStrategyDesc": "控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性", + "codexStrategySelect": "策略选择", + "codexStrategyAutoLabel": "自动 (推荐)", + "codexStrategyAutoDesc": "透传客户端 instructions,400 错误时自动重试官方 prompt", + "codexStrategyForceLabel": "强制官方", + "codexStrategyForceDesc": "始终使用官方 Codex CLI instructions(约 4000+ 字)", + "codexStrategyKeepLabel": "透传原样", + "codexStrategyKeepDesc": "始终透传客户端 instructions,不自动重试(适用于宽松中转站)", + "codexStrategyHint": "提示: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方 instructions,选择\"自动\"或\"强制官方\"策略", + "confirmAdd": "确认添加", + "confirmUpdate": "确认更新", + "confirmAddPending": "添加中...", + "confirmUpdatePending": "更新中...", + "deleteButton": "删除", + "validUrlRequired": "请输入有效的 API 地址", + "filterProvider": "筛选供应商类型", + "filterAllProviders": "全部供应商", + "searchClear": "清除搜索", + "dialogDescription": "配置供应商信息及高级设置。" +} diff --git a/messages/zh-CN/settings/providers/form/success.json b/messages/zh-CN/settings/providers/form/success.json new file mode 100644 index 000000000..7f79b9556 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/success.json @@ -0,0 +1,4 @@ +{ + "created": "添加服务商成功", + "createdDesc": "服务商 \"{name}\" 已添加" +} diff --git a/messages/zh-CN/settings/providers/form/title.json b/messages/zh-CN/settings/providers/form/title.json new file mode 100644 index 000000000..674a8cf81 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/title.json @@ -0,0 +1,4 @@ +{ + "create": "新增服务商", + "edit": "编辑服务商" +} diff --git a/messages/zh-CN/settings/providers/form/url.json b/messages/zh-CN/settings/providers/form/url.json new file mode 100644 index 000000000..338c3eb4e --- /dev/null +++ b/messages/zh-CN/settings/providers/form/url.json @@ -0,0 +1,4 @@ +{ + "label": "API 地址 *", + "placeholder": "例如: https://open.bigmodel.cn/api/anthropic" +} diff --git a/messages/zh-CN/settings/providers/form/urlPreview.json b/messages/zh-CN/settings/providers/form/urlPreview.json new file mode 100644 index 000000000..c2566d4b0 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/urlPreview.json @@ -0,0 +1,9 @@ +{ + "title": "URL 拼接预览", + "invalidUrl": "无效的 URL 格式", + "invalidUrlDesc": "请输入有效的 HTTP/HTTPS 地址", + "duplicatePath": "检测到重复路径", + "copy": "复制", + "copySuccess": "已复制 {name} 到剪贴板", + "copyFailed": "复制失败" +} diff --git a/messages/zh-CN/settings/providers/form/websiteUrl.json b/messages/zh-CN/settings/providers/form/websiteUrl.json new file mode 100644 index 000000000..a149e7750 --- /dev/null +++ b/messages/zh-CN/settings/providers/form/websiteUrl.json @@ -0,0 +1,5 @@ +{ + "label": "供应商官网地址", + "placeholder": "https://example.com", + "desc": "供应商官网地址,用于快速跳转管理" +} diff --git a/messages/zh-CN/settings/providers/guide.json b/messages/zh-CN/settings/providers/guide.json new file mode 100644 index 000000000..691bce245 --- /dev/null +++ b/messages/zh-CN/settings/providers/guide.json @@ -0,0 +1,120 @@ +{ + "priority": "优先级分层选择", + "priorityExample": "有 4 个已启用的供应商,优先级各不相同", + "priorityStep": "系统首先按优先级过滤,只从最高优先级的供应商中选择", + "priorityResult": "筛选出最高优先级(0)的供应商:A, C", + "weight": "使用权重进行随机选择,权重越高被选中概率越大", + "weightExample": "C (权重 3), A (权重 1)", + "weightCalc": "C 被选中概率 75%, A 被选中概率 25%", + "costSort": "成本排序降级", + "costSortExample": "所有供应商:A (default), B (premium), C (premium), D (economy)", + "costSortResult": "排序后:C (0.8x), A (1.0x)", + "costSortProb": "成本更低的 C 有更高的被选中概率", + "session": "会话复用机制", + "sessionDesc": "如果上次使用的供应商不可用,则重新选择", + "sessionExample": "最近一次请求使用了供应商 B", + "sessionLastUsed": "B 可用,直接复用,跳过随机选择", + "sessionExpired": "Session 过期(5 分钟)后自动释放", + "sessionUnavailable": "上次使用的供应商 B 已被禁用或熔断", + "sessionFallback": "从其他可用供应商中选择", + "group": "用户分组过滤", + "groupDesc": "如果用户指定了供应商组,系统会优先从该组中选择", + "groupExample": "用户配置了 providerGroup = 'premium'", + "groupFiltered": "只从 A 和 C 中选择,B 和 D 被过滤", + "groupFallback": "如果用户组内没有可用供应商,降级到所有供应商", + "groupUnavailable": "用户组 'vip' 内的供应商全部禁用或超限", + "groupDowngrade": "记录警告并从全局供应商池中选择", + "health": "健康度过滤(熔断器 + 限流)", + "healthCheck": "检查 B 是否启用且健康", + "healthCheckCircuit": "供应商 A 连续失败 5 次,熔断器状态:open", + "healthCheckConcurrent": "检查当前活跃 Session 数是否超过配置的并发限制", + "healthCheckConcurrentExample": "供应商 C 并发限制 2,当前活跃 Session 数:2", + "healthCheckAmountLimit": "检查 5 小时、7 天、30 天的消费额度是否超限", + "healthCheckAmountLimitExample": "供应商 B 的 5 小时限额 $10,已消耗 $9.8", + "healthFiltered": "B 被过滤(接近限额),剩余:C, D", + "healthFiltered2": "C 被过滤(已满),剩余:D", + "randomSelect": "加权随机", + "randomResult": "最终随机选择了 C", + "history": "检查历史请求", + "historyDesc": "查询该 API Key 最近 10 秒内使用的供应商", + "circuitBreaker": "熔断器检查", + "circuitBreakerOpen": "A 被过滤,剩余:B, C, D", + "circuitBreakerRecovery": "A 在 60 秒后自动恢复到半开状态", + "circuitBreakerRecovery5h": "5 小时窗口滑动后自动恢复", + "reset": "手动解除熔断", + "resetSuccess": "熔断器已重置", + "title": "核心原则", + "priorityFirst": "1. 优先级优先:只从最高优先级(数值最小)的供应商中选择", + "costOptimize": "2. 成本优化:同优先级内,成本倍率低的供应商有更高概率", + "healthFilter": "3. 健康过滤:自动跳过熔断或超限的供应商", + "sessionReuse": "4. 会话复用:连续对话复用同一供应商,节省上下文成本", + "scenariosTitle": "交互式场景演示", + "bestPracticesTitle": "最佳实践建议", + "bestPracticesPriority": "• 优先级设置:核心供应商设为 0,备用供应商设为 1-3", + "bestPracticesWeight": "• 权重配置:根据供应商容量设置权重(容量大 = 权重高)", + "bestPracticesCost": "• 成本倍率:官方倍率为 1.0,自建服务可设置为 0.8-1.2", + "bestPracticesLimit": "• 限额设置:根据预算设置 5 小时、7 天、30 天限额", + "bestPracticesConcurrent": "• 并发控制:根据供应商 API 限制设置 Session 并发数", + "scenario1Title": "优先级分层选择", + "scenario1Desc": "系统首先按优先级过滤,只从最高优先级的供应商中选择", + "scenario1Step1": "初始状态", + "scenario1Step1Desc": "有 4 个已启用的供应商,优先级各不相同", + "scenario1Step1Before": "供应商 A (优先级 0), B (优先级 1), C (优先级 0), D (优先级 2)", + "scenario1Step1After": "筛选出最高优先级(0)的供应商:A, C", + "scenario1Step1Decision": "只从 A 和 C 中选择,B 和 D 被过滤", + "scenario1Step2": "成本排序", + "scenario1Step2Desc": "在同优先级内,按成本倍率从低到高排序", + "scenario1Step2Before": "A (成本 1.0x), C (成本 0.8x)", + "scenario1Step2After": "排序后:C (0.8x), A (1.0x)", + "scenario1Step2Decision": "成本更低的 C 有更高的被选中概率", + "scenario1Step3": "加权随机", + "scenario1Step3Desc": "使用权重进行随机选择,权重越高被选中概率越大", + "scenario1Step3Before": "C (权重 3), A (权重 1)", + "scenario1Step3After": "C 被选中概率 75%, A 被选中概率 25%", + "scenario1Step3Decision": "最终随机选择了 C", + "scenario2Title": "用户分组过滤", + "scenario2Desc": "如果用户指定了供应商组,系统会优先从该组中选择", + "scenario2Step1": "检查用户分组", + "scenario2Step1Desc": "用户配置了 providerGroup = 'premium'", + "scenario2Step1Before": "所有供应商:A (default), B (premium), C (premium), D (economy)", + "scenario2Step1After": "过滤出 'premium' 组:B, C", + "scenario2Step1Decision": "只从 B 和 C 中选择", + "scenario2Step2": "分组降级", + "scenario2Step2Desc": "如果用户组内没有可用供应商,降级到所有供应商", + "scenario2Step2Before": "用户组 'vip' 内的供应商全部禁用或超限", + "scenario2Step2After": "降级到所有启用的供应商:A, B, C, D", + "scenario2Step2Decision": "记录警告并从全局供应商池中选择", + "scenario3Title": "健康度过滤(熔断器 + 限流)", + "scenario3Desc": "系统自动过滤掉熔断或超限的供应商", + "scenario3Step1": "熔断器检查", + "scenario3Step1Desc": "连续失败 5 次后熔断器打开,60 秒内不可用", + "scenario3Step1Before": "供应商 A 连续失败 5 次,熔断器状态:open", + "scenario3Step1After": "A 被过滤,剩余:B, C, D", + "scenario3Step1Decision": "A 在 60 秒后自动恢复到半开状态", + "scenario3Step2": "金额限流", + "scenario3Step2Desc": "检查 5 小时、7 天、30 天的消费额度是否超限", + "scenario3Step2Before": "供应商 B 的 5 小时限额 $10,已消耗 $9.8", + "scenario3Step2After": "B 被过滤(接近限额),剩余:C, D", + "scenario3Step2Decision": "5 小时窗口滑动后自动恢复", + "scenario3Step3": "并发 Session 限制", + "scenario3Step3Desc": "检查当前活跃 Session 数是否超过配置的并发限制", + "scenario3Step3Before": "供应商 C 并发限制 2,当前活跃 Session 数:2", + "scenario3Step3After": "C 被过滤(已满),剩余:D", + "scenario3Step3Decision": "Session 过期(5 分钟)后自动释放", + "scenario4Title": "会话复用机制", + "scenario4Desc": "连续对话优先使用同一供应商,利用 Claude 的上下文缓存", + "scenario4Step1": "检查历史请求", + "scenario4Step1Desc": "查询该 API Key 最近 10 秒内使用的供应商", + "scenario4Step1Before": "最近一次请求使用了供应商 B", + "scenario4Step1After": "检查 B 是否启用且健康", + "scenario4Step1Decision": "B 可用,直接复用,跳过随机选择", + "scenario4Step2": "复用失效", + "scenario4Step2Desc": "如果上次使用的供应商不可用,则重新选择", + "scenario4Step2Before": "上次使用的供应商 B 已被禁用或熔断", + "scenario4Step2After": "进入正常选择流程", + "scenario4Step2Decision": "从其他可用供应商中选择", + "step": "步骤", + "before": "过滤前:", + "after": "过滤后:", + "decision": "决策:" +} diff --git a/messages/zh-CN/settings/providers/inlineEdit.json b/messages/zh-CN/settings/providers/inlineEdit.json new file mode 100644 index 000000000..1cb7c1ab3 --- /dev/null +++ b/messages/zh-CN/settings/providers/inlineEdit.json @@ -0,0 +1,12 @@ +{ + "save": "保存", + "cancel": "取消", + "saveSuccess": "保存成功", + "saveFailed": "保存失败", + "priorityLabel": "优先级", + "weightLabel": "权重", + "costMultiplierLabel": "成本倍数", + "priorityInvalid": "请输入大于等于 0 的整数", + "weightInvalid": "请输入 1-100 之间的整数", + "costMultiplierInvalid": "请输入大于等于 0 的数字" +} diff --git a/messages/zh-CN/settings/providers/list.json b/messages/zh-CN/settings/providers/list.json new file mode 100644 index 000000000..a63cdf96d --- /dev/null +++ b/messages/zh-CN/settings/providers/list.json @@ -0,0 +1,37 @@ +{ + "priority": "优先级", + "weight": "权重", + "costMultiplier": "成本倍数", + "todayUsageLabel": "今日用量", + "todayUsageCount": "{count} 次", + "circuitBroken": "熔断中", + "officialWebsite": "官网", + "viewFullKey": "查看完整 API Key", + "viewFullKeyDesc": "请妥善保管,不要泄露给他人", + "keyLoading": "加载中...", + "confirmDeleteTitle": "确认删除供应商?", + "confirmDeleteMessage": "确定要删除供应商 \"{name}\" 吗?此操作无法撤销。", + "deleteButton": "删除", + "cancelButton": "取消", + "deleteSuccess": "删除成功", + "deleteSuccessDesc": "供应商 \"{name}\" 已删除", + "deleteFailed": "删除失败", + "deleteError": "操作过程中出现异常", + "unknownError": "未知错误", + "getKeyFailed": "获取密钥失败", + "keyCopied": "密钥已复制到剪贴板", + "copyFailed": "复制失败", + "clipboardUnavailable": "当前环境无法访问剪贴板,请手动选择复制。", + "resetCircuitSuccess": "熔断器已重置", + "resetCircuitSuccessDesc": "供应商 \"{name}\" 的熔断状态已解除", + "resetCircuitFailed": "重置熔断器失败", + "resetUsageTitle": "重置总用量", + "resetUsageSuccess": "总用量已重置", + "resetUsageSuccessDesc": "供应商 \"{name}\" 的总用量已重置", + "resetUsageFailed": "重置总用量失败", + "toggleSuccess": "供应商已{status}", + "toggleSuccessDesc": "供应商 \"{name}\" 状态已更新", + "toggleFailed": "状态切换失败", + "statusEnabled": "启用", + "statusDisabled": "禁用" +} diff --git a/messages/zh-CN/settings/providers/schedulingDialog.json b/messages/zh-CN/settings/providers/schedulingDialog.json new file mode 100644 index 000000000..39cac5504 --- /dev/null +++ b/messages/zh-CN/settings/providers/schedulingDialog.json @@ -0,0 +1,9 @@ +{ + "title": "供应商调度规则说明", + "description": "了解系统如何智能选择上游供应商,确保高可用性和成本优化", + "triggerButton": "调度规则说明", + "step": "步骤", + "before": "过滤前:", + "after": "过滤后:", + "decision": "决策:" +} diff --git a/messages/zh-CN/settings/providers/search.json b/messages/zh-CN/settings/providers/search.json new file mode 100644 index 000000000..6ac1c2cb5 --- /dev/null +++ b/messages/zh-CN/settings/providers/search.json @@ -0,0 +1,7 @@ +{ + "placeholder": "搜索供应商名称、URL、备注...", + "clear": "清除搜索", + "found": "找到 {count} 个匹配的供应商", + "notFound": "未找到匹配的供应商", + "showing": "显示 {filtered} / {total} 个供应商" +} diff --git a/messages/zh-CN/settings/providers/section.json b/messages/zh-CN/settings/providers/section.json new file mode 100644 index 000000000..5283be440 --- /dev/null +++ b/messages/zh-CN/settings/providers/section.json @@ -0,0 +1,5 @@ +{ + "title": "服务商管理", + "leaderboard": "供应商排行榜", + "description": "配置上游服务商的金额限流和并发限制,留空表示无限制。" +} diff --git a/messages/zh-CN/settings/providers/sort.json b/messages/zh-CN/settings/providers/sort.json new file mode 100644 index 000000000..9aee2eff1 --- /dev/null +++ b/messages/zh-CN/settings/providers/sort.json @@ -0,0 +1,8 @@ +{ + "byName": "按名称 (A-Z)", + "byPriority": "按优先级 (高-低)", + "byWeight": "按权重 (高-低)", + "byActualPriority": "按实际选取顺序", + "byCreatedAt": "按创建时间 (新-旧)", + "placeholder": "排序供应商" +} diff --git a/messages/zh-CN/settings/providers/strings.json b/messages/zh-CN/settings/providers/strings.json new file mode 100644 index 000000000..af279a30d --- /dev/null +++ b/messages/zh-CN/settings/providers/strings.json @@ -0,0 +1,47 @@ +{ + "title": "供应商管理", + "description": "配置 API 服务商并维护可用状态。", + "subtitle": "服务商管理", + "subtitleDesc": "配置上游服务商的金额限流和并发限制,留空表示无限制。", + "add": "添加供应商", + "addSuccess": "添加服务商成功", + "addFailed": "添加服务商失败", + "edit": "编辑服务商", + "editFailed": "更新服务商失败", + "delete": "删除供应商", + "deleteFailed": "删除供应商失败", + "clone": "克隆服务商", + "cloneFailed": "复制失败", + "confirmDelete": "确定要删除此供应商吗?", + "notFound": "未找到匹配的供应商", + "scheduling": "调度策略详解", + "schedulingDesc": "了解供应商选择如何进行优先级分层、会话复用、负载均衡和故障转移", + "addProvider": "新增服务商", + "editProvider": "编辑服务商", + "createProvider": "新增服务商", + "updateFailed": "更新服务商失败", + "deleteSuccess": "删除成功", + "confirmDeleteDesc": "确定要删除供应商 \"{name}\" 吗?此操作无法撤销。", + "confirmDeleteProvider": "确认删除供应商?", + "confirmDeleteProviderDesc": "确定要删除服务商\"{name}\"吗?此操作不可恢复。", + "noProviders": "暂无服务商配置", + "noProvidersDesc": "添加你的第一个 API 服务商", + "toggleSuccess": "供应商已{status}", + "toggleSuccessDesc": "供应商 \"{name}\" 状态已更新", + "toggleFailed": "状态切换失败", + "enabledStatus": "启用", + "disabledStatus": "禁用", + "searchResults": "找到 {count} 个匹配的供应商", + "searchNoResults": "未找到匹配的供应商", + "displayCount": "显示 {filtered} / {total} 个供应商", + "todayUsage": "今日用量", + "todayUsageCount": "{count} 次", + "circuitBroken": "熔断中", + "official": "官网", + "viewKey": "查看完整 API Key", + "viewKeyDesc": "请妥善保管,不要泄露给他人", + "keyLoading": "加载中...", + "resetCircuit": "熔断器已重置", + "resetCircuitDesc": "供应商 \"{name}\" 的熔断状态已解除", + "resetCircuitFailed": "重置熔断器失败" +} diff --git a/messages/zh-CN/settings/providers/types.json b/messages/zh-CN/settings/providers/types.json new file mode 100644 index 000000000..c567a7d1b --- /dev/null +++ b/messages/zh-CN/settings/providers/types.json @@ -0,0 +1,26 @@ +{ + "claude": { + "label": "Claude", + "description": "Anthropic 官方 API" + }, + "claudeAuth": { + "label": "Claude Auth", + "description": "Claude 中转服务" + }, + "codex": { + "label": "Codex", + "description": "Codex CLI API" + }, + "gemini": { + "label": "Gemini", + "description": "Google Gemini API" + }, + "geminiCli": { + "label": "Gemini CLI", + "description": "Gemini CLI API" + }, + "openaiCompatible": { + "label": "OpenAI Compatible", + "description": "OpenAI 兼容 API" + } +} diff --git a/messages/zh-CN/settings/requestFilters.json b/messages/zh-CN/settings/requestFilters.json new file mode 100644 index 000000000..73f7f6c06 --- /dev/null +++ b/messages/zh-CN/settings/requestFilters.json @@ -0,0 +1,84 @@ +{ + "nav": "请求过滤", + "title": "请求过滤器", + "description": "配置 Header 删除/覆盖以及 Body 替换规则,在转发到上游前自动脱敏或规范化请求。", + "add": "新增过滤器", + "addSuccess": "创建成功", + "addFailed": "创建失败", + "edit": "编辑过滤器", + "editSuccess": "更新成功", + "editFailed": "更新失败", + "delete": "删除过滤器", + "deleteSuccess": "删除成功", + "deleteFailed": "删除失败", + "enable": "已启用", + "disable": "已禁用", + "confirmDelete": "确定删除过滤器\"{name}\"?", + "empty": "暂无过滤器,点击右上角新增。", + "refreshCache": "刷新缓存", + "refreshSuccess": "缓存已刷新,加载 {count} 条过滤器", + "refreshFailed": "刷新失败", + "dialog": { + "createTitle": "新增过滤器", + "editTitle": "编辑过滤器", + "name": "名称", + "scope": "作用域", + "action": "动作", + "target": "目标字段/路径", + "replacement": "替换值 (可选)", + "description": "描述 (可选)", + "priority": "优先级", + "matchType": "匹配类型", + "matchTypeContains": "包含", + "matchTypeExact": "精确匹配", + "matchTypeRegex": "正则", + "jsonPathPlaceholder": "例如: messages.0.content 或 data.items[0].token", + "targetPlaceholder": "Header 名称或文本/路径", + "replacementPlaceholder": "字符串或 JSON,留空表示删除", + "save": "保存", + "saving": "保存中...", + "validation": { + "fieldRequired": "名称和目标为必填项" + }, + "bindingType": "应用范围", + "bindingGlobal": "所有Provider(全局)", + "bindingProviders": "指定Provider", + "bindingGroups": "Provider分组", + "selectProviders": "选择Provider...", + "selectGroups": "选择分组...", + "searchProviders": "搜索Provider...", + "searchGroups": "搜索分组...", + "noProvidersFound": "未找到Provider", + "noGroupsFound": "未找到分组", + "providersSelected": "已选 {count} 个Provider", + "groupsSelected": "已选 {count} 个分组", + "loading": "加载中...", + "clear": "清空", + "selectAll": "全选" + }, + "table": { + "name": "名称", + "scope": "作用域", + "action": "动作", + "target": "目标", + "replacement": "替换值", + "priority": "优先级", + "apply": "范围", + "status": "状态", + "createdAt": "创建时间", + "actions": "操作" + }, + "scopeLabel": { + "header": "Header", + "body": "Body" + }, + "actionLabel": { + "remove": "删除 Header", + "set": "设置 Header", + "json_path": "JSON 路径替换", + "text_replace": "文本替换" + }, + "applyToAll": "应用于所有请求", + "providers": "供应商", + "groups": "组" +} diff --git a/messages/zh-CN/settings/sensitiveWords.json b/messages/zh-CN/settings/sensitiveWords.json new file mode 100644 index 000000000..20d2fad47 --- /dev/null +++ b/messages/zh-CN/settings/sensitiveWords.json @@ -0,0 +1,55 @@ +{ + "title": "敏感词管理", + "description": "配置敏感词过滤规则,拦截包含敏感内容的请求。", + "section": { + "title": "敏感词列表", + "description": "被敏感词拦截的请求不会转发到上游,也不会计费。支持包含匹配、精确匹配和正则表达式三种模式。" + }, + "add": "添加敏感词", + "addSuccess": "敏感词创建成功", + "addFailed": "创建敏感词失败", + "edit": "编辑敏感词", + "editSuccess": "敏感词更新成功", + "editFailed": "更新敏感词失败", + "delete": "删除敏感词", + "deleteSuccess": "敏感词删除成功", + "deleteFailed": "删除失败", + "enable": "敏感词已启用", + "disable": "敏感词已禁用", + "toggleFailed": "状态切换失败", + "toggleFailedError": "状态切换失败:", + "refreshCache": "刷新缓存", + "refreshCacheSuccess": "缓存刷新成功,已加载 {count} 个敏感词", + "refreshCacheFailed": "刷新缓存失败", + "cacheStats": "缓存统计: 包含({containsCount}) 精确({exactCount}) 正则({regexCount})", + "emptyState": "暂无敏感词,点击右上角\"添加敏感词\"开始配置。", + "confirmDelete": "确定要删除敏感词\"{word}\"吗?", + "dialog": { + "addTitle": "添加敏感词", + "addDescription": "配置敏感词过滤规则,被命中的请求将不会转发到上游。", + "editTitle": "编辑敏感词", + "editDescription": "修改敏感词配置,更改后将自动刷新缓存。", + "wordLabel": "敏感词 *", + "wordPlaceholder": "输入敏感词...", + "wordRequired": "请输入敏感词", + "matchTypeLabel": "匹配类型 *", + "matchTypeContains": "包含匹配 - 文本中包含该词即拦截", + "matchTypeExact": "精确匹配 - 完全匹配该词才拦截", + "matchTypeRegex": "正则表达式 - 支持复杂模式匹配", + "descriptionLabel": "说明", + "descriptionPlaceholder": "可选:添加说明...", + "creating": "创建中...", + "saving": "保存中..." + }, + "table": { + "word": "敏感词", + "matchType": "匹配类型", + "matchTypeContains": "包含匹配", + "matchTypeExact": "精确匹配", + "matchTypeRegex": "正则表达式", + "description": "说明", + "status": "状态", + "createdAt": "创建时间", + "actions": "操作" + } +} diff --git a/messages/zh-CN/settings/strings.json b/messages/zh-CN/settings/strings.json new file mode 100644 index 000000000..b1613de67 --- /dev/null +++ b/messages/zh-CN/settings/strings.json @@ -0,0 +1,22 @@ +{ + "mcpPassthroughConfig": "MCP 透传配置", + "mcpPassthroughConfigNone": "不启用", + "mcpPassthroughConfigMinimax": "Minimax", + "mcpPassthroughConfigGlm": "智谱 GLM", + "mcpPassthroughConfigCustom": "自定义 (预留)", + "mcpPassthroughDesc": "启用后,将 MCP 工具调用透传到指定的 AI 服务商(如 minimax 的图片识别、联网搜索)", + "mcpPassthroughSelect": "透传类型", + "mcpPassthroughNoneLabel": "不启用", + "mcpPassthroughNoneDesc": "不启用 MCP 透传功能(默认)", + "mcpPassthroughMinimaxLabel": "Minimax", + "mcpPassthroughMinimaxDesc": "透传到 minimax MCP 服务(支持图片识别、联网搜索等工具)", + "mcpPassthroughGlmLabel": "智谱 GLM", + "mcpPassthroughGlmDesc": "透传到智谱 GLM MCP 服务(支持图片分析、视频分析等工具)", + "mcpPassthroughCustomLabel": "自定义", + "mcpPassthroughCustomDesc": "透传到自定义 MCP 服务(预留,暂未实现)", + "mcpPassthroughHint": "提示: MCP 透传功能允许 Claude Code 客户端使用第三方 AI 服务商提供的工具能力(如图片识别、联网搜索)", + "mcpPassthroughUrlLabel": "MCP 透传 URL", + "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com", + "mcpPassthroughUrlDesc": "MCP 服务的基础 URL。留空则自动从提供商 URL 提取基础域名", + "mcpPassthroughUrlAuto": "自动提取: {url}" +} diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index 5751f6d32..7fb6ace8b 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -5,7 +5,7 @@ "copied": "已複製" }, "title": { - "costRanking": "消耗排行榜", + "costRanking": "消耗排行", "costRankingDescription": "查看用戶消耗排名,資料每 5 分鐘更新一次", "usageLogs": "使用記錄", "clients": "用戶端", @@ -45,13 +45,13 @@ "stats": { "title": "統計", "requests": "請求", - "cost": "成本", + "cost": "費用", "tokens": "Tokens", "period": { "hour": "最近1小時", - "day": "今天", + "day": "今日", "week": "本週", - "month": "本月" + "month": "當月" } }, "logs": { @@ -64,7 +64,7 @@ "searchProvider": "搜尋供應商...", "noUserFound": "未找到匹配的使用者", "noProviderFound": "未找到匹配的供應商", - "model": "模型", + "model": "Model", "endpoint": "端點", "status": "狀態", "timeRange": "時間範圍", @@ -84,8 +84,8 @@ "minRetryCountPlaceholder": "輸入次數(0 表示不限)", "apply": "套用篩選", "reset": "重設", - "last7days": "近7天", - "last30days": "近30天", + "last7days": "近 7 天", + "last30days": "近 30 天", "customRange": "自訂範圍", "export": "匯出", "exporting": "匯出中...", @@ -105,7 +105,7 @@ "cacheWrite": "快取寫入", "cacheRead": "快取讀取", "cache": "快取", - "cost": "成本", + "cost": "費用", "duration": "耗時", "performance": "效能", "status": "狀態" @@ -114,7 +114,7 @@ "title": "統計匯總", "description": "點擊展開查看當前篩選條件下的彙總統計", "expand": "展開", - "collapse": "收起", + "collapse": "收合", "totalAmount": "總消耗金額", "totalTokens": "總 Token 數", "cacheTokens": "快取 Token", @@ -134,7 +134,7 @@ "nonBilling": "非計費", "skipped": "已跳過", "specialSettings": "特殊設定", - "times": "次", + "times": "次數", "loadedCount": "已載入 {count} 筆記錄", "loadingMore": "載入更多中...", "noMoreData": "已載入全部記錄", @@ -155,7 +155,7 @@ "title": "請求詳情", "statusTitle": "請求詳情 - 狀態碼 {status}", "inProgress": "請求中", - "unknown": "未知", + "unknown": "未知狀態", "success": "請求成功完成", "error": "請求失敗,以下是詳細的錯誤訊息和供應商決策鏈", "processing": "請求正在進行中,尚未完成", @@ -164,7 +164,7 @@ }, "skipped": { "title": "跳過資訊", - "reason": "原因", + "reason": "原因說明", "warmup": "Warmup 搶答(CCH)", "desc": "該請求被識別為 Warmup 探測請求,已由 CCH 直接搶答回應,未轉發上游供應商;不計費、不限流、不計入統計。" }, @@ -174,7 +174,7 @@ "sensitiveWord": "敏感詞攔截", "word": "敏感詞", "matchType": "匹配類型", - "matchTypeContains": "包含匹配", + "matchTypeContains": "包含比對", "matchTypeExact": "精確匹配", "matchTypeRegex": "正規表達式", "matchedText": "匹配內容" @@ -192,7 +192,7 @@ "billingDescription": "系統優先使用請求模型({original})的價格計費。如果價格表中不存在該模型,則使用實際呼叫模型({current})的價格。", "billingModel": "計費模型", "actualModelTooltip": "實際模型: {model}", - "originalModelTooltip": "原始模型: {model}", + "originalModelTooltip": "原始模型:{model}", "billingDescription_original": "目前計費模式:使用重新導向前的原始模型({original})計費", "billingDescription_redirected": "目前計費模式:使用重新導向後的實際模型({current})計費", "billingOriginal": "計費: 原始", @@ -234,30 +234,30 @@ }, "statusCodes": { "not200": "非 200(所有非成功請求)", - "200": "200 (成功)", - "400": "400 (錯誤請求)", - "401": "401 (未授權)", - "429": "429 (限流)", - "500": "500 (服務器錯誤)" + "200": "200(成功)", + "400": "400(錯誤請求)", + "401": "401(未授權)", + "429": "429(限流)", + "500": "500(服務器錯誤)" }, "billingDetails": { "input": "輸入", "output": "輸出", - "cacheWrite5m": "快取寫入 (5m)", - "cacheWrite1h": "快取寫入 (1h)", + "cacheWrite5m": "快取寫入(5m)", + "cacheWrite1h": "快取寫入(1h)", "cacheRead": "快取讀取", "cacheTtl": "快取 TTL", "multiplier": "供應商倍率", "totalCost": "總費用", - "context1m": "1M 上下文", + "context1m": "1M 上下文長度", "context1mPricing": "輸入 >200k 2倍, 輸出 >200k 1.5倍" } }, "leaderboard": { - "title": "成本排行榜", + "title": "費用排行榜", "description": "查看用戶和密鑰的成本統計排名", - "todayTitle": "今日排行榜", - "viewAll": "查看全部", + "todayTitle": "今日排行", + "viewAll": "檢視全部", "userRankings": "使用者排名", "providerRankings": "供應商排名", "modelRankings": "模型排名", @@ -271,40 +271,40 @@ "providerRanking": "供應商排名", "providerCacheHitRateRanking": "供應商快取命中率排行", "modelRanking": "模型排名", - "dailyRanking": "今日", + "dailyRanking": "今天", "weeklyRanking": "本週", - "monthlyRanking": "本月", - "allTimeRanking": "全部" + "monthlyRanking": "當月", + "allTimeRanking": "全期間" }, "dateRange": { - "to": "至", + "to": "到", "prevPeriod": "上一週期", "nextPeriod": "下一週期", "customRange": "自訂日期範圍" }, "columns": { - "rank": "排名", + "rank": "名次", "name": "名稱", "totalCost": "總成本", "totalRequests": "總請求數", - "avgCost": "平均成本", + "avgCost": "平均費用", "lastActive": "最後活躍時間", "user": "使用者", "requests": "請求數", "tokens": "Token 數", "consumedAmount": "消耗金額", "provider": "供應商", - "model": "模型", - "cost": "成本", + "model": "Model", + "cost": "費用", "cacheHitRequests": "快取命中請求數(納入快取命中率計算的請求總數)", "cacheHitRate": "快取命中率", "cacheReadTokens": "快取讀取 Token 數", "totalTokens": "總 Token 數", "cacheCreationConsumedAmount": "快取建立消耗金額", "totalConsumedAmount": "總消耗金額", - "successRate": "成功率", + "successRate": "成功率(%)", "avgResponseTime": "平均回覆時間", - "avgTtfbMs": "平均 TTFB", + "avgTtfbMs": "平均 TTFB(ms)", "avgTokensPerSecond": "平均輸出速率" }, "states": { @@ -336,7 +336,7 @@ "user": "使用者", "key": "金鑰", "provider": "供應商", - "model": "模型", + "model": "Model", "startTime": "開始時間", "lastActivity": "最後活動", "requestCount": "請求數", @@ -345,13 +345,13 @@ "totalCost": "總成本", "totalDuration": "總耗時", "status": "狀態", - "actions": "操作" + "actions": "動作" }, "table": { "count": "共 {count} 個{type} 會話", "active": "活躍", "inactive": "非活躍", - "notCountedInConcurrency": "(不計入並發數)", + "notCountedInConcurrency": "(不計入並發數)", "refreshing": "重新整理中...", "noActiveSessions": "暫無活躍 Session" }, @@ -366,7 +366,7 @@ "overview": "Session 概覽", "overviewDescription": "彙總統計資訊", "providersAndModels": "供應商與模型", - "providersAndModelsDescription": "使用的提供商和模型", + "providersAndModelsDescription": "使用的供應商與模型", "tokenUsage": "Token 使用(總量)", "tokenUsageDescription": "所有請求的累計統計", "costInfo": "成本資訊(總計)", @@ -378,12 +378,12 @@ "totalInput": "總輸入", "totalOutput": "總輸出", "cacheCreation": "快取建立", - "cacheTtlMixed": "混合", + "cacheTtlMixed": "混合模式", "cacheRead": "快取讀取", "total": "總計", "totalFee": "總費用", "providers": "供應商", - "models": "模型", + "models": "Model", "noDetailedData": "暫無詳細資料", "storageTip": "未找到詳細資料。如需查看請求詳情,請檢查環境變數 STORE_SESSION_MESSAGES 是否已設定為 true。注意:啟用後會增加 Redis 記憶體使用,且可能包含敏感資訊。", "clientInfo": "用戶端資訊", @@ -401,7 +401,7 @@ "nextRequest": "下一條" }, "actions": { - "back": "返回", + "back": "返回上一頁", "view": "檢視", "copy": "複製", "download": "下載", @@ -413,7 +413,7 @@ "terminateTitle": "終止會話", "terminateDescription": "確定要終止此會話嗎?終止後會清除供應商綁定,下一次請求將重新選擇供應商。", "sessionIdLabel": "會話 ID:{sessionId}", - "cancel": "取消", + "cancel": "取消操作", "confirmTerminate": "確認終止", "terminating": "終止中...", "terminateSuccess": "會話已成功終止", @@ -423,7 +423,7 @@ "selectAll": "全選", "selectSessionLabel": "選擇會話", "multiSelect": "多選", - "cancelMultiSelect": "取消", + "cancelMultiSelect": "取消多選", "terminateSessionTitle": "終止會話", "terminateSessionDescription": "確定要終止此會話嗎?此操作無法撤銷。", "batchTerminateSuccess": "已終止 {count} 個會話", @@ -433,26 +433,26 @@ "noSelection": "請至少選擇一個會話" }, "codeDisplay": { - "raw": "原始", - "pretty": "美化", + "raw": "原始內容", + "pretty": "格式化", "searchPlaceholder": "搜尋", "expand": "展開", - "collapse": "收起", + "collapse": "摺疊", "themeAuto": "跟隨系統", "themeLight": "淺色", - "themeDark": "深色", + "themeDark": "暗色", "noMatches": "沒有符合結果", "onlyMatches": "僅符合行", "showAll": "顯示全部", "prevPage": "上一頁", "nextPage": "下一頁", "pageInfo": "第 {page} / {total} 頁", - "sseEvent": "事件", + "sseEvent": "事件類型", "sseData": "資料", "hardLimit": { "title": "內容過大", "size": "大小:{sizeMB} MB({sizeBytes} 位元組)", - "maximum": "上限:{maxSizeMB} MB 或 {maxLines} 行", + "maximum": "上限:{maxSizeMB} MB 或 {maxLines} 行(擇一)", "hint": "請下載檔案以查看完整內容。", "download": "下載" } @@ -478,7 +478,7 @@ "orderAsc": "切換為正序(最早的在前)", "orderDesc": "切換為倒序(最新的在前)" }, - "back": "返回", + "back": "返回上一頁", "loadingError": "載入失敗" }, "quotas": { @@ -493,14 +493,14 @@ "columns": { "name": "名稱", "limit": "額度", - "used": "已用", + "used": "已使用", "remaining": "剩餘", "resetTime": "重設時間", "status": "狀態" }, "labels": { "byName": "按名稱", - "byUsageRate": "按使用率" + "byUsageRate": "依使用率" }, "users": { "title": "用戶額度統計", @@ -522,14 +522,14 @@ }, "labels": { "byName": "按名稱", - "byUsageRate": "按使用率" + "byUsageRate": "依使用率" }, "all": "全部", "nav": { "mobileMenuTitle": "導覽選單", "dashboard": "儀表板", "usageLogs": "使用記錄", - "leaderboard": "排行榜", + "leaderboard": "排行", "availability": "可用性監控", "myQuota": "我的額度", "quotasManagement": "額度管理", @@ -548,13 +548,13 @@ "totalCost": "總消費金額", "totalCalls": "總 API 呼叫次數", "timeRange": { - "today": "今天", + "today": "本日", "todayDescription": "今天的使用情況", "7days": "過去 7 天", "7daysDescription": "過去 7 天的使用情況", "30days": "過去 30 天", "30daysDescription": "過去 30 天的使用情況", - "thisMonth": "本月", + "thisMonth": "這個月", "thisMonthDescription": "本月的使用情況", "default": "使用情況" }, @@ -565,7 +565,7 @@ }, "legend": { "selectAll": "全選", - "deselectAll": "清空", + "deselectAll": "取消全選", "selected": "已選" }, "chartMode": { @@ -588,25 +588,25 @@ "name": "名稱", "key": "金鑰", "todayCalls": "今日呼叫", - "todayCost": "今日消耗", + "todayCost": "今日花費", "lastUsed": "最後使用", - "actions": "操作" + "actions": "動作" }, "detailsButton": "詳細資訊", "modelStats": "模型統計", "modelStatsColumns": { - "model": "模型", + "model": "Model", "calls": "呼叫次數", - "cost": "消耗" + "cost": "花費" }, "limitUsage": "額度使用情況", "copyKeyTooltip": "複製完整金鑰", "showKeyTooltip": "顯示完整金鑰", "hideKeyTooltip": "隱藏金鑰", "copyFailed": "複製失敗:", - "timesUnit": "次", + "timesUnit": "次數", "provider": "供應商", - "neverUsed": "未使用", + "neverUsed": "從未使用", "viewLogsTooltip": "查看詳細記錄", "logsButton": "記錄", "emptyState": { @@ -636,7 +636,7 @@ "invalidDate": "請輸入有效日期", "enable": "啟用", "disable": "停用", - "success": "操作成功", + "success": "操作完成", "failed": "操作失敗,請稍後再試" }, "emptyState": { @@ -647,7 +647,7 @@ "addUser": "新增使用者" }, "keyListHeader": { - "todayUsage": "今日用量", + "todayUsage": "今日使用量", "allowedModels": { "label": "允許的模型", "noRestrictions": "允許的模型:無限制" @@ -665,7 +665,7 @@ "secondsAgo": "{count}秒前", "minutesAgo": "{count}分鐘前", "hoursAgo": "{count}小時前", - "daysAgo": "{count}天前" + "daysAgo": "{count} 天前" } }, "addKey": "新增金鑰", @@ -715,12 +715,12 @@ "description": "關閉後,此金鑰僅可用於 API 呼叫,無法登入管理後台" }, "limit5hUsd": { - "label": "5小時消費上限 (USD)", + "label": "5小時消費上限(USD)", "placeholder": "留空表示無限制", "description": "5小時內最大消費金額" }, "limitDailyUsd": { - "label": "每日消費上限 (USD)", + "label": "每日消費上限(USD)", "placeholder": "留空表示無限制", "description": "每日最大消費金額" }, @@ -741,12 +741,12 @@ "description": "每日限額的重設時間(使用系統時區)" }, "limitWeeklyUsd": { - "label": "週消費上限 (USD)", + "label": "週消費上限(USD)", "placeholder": "留空表示無限制", "description": "每週最大消費金額" }, "limitMonthlyUsd": { - "label": "月消費上限 (USD)", + "label": "月消費上限(USD)", "placeholder": "留空表示無限制", "description": "每月最大消費金額" }, @@ -802,7 +802,7 @@ }, "providerGroup": { "label": "供應商分組", - "placeholder": "例如:default 或 premium,economy", + "placeholder": "例如:default 或 premium,economy(多個以逗號分隔)", "description": "使用者供應商分組(預設:default)。default 分組包含所有未設定 groupTag 的供應商。" }, "tags": { @@ -822,17 +822,17 @@ "description": "預設值:${default},範圍:$0.01-$1000" }, "limit5hUsd": { - "label": "5小時消費上限 (USD)", + "label": "5小時消費上限(USD)", "placeholder": "留空表示無限制", "description": "5小時內最大消費金額" }, "limitWeeklyUsd": { - "label": "週消費上限 (USD)", + "label": "週消費上限(USD)", "placeholder": "留空表示無限制", "description": "每週最大消費金額" }, "limitMonthlyUsd": { - "label": "月消費上限 (USD)", + "label": "月消費上限(USD)", "placeholder": "留空表示無限制", "description": "每月最大消費金額" }, @@ -865,7 +865,7 @@ "deleteKeyConfirm": { "title": "確認刪除金鑰", "description": "您確定要刪除金鑰「{name}」嗎?\n{maskedKey}\n此操作無法復原,刪除後所有使用此金鑰的應用程式將無法存取。", - "cancel": "取消", + "cancel": "取消刪除", "confirm": "確認刪除", "confirmLoading": "刪除中...", "errors": { @@ -890,9 +890,9 @@ "description": "即時監控供應商的可用性狀態和效能指標", "nav": "可用性監控", "status": { - "green": "正常", + "green": "良好", "red": "異常", - "unknown": "未知" + "unknown": "不明" }, "statusDescription": { "green": "服務正常,請求成功", @@ -902,7 +902,7 @@ "metrics": { "systemAvailability": "系統可用性", "totalRequests": "總請求數", - "successRate": "成功率", + "successRate": "成功率(%)", "avgLatency": "平均延遲", "p50Latency": "P50 延遲", "p95Latency": "P95 延遲", @@ -916,7 +916,7 @@ "last1h": "最近 1 小時", "last6h": "最近 6 小時", "last24h": "最近 24 小時", - "last7d": "最近 7 天", + "last7d": "近 7 天", "custom": "自訂" }, "filters": { @@ -925,8 +925,8 @@ "includeDisabled": "包含已停用" }, "sort": { - "label": "排序", - "availability": "可用性", + "label": "排序方式", + "availability": "可用度", "name": "名稱", "requests": "請求數" }, @@ -934,12 +934,12 @@ "provider": "供應商", "type": "類型", "status": "狀態", - "availability": "可用性", + "availability": "可用度", "requests": "請求數", - "successRate": "成功率", + "successRate": "成功率(%)", "avgLatency": "平均延遲", "lastRequest": "最後請求", - "actions": "操作" + "actions": "動作" }, "chart": { "title": "可用性趨勢", @@ -972,14 +972,14 @@ "fetchFailed": "取得可用性資料失敗" }, "legend": { - "green": "優秀 (可用性 95%+)", - "lime": "良好 (可用性 80-95%)", - "orange": "警告 (可用性 50-80%)", - "red": "異常 (可用性 <50%)", + "green": "優秀(可用性 95%+)", + "lime": "良好(可用性 80-95%)", + "orange": "警告(可用性 50-80%)", + "red": "異常(可用性 <50%)", "noData": "無資料" }, "summary": { - "title": "可用性摘要", + "title": "可用度摘要", "healthyProviders": "健康供應商", "unhealthyProviders": "異常供應商", "unknownProviders": "無資料", @@ -1003,7 +1003,7 @@ "loading": "載入中...", "error": "載入失敗", "totalEvents": "總事件數", - "avgUsage": "平均使用率", + "avgUsage": "平均使用率(%)", "affectedUsers": "受影響使用者數", "noData": "無資料", "noDataHint": "在選定的時間範圍內沒有限流事件", @@ -1039,7 +1039,7 @@ "description": "不同限流類型的事件占比", "total": "總計", "count": "事件數", - "percentage": "占比", + "percentage": "佔比", "noData": "無資料", "types": { "rpm": "RPM 限流", @@ -1054,10 +1054,10 @@ "title": "受影響使用者排行", "description": "觸發限流最多的使用者列表", "total": "總計", - "rank": "排名", + "rank": "名次", "username": "使用者名稱", "eventCount": "事件數", - "percentage": "占比", + "percentage": "佔比", "loading": "載入中...", "noData": "無資料" } @@ -1073,26 +1073,26 @@ "allTags": "所有標籤", "keyGroupFilter": "金鑰分組", "allKeyGroups": "所有金鑰分組", - "sortBy": "排序方式", + "sortBy": "排序依據", "sortOrder": "排序順序", "sortByName": "按名稱", "sortByTags": "按標籤", "sortByExpiresAt": "按過期時間", - "sortByRpm": "按RPM限制", + "sortByRpm": "依 RPM 上限", "sortByLimit5h": "按5小時限額", "sortByLimitDaily": "按每日限額", "sortByLimitWeekly": "按週限額", "sortByLimitMonthly": "按月限額", "sortByCreatedAt": "按建立時間", - "ascending": "升序", - "descending": "降序", + "ascending": "由小到大", + "descending": "由大到小", "statusFilter": "狀態篩選", "allStatus": "全部狀態", - "statusActive": "正常", + "statusActive": "啟用", "statusExpired": "已過期", "statusExpiringSoon": "即將過期", "statusEnabled": "已啟用", - "statusDisabled": "已禁用", + "statusDisabled": "已停用", "createUser": "建立使用者", "createKey": "建立 Key" }, @@ -1109,21 +1109,21 @@ "expiresAt": "到期時間", "expiresAtHint": "點擊快速續期", "limitRpm": "RPM 限制", - "limit5h": "5h 限額 (USD)", - "limitDaily": "每日限額 (USD)", - "limitWeekly": "週限額 (USD)", - "limitMonthly": "月限額 (USD)", - "limitTotal": "總限額 (USD)", + "limit5h": "5h 限額(USD)", + "limitDaily": "每日限額(USD)", + "limitWeekly": "週限額(USD)", + "limitMonthly": "月限額(USD)", + "limitTotal": "總限額(USD)", "limitSessions": "並發" }, "keyRow": { "name": "金鑰名稱", "key": "金鑰", "group": "分組", - "todayUsage": "今日用量", - "todayCost": "今日消耗", + "todayUsage": "今日使用量", + "todayCost": "今日花費", "lastUsed": "最後使用", - "actions": "操作", + "actions": "動作", "quotaButton": "查看限額用量", "fields": { "callsLabel": "今日呼叫", @@ -1131,7 +1131,7 @@ } }, "expand": "展開", - "collapse": "收起", + "collapse": "摺疊", "noKeys": "無金鑰", "defaultGroup": "default", "userStatus": { @@ -1153,7 +1153,7 @@ "keyStatsDialog": { "title": "今日模型統計", "columns": { - "model": "模型", + "model": "Model", "calls": "呼叫次數", "cost": "消費金額" }, @@ -1170,9 +1170,9 @@ "retry": "重試", "labels": { "limit5h": "5 小時", - "limitDaily": "每日", + "limitDaily": "每天", "limitWeekly": "每週", - "limitMonthly": "每月", + "limitMonthly": "每個月", "limitTotal": "總計", "limitSessions": "並發" } @@ -1184,14 +1184,14 @@ "neverExpires": "永不過期", "expired": "已過期", "quickOptions": { - "7days": "7 天", - "30days": "30 天", - "90days": "90 天", - "1year": "1 年" + "7days": "7天", + "30days": "30天", + "90days": "90天", + "1year": "1年" }, "customDate": "自訂日期", "enableOnRenew": "同時啟用使用者", - "cancel": "取消", + "cancel": "取消續期", "confirm": "確認續期", "confirming": "續期中...", "success": "續期成功", @@ -1221,15 +1221,15 @@ }, "batchEdit": { "enterMode": "批量編輯", - "exitMode": "退出", + "exitMode": "離開", "selectAll": "全選", "selectedCount": "已選 {users} 位使用者,{keys} 個金鑰", "editSelected": "編輯選取項目", "dialog": { "title": "批量編輯", "description": "將影響 {users} 位使用者,{keys} 個金鑰", - "cancel": "取消", - "next": "下一步", + "cancel": "取消變更", + "next": "繼續", "noFieldEnabled": "請先啟用至少一個要覆蓋的欄位", "noUpdate": "沒有可執行的更新", "noSelection": "請先選擇要批量編輯的使用者或金鑰。" @@ -1239,9 +1239,9 @@ "description": "此操作將更新 {users} 位使用者和 {keys} 個金鑰,操作不可撤銷。", "userFields": "使用者欄位", "keyFields": "金鑰欄位", - "goBack": "返回修改", + "goBack": "返回編輯", "update": "確認更新", - "updating": "更新中..." + "updating": "正在更新..." }, "toast": { "usersUpdated": "已更新 {count} 位使用者", @@ -1265,14 +1265,14 @@ "fields": { "note": "備註", "tags": "標籤", - "rpm": "RPM 限制", - "limit5h": "5h 限額 (USD)", - "limitDaily": "每日限額 (USD)", - "limitWeekly": "週限額 (USD)", - "limitMonthly": "月限額 (USD)" + "rpm": "RPM 上限", + "limit5h": "5h 限額(USD)", + "limitDaily": "每日限額(USD)", + "limitWeekly": "週限額(USD)", + "limitMonthly": "月限額(USD)" }, "placeholders": { - "emptyToClear": "留空表示清空", + "emptyToClear": "留空表示清除", "tagsPlaceholder": "輸入後按 Enter 新增,支援逗號分隔", "emptyNoLimit": "留空表示不限額" } @@ -1281,11 +1281,11 @@ "title": "金鑰設定", "affected": "將影響 {count} 個金鑰", "fields": { - "providerGroup": "分組 (providerGroup)", - "limit5h": "5h 限額 (USD)", - "limitDaily": "每日限額 (USD)", - "limitWeekly": "週限額 (USD)", - "limitMonthly": "月限額 (USD)", + "providerGroup": "分組(providerGroup)", + "limit5h": "5h 限額(USD)", + "limitDaily": "每日限額(USD)", + "limitWeekly": "週限額(USD)", + "limitMonthly": "月限額(USD)", "canLoginWebUi": "允許登入 Web UI", "keyEnabled": "Key 啟用狀態" }, @@ -1340,7 +1340,7 @@ }, "alreadySet": "已設定", "confirmAdd": "新增", - "cancel": "取消" + "cancel": "取消設定" }, "quickExpire": { "oneWeek": "一週後", @@ -1388,7 +1388,7 @@ "loading": "刪除中..." }, "actions": { - "cancel": "取消" + "cancel": "取消操作" }, "errors": { "enableFailed": "啟用使用者失敗,請稍後重試", @@ -1425,7 +1425,7 @@ "keyStatus": { "enabled": "啟用", "disabled": "停用", - "active": "正常", + "active": "啟用中", "expired": "已過期", "expiringSoon": "即將過期", "keyEnabled": "密鑰已啟用", @@ -1439,7 +1439,7 @@ "userStatus": { "enabled": "啟用", "disabled": "停用", - "active": "正常", + "active": "啟用中", "expired": "已過期", "expiringSoon": "即將過期", "userEnabled": "使用者已啟用", @@ -1482,7 +1482,7 @@ "customPlaceholder": "輸入自訂模式(如:'xcode', 'my-ide')" }, "allowedModels": { - "label": "模型限制", + "label": "Model 限制", "placeholder": "輸入模型名稱或從下拉選單選擇", "description": "限制使用者只能使用指定的 AI 模型。留空表示無限制。" }, @@ -1496,7 +1496,7 @@ "confirmDisableTitle": "確認停用使用者", "confirmEnableDescription": "啟用後該使用者及其金鑰將恢復正常使用", "confirmDisableDescription": "停用後該使用者及其金鑰將無法繼續使用", - "cancel": "取消", + "cancel": "取消變更", "processing": "處理中..." } }, @@ -1512,7 +1512,7 @@ "basicInfo": "基本資訊", "expireTime": "過期時間", "limitRules": "限額規則", - "specialFeatures": "特殊功能" + "specialFeatures": "進階功能" }, "fields": { "keyName": { @@ -1530,14 +1530,14 @@ "descriptionEnabled": "啟用後,此金鑰在登入時將進入獨立的個人用量頁面。但不可修改自己金鑰的供應商分組。", "descriptionDisabled": "關閉後,使用者將無法進入個人獨立用量頁面 UI,而是進入受限的 Web UI。" }, - "providerGroup": { - "label": "供應商分組", - "placeholder": "預設:default", - "selectHint": "選擇此 Key 可使用的供應商分組", - "editHint": "已有密鑰的分組不可修改", - "allGroups": "使用全部群組", - "noGroupHint": "default 分組包含所有未設定 groupTag 的供應商" - }, + "providerGroup": { + "label": "供應商分組", + "placeholder": "預設:default", + "selectHint": "選擇此 Key 可使用的供應商分組", + "editHint": "已有密鑰的分組不可修改", + "allGroups": "使用全部群組", + "noGroupHint": "default 分組包含所有未設定 groupTag 的供應商" + }, "cacheTtl": { "label": "Cache TTL 覆寫", "description": "強制為包含 cache_control 的請求設定 Anthropic prompt cache TTL。", @@ -1552,7 +1552,7 @@ "title": "新增限額規則", "actions": { "add": "新增規則", - "remove": "移除" + "remove": "移除項目" }, "daily": { "mode": { @@ -1584,7 +1584,7 @@ }, "migrateButton": "開始遷移", "skipButton": "稍後再說", - "nextButton": "下一步", + "nextButton": "繼續", "goToSettingsButton": "前往設定", "migrating": "正在遷移...", "success": "遷移完成", @@ -1594,10 +1594,10 @@ "detectedWebhooks": "偵測到的 Webhook", "notificationTypes": { "circuit_breaker": "熔斷器告警", - "daily_leaderboard": "每日排行榜", + "daily_leaderboard": "每日排行", "cost_alert": "成本預警" }, - "urlLabel": "Webhook 地址", + "urlLabel": "Webhook URL", "platformLabel": "平台類型", "autoDetected": "自動識別", "selectPlatform": "請選擇平台", diff --git a/messages/zh-TW/index.ts b/messages/zh-TW/index.ts index d5bad5de2..70f0e4ca7 100644 --- a/messages/zh-TW/index.ts +++ b/messages/zh-TW/index.ts @@ -11,7 +11,7 @@ import notifications from "./notifications.json"; import providerChain from "./provider-chain.json"; import providers from "./providers.json"; import quota from "./quota.json"; -import settings from "./settings.json"; +import settings from "./settings"; import ui from "./ui.json"; import usage from "./usage.json"; import users from "./users.json"; diff --git a/messages/zh-TW/myUsage.json b/messages/zh-TW/myUsage.json index 0fa88c279..b72b9ffc3 100644 --- a/messages/zh-TW/myUsage.json +++ b/messages/zh-TW/myUsage.json @@ -1,6 +1,6 @@ { "header": { - "title": "我的用量", + "title": "我的使用量", "welcome": "歡迎,{name}", "logout": "登出", "keyLabel": "金鑰", @@ -12,7 +12,7 @@ "quotaDialog": { "trigger": "查看配額", "title": "配額使用詳情", - "refresh": "刷新", + "refresh": "重新整理", "lastUpdated": "更新於 {time}", "loadFailed": "配額載入失敗", "close": "關閉" @@ -26,7 +26,7 @@ "concurrent": "並發會話", "keyLevel": "金鑰", "userLevel": "使用者", - "unlimited": "不限", + "unlimited": "不限制", "empty": "暫無額度資料" }, "logs": { @@ -35,18 +35,18 @@ "filters": { "startDate": "開始日期", "endDate": "結束日期", - "model": "模型", + "model": "模型(Model)", "status": "狀態碼", - "allModels": "全部模型", + "allModels": "所有模型", "allStatus": "全部狀態", "apply": "套用", - "reset": "重置" + "reset": "重設" }, "loadFailed": "載入日誌失敗", "table": { "time": "時間", - "model": "模型", - "tokens": "Tokens (入/出)", + "model": "模型(Model)", + "tokens": "Tokens(入/出)", "cacheWrite": "快取寫入", "cacheRead": "快取讀取", "cost": "花費", @@ -57,14 +57,14 @@ "prev": "上一頁", "next": "下一頁", "noLogs": "暫無日誌", - "unknownModel": "未知模型", + "unknownModel": "未知的模型", "billingModel": "計費:{model}" }, "expiration": { "title": "到期時間", "keyExpires": "金鑰到期", "userExpires": "使用者到期", - "rpmLimit": "RPM限制", + "rpmLimit": "RPM 限制", "neverExpires": "永不過期", "expired": "已過期", "expiresIn": "剩餘 {time}", @@ -88,27 +88,27 @@ "output": "輸出", "write": "寫入", "read": "讀取", - "modelBreakdown": "按模型", + "modelBreakdown": "依模型", "keyStats": "金鑰", "userStats": "使用者", "noData": "所選時段無資料", - "unknownModel": "未知" + "unknownModel": "不明" }, "accessRestrictions": { "title": "存取限制", - "models": "模型", + "models": "模型(Model)", "clients": "客戶端", "noRestrictions": "無限制" }, "quotaCollapsible": { "title": "配額使用", - "daily": "日", - "monthly": "月", + "daily": "每日", + "monthly": "每月", "total": "總計" }, "logsCollapsible": { "title": "使用記錄", - "lastStatus": "最近: {code} ({time})", + "lastStatus": "最近:{code}({time})", "successRate": "{rate}%", "noData": "無資料" } diff --git a/messages/zh-TW/provider-chain.json b/messages/zh-TW/provider-chain.json index 9ffe2561f..8dd1ee7d2 100644 --- a/messages/zh-TW/provider-chain.json +++ b/messages/zh-TW/provider-chain.json @@ -86,7 +86,7 @@ "remaining": "還有{count}次", "status": "狀態", "alreadyBroken": "已觸發熔斷", - "circuitTriggered": "⚠️ 已觸發熔斷", + "circuitTriggered": "警告:已觸發熔斷", "errorDetails": "錯誤詳情", "systemError": "系統錯誤", "systemErrorFailed": "系統錯誤(第 {attempt} 次嘗試)", @@ -97,7 +97,7 @@ "errorMeaning": "含義: {meaning}", "meaning": "含義", "notCountedInCircuit": "此錯誤不計入供應商熔斷器", - "systemErrorNote": "ℹ️ 此錯誤不計入供應商熔斷器", + "systemErrorNote": "說明:此錯誤不計入供應商熔斷器", "reselection": "重新選擇供應商", "reselect": "重新選擇供應商", "excluded": "已排除: {providers}", diff --git a/messages/zh-TW/settings.json b/messages/zh-TW/settings.json deleted file mode 100644 index 7688f9d7b..000000000 --- a/messages/zh-TW/settings.json +++ /dev/null @@ -1,2132 +0,0 @@ -{ - "clientVersions": { - "description": "管理用戶端版本要求,確保使用者使用最新的穩定版本。VSCode 外掛和 CLI 作為獨立用戶端分別管理版本。", - "empty": { - "description": "过去 7 天内没有活跃用戶使用可识别的客户端", - "title": "暂无客户端数据" - }, - "features": { - "activeWindow": "活跃窗口:", - "activeWindowDesc": "仅统计过去 7 天内有请求的用戶", - "autoDetect": "系统会自动检测每种客户端的最新稳定版本(GA 版本)", - "blockOldVersion": "使用旧版本的用戶将收到 HTTP 400 错误,无法继续使用服务", - "errorMessage": "错误提示中包含当前版本和需要升级的版本号", - "gaRule": "判定规则:", - "gaRuleDesc": "当某个版本被 1 个以上用戶使用时,视为 GA 版本", - "recommendation": "推荐做法:", - "recommendationDesc": "先观察下方的版本分布,确认新版本稳定后再启用。", - "title": "功能说明", - "whatHappens": "启用后会发生什么:" - }, - "section": { - "distribution": { - "description": "显示过去 7 天内活跃用戶的客户端版本信息。每种客户端类型独立统计 GA 版本。", - "title": "客户端版本分布" - }, - "settings": { - "description": "启用后,系统将自动检测客户端版本并拦截旧版本用戶的请求。", - "title": "升级提醒设置" - } - }, - "table": { - "currentGA": "当前 GA 版本:", - "internalType": "内部类型:", - "lastActive": "最后活跃时间", - "latest": "最新", - "needsUpgrade": "需升级", - "noUsers": "暂无用戶数据", - "status": "状态", - "unknown": "未知", - "user": "用戶", - "usersCount": "{count} 位用戶", - "version": "当前版本" - }, - "title": "用戶端升級提醒", - "toggle": { - "description": "啟用後,系統將自動偵測用戶端版本並攔截舊版本使用者的請求。", - "disableSuccess": "已關閉用戶端版本檢查", - "enable": "啟用用戶端版本檢查", - "enableSuccess": "已啟用用戶端版本檢查", - "toggleFailed": "狀態切換失敗" - } - }, - "common": { - "cancel": "取消", - "completed": "完成", - "confirm": "確認", - "copied": "金鑰已複製到剪貼簿", - "copy": "複製", - "copyFailed": "複製失敗", - "create": "建立", - "creating": "建立中...", - "delete": "刪除", - "disabled": "停用", - "edit": "編輯", - "empty": "未找到相符的結果", - "enabled": "啟用", - "error": "未知錯誤", - "failed": "失敗", - "loading": "載入中...", - "none": "無(暫無使用者使用此版本)", - "refresh": "重新整理", - "reset": "重置", - "save": "保存", - "saving": "保存中...", - "submit": "提交", - "success": "成功", - "test": "測試", - "testing": "測試中...", - "unlimited": "無限", - "unlimited_desc": "無限制", - "update": "更新", - "updating": "更新中..." - }, - "config": { - "autoCleanup": "自動日誌清理", - "autoCleanupDesc": "定時自動清理歷史日誌資料,釋放資料庫儲存空間。", - "description": "管理系統的基礎參數,影響站台顯示和統計行為。", - "section": { - "siteParams": { - "title": "站台參數", - "description": "設定站台標題、貨幣顯示單位與儀表板統計展示策略。" - }, - "autoCleanup": { - "title": "自動日誌清理", - "description": "定時自動清理歷史日誌資料,釋放資料庫儲存空間。" - } - }, - "form": { - "allowGlobalView": "允許查看全站使用量", - "allowGlobalViewDesc": "關閉後,普通使用者在儀表板僅能查看自己金鑰的使用統計。", - "verboseProviderError": "詳細供應商錯誤資訊", - "verboseProviderErrorDesc": "開啟後,當所有供應商不可用時返回詳細錯誤資訊(包含供應商數量、限流原因等);關閉後僅返回簡潔錯誤碼。", - "interceptAnthropicWarmupRequests": "攔截 Warmup 請求(Anthropic)", - "interceptAnthropicWarmupRequestsDesc": "開啟後,識別到 Claude Code 的 Warmup 探測請求將由 CCH 直接搶答短回應,避免存取上游供應商;該請求會記錄在日誌中,但不計費、不限流、不計入統計。", - "enableThinkingSignatureRectifier": "啟用 thinking 簽名整流器", - "enableThinkingSignatureRectifierDesc": "當 Anthropic 類型供應商返回 thinking 簽名不相容或非法請求等錯誤時,自動移除不相容的 thinking 相關區塊並對同一供應商重試一次(預設開啟)。", - "enableResponseFixer": "啟用回應整流", - "enableResponseFixerDesc": "自動修復上游回應中常見的編碼、SSE 與 JSON 格式問題(預設開啟)。", - "responseFixerFixEncoding": "修復編碼問題", - "responseFixerFixEncodingDesc": "移除 BOM 與空字節,並對無效 UTF-8 做相容處理。", - "responseFixerFixSseFormat": "修復 SSE 格式", - "responseFixerFixSseFormatDesc": "補齊 data: 前綴、統一換行符,並修復常見欄位格式。", - "responseFixerFixTruncatedJson": "修復截斷的 JSON", - "responseFixerFixTruncatedJsonDesc": "補齊未閉合的括號/引號,移除尾隨逗號,必要時補 null。", - "cleanupSchedule": "清理週期", - "cleanupScheduleDesc": "選擇自動清理的執行週期", - "configUpdated": "系統設定已更新,頁面將重新整理以應用貨幣顯示變更。", - "currencyDisplay": "貨幣顯示單位", - "currencyDisplayPlaceholder": "選擇貨幣單位", - "currencyDisplayDesc": "修改後,系統所有頁面和 API 介面的金額顯示將使用對應的貨幣符號(僅修改符號,不進行匯率轉換)。", - "keepDays": "保留天數", - "keepDaysDesc": "清理超過此天數的歷史日誌", - "saveFailed": "保存失敗", - "saveSuccess": "保存成功", - "saveError": "保存失敗", - "saveSettings": "保存設定", - "siteTitle": "站台標題", - "siteTitlePlaceholder": "例如:Claude Code Hub", - "siteTitleRequired": "站台標題不能為空", - "siteTitleDesc": "用於設定瀏覽器分頁標題以及系統預設顯示名稱。", - "enableAutoCleanup": "啟用自動清理", - "enableAutoCleanupDesc": "定時自動清理歷史日誌資料", - "cleanupRetentionDays": "保留天數", - "cleanupRetentionDaysRequired": "保留天數 *", - "cleanupRetentionDaysPlaceholder": "30", - "cleanupRetentionDaysDesc": "超過此天數的日誌將被自動清理(範圍:1-365 天)", - "cleanupScheduleLabel": "執行時間 (Cron)", - "cleanupScheduleRequired": "執行時間 (Cron) *", - "cleanupSchedulePlaceholder": "0 2 * * *", - "cleanupScheduleCronDesc": "Cron 表達式,預設:0 2 * * *(每天凌晨 2 點)", - "cleanupScheduleCronExample": "範例:0 3 * * 0(每週日凌晨 3 點)", - "cleanupBatchSize": "批次大小", - "cleanupBatchSizeRequired": "批次大小 *", - "cleanupBatchSizePlaceholder": "10000", - "cleanupBatchSizeDesc": "每批刪除的記錄數(範圍:1000-100000,建議 10000)", - "saveConfig": "保存設定", - "autoCleanupSaved": "自動清理設定已保存", - "currencies": { - "USD": "$ 美元 (USD)", - "CNY": "¥ 人民幣 (CNY)", - "EUR": "€ 歐元 (EUR)", - "JPY": "¥ 日圓 (JPY)", - "GBP": "£ 英鎊 (GBP)", - "HKD": "HK$ 港幣 (HKD)", - "TWD": "NT$ 新台幣 (TWD)", - "KRW": "₩ 韓元 (KRW)", - "SGD": "S$ 新加坡元 (SGD)" - }, - "billingModelSource": "計費模型來源", - "billingModelSourcePlaceholder": "選擇計費模型來源", - "billingModelSourceDesc": "設定模型重新導向時使用哪個模型進行計費。「重新導向前」使用使用者請求的原始模型計費,「重新導向後」使用實際呼叫的模型計費。", - "billingModelSourceOptions": { - "original": "重新導向前(原始模型)", - "redirected": "重新導向後(實際模型)" - } - }, - "siteSettings": "站台參數", - "siteSettingsDesc": "設定站台標題、貨幣顯示單位與儀表板統計顯示策略。", - "title": "基礎設定" - }, - "data": { - "cleanup": { - "rangeLabel": "清理範圍", - "range": { - "7days": "一週前的日誌 (7 天)", - "30days": "一個月前的日誌 (30 天)", - "90days": "三個月前的日誌 (90 天)", - "180days": "六個月前的日誌 (180 天)" - }, - "rangeDescription": { - "7days": "一週前", - "30days": "一個月前", - "90days": "三個月前", - "180days": "六個月前", - "default": "{days} 天前" - }, - "willClean": "將清理 {range} 的所有日誌記錄", - "button": "清理日誌", - "confirmTitle": "確認清理日誌", - "confirmWarning": "此操作將永久刪除 {range}的所有日誌記錄,且無法復原。", - "previewLoading": "正在統計...", - "previewCount": "將刪除 {count} 筆日誌記錄", - "previewError": "無法取得預覽資訊", - "statisticsRetained": "✓ 統計資料將被保留(用於趨勢分析)", - "logsDeleted": "✗ 日誌詳情將被刪除(請求/回應內容、錯誤資訊等)", - "backupRecommendation": "建議:在清理前先匯出資料庫備份,以防需要還原資料。", - "cancel": "取消", - "confirm": "確認清理", - "cleaning": "正在清理...", - "successMessage": "成功清理 {count} 筆日誌記錄({batches} 批次,耗時 {duration}秒)", - "failed": "清理失敗", - "error": "清理日誌失敗", - "descriptionWarning": "清理歷史日誌資料以釋放資料庫儲存空間。注意:統計資料將被保留,但日誌詳情將被永久刪除。" - }, - "description": "管理資料庫的備份與恢復,支援完整資料匯入匯出和日誌清理。", - "export": { - "button": "匯出資料庫", - "exporting": "正在匯出...", - "successMessage": "資料庫匯出成功!", - "failed": "匯出失敗", - "error": "匯出資料庫失敗", - "descriptionFull": "匯出完整的資料庫備份檔案(.dump 格式),可用於資料遷移或還原。備份檔案使用 PostgreSQL custom format,自動壓縮且相容不同版本的資料庫結構。" - }, - "guide": { - "title": "使用說明與注意事項", - "items": { - "cleanup": { - "title": "日誌清理", - "description": "物理刪除歷史日誌資料,不可恢復。統計資料(statistics 表)將被保留。建議清理前先匯出資料庫備份。" - }, - "format": { - "title": "備份格式", - "description": "使用 PostgreSQL custom format (.dump),自動壓縮且能夠相容不同版本的資料庫結構。" - }, - "overwrite": { - "title": "覆寫模式", - "description": "匯入前會刪除所有現有資料,確保資料庫與備份檔案完全一致。適合完整復原場景。" - }, - "merge": { - "title": "合併模式", - "description": "保留現有資料,嘗試插入備份中的資料。如果存在主鍵衝突可能導致匯入失敗。" - }, - "safety": { - "title": "安全建議", - "description": "在執行匯入操作前,建議先匯出目前資料庫作為備份,避免資料遺失。" - }, - "environment": { - "title": "環境要求", - "description": "此功能需要 Docker Compose 部署環境。本機開發環境可能無法使用。" - } - } - }, - "import": { - "selectFileLabel": "選擇備份檔案", - "fileSelected": "已選擇:{name}({size} MB)", - "fileError": "請選擇 .dump 格式的備份檔案", - "noFileSelected": "請先選擇備份檔案", - "cleanFirstLabel": "清除現有資料(覆蓋模式)", - "cleanFirstDescription": "匯入前刪除所有現有資料,確保資料庫與備份檔案完全一致。如果不勾選,將嘗試合併資料,但可能因主鍵衝突而失敗。", - "button": "匯入資料庫", - "importing": "正在匯入...", - "progressTitle": "匯入進度", - "confirmTitle": "確認匯入資料庫", - "confirmOverwrite": "您選擇了「覆蓋模式」,這將會刪除所有現有資料後匯入備份。", - "confirmMerge": "您選擇了「合併模式」,這將嘗試在保留現有資料的基礎上匯入備份。", - "warningOverwrite": "⚠️ 警告:此操作不可逆,所有目前資料將被永久刪除!", - "warningMerge": "⚠️ 注意:如果存在主鍵衝突,匯入可能會失敗。", - "backupFile": "備份檔案:", - "backupRecommendation": "建議在執行此操作前,先匯出目前資料庫作為備份。", - "cancel": "取消", - "confirm": "確認匯入", - "successMessage": "資料匯入完成!", - "failedMessage": "資料匯入失敗,請查看詳細日誌", - "error": "匯入資料庫失敗", - "streamError": "無法讀取回應串流", - "errorUnknown": "未知錯誤", - "descriptionFull": "從備份檔案還原資料庫。支援 PostgreSQL custom format (.dump) 格式的備份檔案。" - }, - "status": { - "loading": "載入中...", - "error": "取得資料庫狀態失敗", - "retry": "重試", - "connected": "資料庫連線正常", - "unavailable": "資料庫無法使用", - "tables": "{count} 個資料表" - }, - "title": "資料管理", - "section": { - "status": { - "title": "資料庫狀態", - "description": "查看目前資料庫的連線狀態和基本資訊。" - }, - "cleanup": { - "title": "日誌清理", - "description": "清理歷史日誌資料以釋放資料庫儲存空間。注意:統計資料將被保留,但日誌詳情將被永久刪除。" - }, - "export": { - "title": "資料匯出", - "description": "匯出完整的資料庫備份檔案(.dump 格式),可用於資料遷移或還原。" - }, - "import": { - "title": "資料匯入", - "description": "從備份檔案還原資料庫。支援 PostgreSQL custom format (.dump) 格式的備份檔案。" - } - } - }, - "errors": { - "saveSuccess": "儲存成功", - "saveFailed": "儲存失敗", - "saveFailed_error": "儲存設定失敗", - "addSuccess": "新增成功", - "addFailed": "新增服務商失敗", - "editSuccess": "更新成功", - "editFailed": "更新服務商失敗", - "deleteSuccess": "刪除成功", - "deleteFailed": "刪除供應商失敗", - "syncSuccess": "同步成功", - "syncFailed": "同步失敗", - "testFailed": "測試失敗", - "testFailedRetry": "測試失敗,請重試", - "loadFailed": "載入通知設定失敗", - "unknownError": "操作過程中出現異常" - }, - "logs": { - "description": "動態調整系統日誌級別,即時控制日誌輸出詳細程度。", - "subtitle": "日誌級別控制", - "subtitleDesc": "調整後立即生效,無需重新啟動服務。適合生產環境排查問題時使用。", - "section": { - "title": "日誌級別控制", - "description": "調整後立即生效,無需重新啟動服務。" - }, - "levels": { - "fatal": { - "label": "Fatal", - "description": "僅致命錯誤" - }, - "error": { - "label": "Error", - "description": "錯誤訊息" - }, - "warn": { - "label": "Warn", - "description": "警告 + 錯誤" - }, - "info": { - "label": "Info", - "description": "關鍵業務事件 + 警告 + 錯誤(推薦生產環境)" - }, - "debug": { - "label": "Debug", - "description": "除錯資訊 + 所有級別(推薦開發環境)" - }, - "trace": { - "label": "Trace", - "description": "極詳細追蹤 + 所有級別" - } - }, - "form": { - "currentLevel": "目前日誌級別", - "selectLevel": "選擇日誌級別", - "save": "保存設定", - "saving": "保存中...", - "success": "日誌級別已設定為: {level}", - "failed": "設定失敗", - "failedError": "設定日誌級別失敗", - "fetchFailed": "取得日誌級別失敗", - "effectiveImmediately": "調整日誌級別後立即生效,無需重新啟動服務。", - "levelGuideTitle": "日誌級別說明", - "levelGuideFatal": "Fatal/Error: 僅顯示錯誤,日誌最少,適合高負載生產環境", - "levelGuideWarn": "Warn: 包含警告(限流觸發、熔斷器開啟等)+ 錯誤", - "levelGuideInfo": "Info(推薦生產環境): 顯示關鍵業務事件(供應商選擇、Session 復用、價格同步)+ 警告 + 錯誤", - "levelGuideDebug": "Debug(推薦開發環境): 包含詳細除錯資訊,適合排查問題時使用", - "levelGuideTrace": "Trace: 極詳細的追蹤資訊,包含所有細節", - "changeNotice": "目前級別為 {current},點選保存後將切換到 {selected}" - }, - "title": "日誌管理" - }, - "nav": { - "apiDocs": "API 文檔", - "clientVersions": "用戶端升級提醒", - "config": "設定", - "data": "資料管理", - "errorRules": "錯誤規則", - "feedback": "意見回饋", - "docs": "使用文檔", - "logs": "日誌", - "notifications": "訊息推送", - "prices": "價格表", - "providers": "供應商", - "sensitiveWords": "敏感詞", - "requestFilters": "請求過濾" - }, - "notifications": { - "title": "訊息推送", - "description": "設定 Webhook 訊息推送", - "global": { - "title": "通知總開關", - "description": "啟用或停用所有訊息推送功能", - "enable": "啟用訊息推送", - "legacyModeTitle": "相容模式", - "legacyModeDescription": "目前使用舊版單一 URL 推送設定。建立推送目標後將自動切換為多目標模式。" - }, - "targets": { - "title": "推送目標", - "description": "管理推送目標,支援企業微信、飛書、釘釘、Telegram、自訂 Webhook", - "add": "新增目標", - "update": "儲存目標", - "edit": "編輯", - "delete": "刪除", - "deleteConfirmTitle": "刪除推送目標", - "deleteConfirm": "確定要刪除此目標嗎?相關綁定也會被移除。", - "enable": "啟用目標", - "statusEnabled": "已啟用", - "statusDisabled": "已停用", - "lastTestAt": "上次測試", - "lastTestNever": "從未測試", - "lastTestSuccess": "測試成功", - "lastTestFailed": "測試失敗", - "test": "測試", - "testSelectType": "選擇測試類型", - "emptyHint": "尚無推送目標,點擊「新增目標」建立。", - "created": "目標已新增", - "updated": "目標已更新", - "deleted": "目標已刪除", - "bindingsSaved": "綁定已儲存" - }, - "targetDialog": { - "createTitle": "新增推送目標", - "editTitle": "編輯推送目標", - "name": "目標名稱", - "namePlaceholder": "例如:運維群", - "type": "平台類型", - "selectType": "請選擇平台類型", - "enable": "啟用", - "webhookUrl": "Webhook URL", - "webhookUrlPlaceholder": "https://example.com/webhook", - "telegramBotToken": "Telegram Bot Token", - "telegramBotTokenPlaceholder": "例如:123456:ABCDEF...", - "telegramChatId": "Telegram Chat ID", - "telegramChatIdPlaceholder": "例如:-1001234567890", - "dingtalkSecret": "釘釘簽名密鑰", - "dingtalkSecretPlaceholder": "可選,用於簽名", - "customHeaders": "自訂 Headers(JSON)", - "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", - "errors": { - "headersInvalidJson": "Headers 不是有效 JSON", - "headersMustBeObject": "Headers 必須是 JSON 物件", - "headersValueMustBeString": "Headers 的值必須為字串" - }, - "types": { - "wechat": "企業微信", - "feishu": "飛書", - "dingtalk": "釘釘", - "telegram": "Telegram", - "custom": "自訂 Webhook" - }, - "proxy": { - "title": "代理設定", - "toggle": "展開/收起代理設定", - "url": "代理位址", - "urlPlaceholder": "http://127.0.0.1:7890", - "fallbackToDirect": "代理失敗時回退直連" - } - }, - "bindings": { - "title": "綁定", - "noTargets": "尚無可用推送目標", - "bindTarget": "綁定目標", - "enable": "啟用", - "enableType": "啟用此通知", - "advanced": "進階", - "scheduleCron": "Cron 表達式", - "scheduleCronPlaceholder": "例如:0 9 * * *", - "scheduleTimezone": "時區", - "templateOverride": "模板覆蓋", - "editTemplateOverride": "編輯覆蓋", - "templateOverrideTitle": "編輯模板覆蓋", - "boundCount": "已綁定:{count}", - "enabledCount": "已啟用:{count}" - }, - "templateEditor": { - "title": "模板(JSON)", - "placeholder": "請輸入 JSON 模板...", - "jsonInvalid": "JSON 格式不正確", - "placeholders": "佔位符", - "insert": "插入" - }, - "circuitBreaker": { - "title": "熔斷器告警", - "description": "供應商完全熔斷時立即推送告警訊息", - "enable": "啟用熔斷器告警", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "test": "測試連線" - }, - "dailyLeaderboard": { - "title": "每日使用者消費排行榜", - "description": "每天定時發送使用者消費 Top N 排行榜", - "enable": "啟用每日排行榜", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "time": "發送時間", - "timePlaceholder": "09:00", - "timeError": "時間格式錯誤,應為 HH:mm", - "topN": "顯示前 N 名", - "test": "測試連線" - }, - "costAlert": { - "title": "成本預警", - "description": "偵測使用者/供應商消費超過配額閾值時觸發告警", - "enable": "啟用成本預警", - "webhook": "Webhook URL", - "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", - "webhookTypeWeCom": "企業微信", - "webhookTypeFeishu": "飛書", - "webhookTypeUnknown": "未知平台,請使用企業微信或飛書的 Webhook URL", - "threshold": "預警閾值", - "thresholdLabel": "預警閾值: {percent}%", - "thresholdHelp": "當消費達到配額的 {percent}% 時觸發告警", - "interval": "檢查間隔(分鐘)", - "test": "測試連線" - }, - "form": { - "save": "儲存設定", - "saving": "儲存中...", - "loading": "載入中...", - "success": "通知設定已儲存並重新排程任務", - "saveFailed": "儲存失敗", - "saveError": "儲存設定失敗", - "loadError": "載入通知設定失敗", - "webhookRequired": "請先填寫 Webhook URL", - "testSuccess": "測試訊息已發送", - "testFailed": "測試失敗", - "testFailedRetry": "測試失敗,請重試", - "testError": "測試連線失敗", - "testNoResult": "測試成功但未返回結果" - } - }, - "prices": { - "title": "價格表", - "description": "管理平台基礎設定與模型價格", - "section": { - "title": "模型價格", - "description": "管理 AI 模型的價格設定" - }, - "searchPlaceholder": "搜尋模型名稱...", - "filters": { - "all": "全部", - "local": "本地", - "anthropic": "Anthropic", - "openai": "OpenAI", - "vertex": "Vertex" - }, - "badges": { - "local": "本地" - }, - "capabilities": { - "assistantPrefill": "助手預填充", - "computerUse": "電腦使用", - "functionCalling": "函數呼叫", - "pdfInput": "PDF 輸入", - "promptCaching": "Prompt 快取", - "reasoning": "推理", - "responseSchema": "回應 Schema", - "toolChoice": "工具選擇", - "vision": "視覺", - "statusSupported": "支援", - "statusUnsupported": "不支援", - "tooltip": "{label}: {status}" - }, - "sync": { - "button": "同步雲端價格表", - "syncing": "同步中...", - "checking": "檢查衝突...", - "successWithChanges": "價格表更新: 新增 {added} 個,更新 {updated} 個,未變化 {unchanged} 個", - "successNoChanges": "價格表已是最新,無需更新", - "failed": "同步失敗", - "failedError": "同步失敗: {error}", - "failedNoResult": "價格表更新成功但未返回處理結果", - "noModels": "未找到支援的模型價格", - "partialFailure": "部分更新成功,但有 {failed} 個模型失敗", - "failedModels": "失敗模型: {models}", - "skippedConflicts": "跳過 {count} 個手動模型" - }, - "conflict": { - "title": "選擇要覆蓋的衝突項", - "description": "以下模型存在手動維護的價格,勾選後將用 LiteLLM 價格覆蓋,未勾選的保持本地不變", - "searchPlaceholder": "搜尋模型...", - "table": { - "modelName": "模型", - "manualPrice": "手動價格", - "litellmPrice": "LiteLLM 價格", - "action": "操作" - }, - "viewDiff": "查看差異", - "diffTitle": "價格差異對比", - "diff": { - "field": "欄位", - "manual": "手動", - "litellm": "LiteLLM", - "inputPrice": "輸入價格", - "outputPrice": "輸出價格", - "imagePrice": "圖片價格", - "provider": "供應商", - "mode": "類型" - }, - "pagination": { - "showing": "顯示 {from}-{to} 條,共 {total} 條" - }, - "selectedCount": "已選擇 {count}/{total} 個模型", - "noMatch": "未找到符合的模型", - "noConflicts": "無衝突項", - "applyOverwrite": "套用覆蓋", - "applying": "套用中..." - }, - "table": { - "modelName": "模型名稱", - "provider": "提供商", - "capabilities": "能力", - "price": "價格", - "inputPrice": "輸入價格 ($/M)", - "outputPrice": "輸出價格 ($/M)", - "priceInput": "輸入", - "priceOutput": "輸出", - "pricePerRequest": "按次", - "cacheReadPrice": "快取讀取 ($/M)", - "cacheCreationPrice": "快取建立 ($/M)", - "cache5m": "5m", - "cache1h": "1h+", - "copyModelId": "複製模型 ID", - "updatedAt": "更新時間", - "actions": "操作", - "typeChat": "對話", - "typeImage": "圖像生成", - "typeCompletion": "補全", - "typeUnknown": "未知", - "loading": "載入中...", - "noMatch": "未找到符合的模型", - "noDataTitle": "暫無價格資料", - "noDataHint": "系統已內置價格表,請透過上方按鈕同步或更新" - }, - "pagination": { - "showing": "顯示 {from}-{to} 條,共 {total} 條", - "previous": "上一頁", - "next": "下一頁", - "perPageLabel": "每頁", - "perPage": "每頁 {size} 條" - }, - "stats": { - "totalModels": "共 {count} 個模型", - "searchResults": "搜尋到 {count} 個結果", - "lastUpdated": "最後更新: {time}" - }, - "dialog": { - "title": "更新模型價格表", - "description": "選擇包含模型價格資料的 JSON 或 TOML 檔案並上傳", - "selectFile": "點擊選擇 JSON/TOML 檔案或拖曳到此處", - "fileSizeLimit": "檔案大小不能超過 10MB", - "fileSizeLimitSmall": "檔案大小不超過 10MB", - "invalidFileType": "請選擇 JSON 或 TOML 格式的檔案", - "fileTooLarge": "檔案大小超過 10MB 限制", - "upload": "上傳並更新", - "uploading": "上傳中...", - "updatePriceTable": "更新價格表", - "updating": "更新中...", - "selectJson": "選擇檔案", - "updateSuccess": "價格表更新成功,共更新 {count} 個模型", - "updateFailed": "更新失敗", - "systemHasBuiltIn": "系統已內置價格表", - "manualDownload": "你也可以手動下載", - "latestPriceTable": "雲端價格表", - "andUploadViaButton": ",並透過上方按鈕上傳", - "cloudModelCountLoading": "雲端模型數量載入中...", - "cloudModelCountFailed": "雲端模型數量載入失敗", - "supportedModels": "目前支援 {count} 個模型", - "results": { - "title": "更新結果", - "total": "總計: {total} 個模型", - "success": "成功: {success}", - "failed": "失敗: {failed}", - "skipped": "跳過: {skipped}", - "more": " (+{count})", - "details": "詳細資訊", - "viewDetails": "檢視詳細記錄" - } - }, - "addModel": "新增模型", - "editModel": "編輯模型", - "deleteModel": "刪除模型", - "addModelDescription": "手動新增模型價格設定", - "editModelDescription": "編輯模型的價格設定", - "deleteConfirm": "確定要刪除模型 {name} 嗎?此操作無法復原。", - "form": { - "modelName": "模型 ID", - "modelNamePlaceholder": "例如: gpt-5.2-codex", - "modelNameRequired": "模型 ID 為必填", - "displayName": "顯示名稱(選填)", - "displayNamePlaceholder": "例如: GPT-5.2 Codex", - "type": "類型", - "provider": "提供商", - "providerPlaceholder": "例如: openai", - "requestPrice": "按次呼叫價格 ($/request)", - "inputPrice": "輸入價格 ($/M tokens)", - "outputPrice": "輸出價格 ($/M tokens)", - "outputPriceImage": "輸出價格 ($/張圖)", - "cacheReadPrice": "快取讀取價格 ($/M tokens)", - "cacheCreationPrice5m": "快取建立價格(5m,$/M tokens)", - "cacheCreationPrice1h": "快取建立價格(1h+,$/M tokens)" - }, - "drawer": { - "prefillLabel": "搜尋現有模型並預填充", - "prefillEmpty": "未找到匹配的模型", - "prefillFailed": "搜尋失敗", - "promptCachingHint": "僅當模型支援快取時開啟,並配置下方快取價格", - "cachePricingTitle": "快取價格" - }, - "actions": { - "edit": "編輯", - "more": "更多操作", - "delete": "刪除" - }, - "toast": { - "createSuccess": "模型已新增", - "updateSuccess": "模型已更新", - "deleteSuccess": "模型已刪除", - "saveFailed": "儲存失敗", - "deleteFailed": "刪除失敗" - } - }, - "providers": { - "add": "新增供應商", - "addFailed": "新增服務商失敗", - "addProvider": "新增服务商", - "addSuccess": "新增服務商成功", - "autoSort": { - "button": "自動排序優先級", - "dialogTitle": "自動排序供應商優先級", - "dialogDescription": "根據成本倍率自動分配優先級(低成本 = 高優先級)", - "changeCount": "{count} 個供應商將被更新", - "noChanges": "無需更改(已排序)", - "costMultiplierHeader": "成本倍率", - "priorityHeader": "優先級", - "providersHeader": "供應商", - "changesTitle": "變更詳情", - "providerHeader": "供應商", - "priorityChangeHeader": "優先級變更", - "confirm": "應用變更", - "success": "已更新 {count} 個供應商的優先級", - "error": "更新優先級失敗" - }, - "circuitBroken": "熔断中", - "clone": "複製服務商", - "cloneFailed": "複製失敗", - "confirmDelete": "確定要刪除此供應商嗎?", - "confirmDeleteDesc": "确定要删除供应商 \"{name}\" 吗?此操作无法撤销。", - "confirmDeleteProvider": "确认删除供应商?", - "confirmDeleteProviderDesc": "确定要删除服务商\"{name}\"吗?此操作不可恢复。", - "createProvider": "新增服务商", - "delete": "刪除供應商", - "deleteFailed": "刪除供應商失敗", - "deleteSuccess": "删除成功", - "description": "設定 API 服務商並維護可用狀態。", - "disabledStatus": "禁用", - "displayCount": "显示 {filtered} / {total} 个供应商", - "edit": "編輯服務商", - "editFailed": "更新服務商失敗", - "editProvider": "编辑服务商", - "enabledStatus": "启用", - "form": { - "addRedirect": "新增重定向", - "allowAllModels": "✓ 允許所有模型(推薦)", - "apiAddress": "API 地址", - "apiAddressPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic", - "apiAddressRequired": "API 地址 *", - "apiKey": "API 金鑰", - "apiKeyCurrent": "当前密钥:", - "apiKeyLeaveEmpty": "(留空不更改)", - "apiKeyLeaveEmptyDesc": "留空则不更改密钥", - "apiKeyOptional": "留空則不更改金鑰", - "apiKeyPlaceholder": "輸入 API 金鑰", - "apiKeyRequired": "API 密钥 *", - "baseUrl": "基礎 URL", - "baseUrlPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic", - "baseUrlRequired": "請先填寫供應商 URL", - "circuitBreakerConfig": "斷路器設定", - "circuitBreakerConfigSummary": "{failureThreshold} 次失敗 / {openDuration} 分鐘熔斷 / {successThreshold} 次成功恢復 / 每個供應商最多 {maxRetryAttempts} 次嘗試", - "circuitBreakerDesc": "連續失敗時自動熔斷,避免影響整體服務品質", - "clearSearch": "清除搜尋", - "codexInstructions": "Codex Instructions 策略", - "codexInstructionsAuto": "自動 (推薦)", - "codexInstructionsDesc": "(決定調度策略)", - "codexInstructionsForce": "強制官方", - "codexInstructionsKeep": "保留原值", - "codexStrategyAutoDesc": "透传客户端 instructions,400 错误时自动重试官方 prompt", - "codexStrategyAutoLabel": "自动 (推荐)", - "codexStrategyConfig": "Codex Instructions 策略", - "codexStrategyConfigAuto": "自动 (推荐)", - "codexStrategyConfigForce": "强制官方", - "codexStrategyConfigKeep": "透传原样", - "codexStrategyDesc": "控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性", - "codexStrategyForceDesc": "始终使用官方 Codex CLI instructions(约 4000+ 字)", - "codexStrategyForceLabel": "强制官方", - "codexStrategyHint": "提示: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方 instructions,选择\"自动\"或\"强制官方\"策略", - "codexStrategyKeepDesc": "始终透传客户端 instructions,不自动重试(适用于宽松中转站)", - "codexStrategyKeepLabel": "透传原样", - "codexStrategySelect": "策略选择", - "collapseAll": "折叠全部高级配置", - "proxyTest": { - "fillUrlFirst": "請先填寫供應商 URL", - "testFailed": "測試失敗", - "testFailedRetry": "測試失敗,請重試", - "noResult": "測試成功但未返回結果", - "connectionSuccess": "連線成功", - "connectionFailed": "連線失敗", - "viaProxy": "(透過代理)", - "viaDirect": "(直連)", - "responseTime": "回應時間:", - "statusCode": "狀態碼:", - "connectionMethod": "連線方式:", - "proxy": "代理", - "direct": "直連", - "errorType": "錯誤類型:", - "testing": "測試中...", - "testConnection": "測試連線", - "timeoutError": "連線逾時(5秒)。請檢查:\n1. 代理伺服器是否可存取\n2. 代理位址和連接埠是否正確\n3. 代理認證資訊是否正確", - "proxyError": "代理錯誤:", - "networkError": "網路錯誤:" - }, - "apiTest": { - "fillUrlFirst": "請先填寫供應商 URL", - "invalidUrl": "供應商 URL 無效,僅支援 http/https", - "fillKeyFirst": "請先填寫 API 金鑰", - "testFailed": "測試失敗", - "testFailedRetry": "測試失敗,請重試", - "noResult": "測試成功但未返回結果", - "testSuccess": "模型測試成功", - "testApi": "供應商模型測試", - "testing": "測試中...", - "apiFormat": "供應商類型", - "selectApiFormat": "選擇要測試的供應商類型", - "apiFormatDesc": "預設與路由設定同步,除非手動修改", - "formatAnthropicMessages": "Claude (Anthropic Messages API)", - "formatOpenAIChat": "OpenAI Compatible", - "formatOpenAIResponses": "Codex (Response API)", - "testModel": "測試模型", - "testModelDesc": "可手動輸入,留空則使用預設模型", - "requestConfig": "請求配置", - "presetConfig": "預置配置", - "customConfig": "自訂", - "selectPreset": "選擇預置範本", - "presetDesc": "預置範本包含真實 CLI 請求特徵,用於通過中轉服務驗證", - "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", - "customPayloadDesc": "輸入自訂 JSON payload,將覆蓋預設請求主體", - "successContains": "成功檢測詞", - "successContainsPlaceholder": "pong", - "successContainsDesc": "回應需包含此內容才算測試成功", - "model": "模型", - "responseModel": "回應模型", - "responseTime": "回應時間", - "usage": "Token 用量", - "response": "回應內容", - "error": "錯誤訊息", - "unknown": "未知", - "viewDetails": "檢視詳情", - "copySuccess": "已複製到剪貼簿", - "copyFailed": "複製失敗", - "copyResult": "複製結果", - "close": "關閉", - "success": "成功", - "failed": "失敗", - "streamInfo": "串流響應資訊", - "chunksReceived": "已接收區塊", - "streamFormat": "串流格式", - "streamResponse": "串流響應", - "chunksCount": "已接收 {count} 個區塊({format})", - "truncatedPreview": "顯示前 {length} 個字元,複製可查看完整內容", - "truncatedBrief": "顯示前 {length} 個字元,點擊「查看詳情」以查看更多", - "timeout": { - "label": "逾時時間(秒)", - "desc": "測試請求的最大等待時間(5-120 秒)", - "geminiHint": ",Gemini Thinking 模型建議 60 秒以上" - }, - "geminiAuthFallback": { - "warning": "Header 認證失敗,使用了 URL 參數認證", - "desc": "實際代理轉發僅使用 Header 認證,可能導致請求失敗" - }, - "copyFormat": { - "testResult": "測試結果", - "message": "訊息", - "errorDetails": "錯誤詳情" - }, - "disclaimer": { - "title": "注意", - "resultReference": "【重要】因各家供應商情況不同,測試結果僅供參考,不代表實際呼叫效果", - "realRequest": "測試將向供應商發送真實請求,可能消耗少量額度", - "confirmConfig": "請確認供應商 URL、API 金鑰及模型設定正確" - }, - "resultCard": { - "status": { - "green": "可用", - "yellow": "波動", - "red": "不可用" - }, - "dialogTitle": "供應商測試詳情", - "validation": { - "title": "三層驗證詳情", - "http": { - "title": "Tier 1: HTTP 狀態", - "statusCode": "狀態碼", - "passed": "2xx/3xx 成功", - "failed": "4xx/5xx 失敗" - }, - "latency": { - "title": "Tier 2: 延遲閾值", - "actual": "實際延遲", - "passed": "在閾值內", - "failed": "超過閾值" - }, - "content": { - "title": "Tier 3: 內容驗證", - "target": "目標", - "passed": "包含目標字串", - "failed": "未找到目標" - }, - "passed": "通過", - "failed": "失敗", - "timeout": "逾時" - }, - "labels": { - "http": "HTTP", - "latency": "延遲", - "content": "內容", - "model": "模型", - "firstByte": "首位元組", - "totalLatency": "總延遲", - "error": "錯誤", - "responsePreview": "回應預覽" - }, - "timing": { - "title": "時間資訊", - "totalLatency": "總延遲", - "firstByte": "首位元組", - "testedAt": "測試時間" - }, - "tokenUsage": { - "title": "Token 用量", - "input": "輸入", - "output": "輸出", - "cacheCreation": "快取建立", - "cacheRead": "快取讀取" - }, - "streamInfo": { - "title": "串流回應資訊", - "isStreaming": "串流回應", - "chunksCount": "資料區塊數", - "yes": "是", - "no": "否" - }, - "rawResponse": { - "title": "完整回應內容", - "hint": "此處顯示原始回應內容,您可以在此檢查關鍵字是否存在於回應中" - }, - "errorDetails": { - "title": "錯誤詳情", - "type": "錯誤類型" - }, - "copyText": { - "status": "狀態", - "message": "訊息", - "latency": "延遲", - "httpStatus": "HTTP 狀態", - "model": "模型", - "usage": "用量", - "inputOutput": "輸入 {input} / 輸出 {output} tokens", - "response": "回應", - "error": "錯誤", - "testedAt": "測試時間", - "validationDetails": "驗證詳情", - "httpCheck": "HTTP 檢查", - "latencyCheck": "延遲檢查", - "contentCheck": "內容驗證" - }, - "judgment": "判定" - } - }, - "urlPreview": { - "title": "URL 拼接預覽", - "invalidUrl": "無效的 URL 格式", - "invalidUrlDesc": "請輸入有效的 HTTP/HTTPS 地址", - "duplicatePath": "檢測到重複路徑", - "copy": "複製", - "copySuccess": "已複製 {name} 到剪貼簿", - "copyFailed": "複製失敗" - }, - "modelSelect": { - "allowAllModels": "允許所有 {type} 模型", - "selectedCount": "已選擇 {count} 個模型", - "searchPlaceholder": "搜尋模型名稱...", - "loading": "載入中...", - "notFound": "未找到模型", - "selectAll": "全選 ({count})", - "clear": "清空", - "manualAdd": "手動新增模型", - "manualPlaceholder": "輸入模型名稱(例如 gpt-5-turbo)", - "manualDesc": "支援新增任意模型名稱(不限於價格表中的模型)", - "claude": "Claude", - "openai": "OpenAI", - "gemini": "Gemini", - "sourceUpstream": "上游", - "sourceUpstreamDesc": "模型列表來自上游服務商 API", - "sourceFallback": "本地", - "sourceFallbackDesc": "使用本地價格表中的模型列表(上游獲取失敗或不支援)", - "refresh": "重新整理模型列表" - }, - "modelRedirect": { - "currentRules": "目前規則 ({count})", - "addNewRule": "新增規則", - "sourceModel": "使用者請求的模型", - "targetModel": "實際轉發的模型", - "sourcePlaceholder": "例如:claude-sonnet-4-5-20250929", - "targetPlaceholder": "例如:glm-4.6", - "add": "新增", - "sourceEmpty": "來源模型名稱不能為空", - "targetEmpty": "目標模型名稱不能為空", - "alreadyExists": "模型「{model}」已存在重新導向規則", - "description": "將 Claude Code 客戶端請求的模型(如 claude-sonnet-4.5)重新導向到上游供應商實際支援的模型(如 glm-4.6、gemini-pro)。用於成本最佳化或接入第三方 AI 服務。", - "emptyState": "目前無重新導向規則。新增規則後,系統將自動重寫請求中的模型名稱。" - }, - "confirmAdd": "确认添加", - "confirmAddPending": "添加中...", - "confirmUpdate": "确认更新", - "confirmUpdatePending": "更新中...", - "costMultiplier": "成本倍數", - "costMultiplierDesc": "例如: A (成本 1.0x), C (成本 0.8x)", - "costMultiplierLabel": "成本倍率", - "costMultiplierPlaceholder": "1.0", - "deleteButton": "删除", - "enabled": "啟用", - "expandAll": "展开全部高级配置", - "failureThreshold": "失败阈值(次)", - "failureThresholdDesc": "连续失败多少次后触发熔断", - "failureThresholdPlaceholder": "5", - "filterAllProviders": "全部供应商", - "filterByType": "篩選供應商類型", - "filterProvider": "筛选供应商类型", - "group": "分組", - "groupPlaceholder": "例如: premium, economy", - "joinClaudePool": "加入 Claude 调度池", - "joinClaudePoolDesc": "启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度", - "joinClaudePoolHelp": "仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用戶请求 claude-* 模型时,此供应商也会参与调度选择。", - "leaveEmpty": "留空表示無限制", - "limit0Means": "0 表示無限制", - "limit5hLabel": "5小时消费上限 (USD)", - "limitAmount5h": "5 小時消費上限 (USD)", - "limitAmount5hDesc": "例如: 供應商 B 的 5 小時限額 $10,已消耗 $9.8", - "limitAmountMonthly": "月消費上限 (USD)", - "limitAmountWeekly": "週消費上限 (USD)", - "limitConcurrent": "並行 Session 限制", - "limitConcurrentDesc": "例如: 供應商 C 並行限制 2,目前活躍 Session 數:2", - "limitConcurrentLabel": "并发 Session 上限", - "limitMonthlyLabel": "月消费上限 (USD)", - "limitPlaceholder0": "0 表示无限制", - "limitPlaceholderUnlimited": "留空表示无限制", - "limitWeeklyLabel": "周消费上限 (USD)", - "modelRedirects": "模型重定向", - "modelRedirectsAddNew": "添加新规则", - "modelRedirectsCurrentRules": "当前规则 ({count})", - "modelRedirectsDesc": "將 Claude 模型請求重定向到其他供應商支援的模型", - "modelRedirectsEmpty": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。", - "modelRedirectsExists": "模型 \"{model}\" 已存在重定向规则", - "modelRedirectsLabel": "模型重定向配置", - "modelRedirectsOptional": "(可选)", - "modelRedirectsSourceModel": "用戶请求的模型", - "modelRedirectsSourcePlaceholder": "例如: claude-sonnet-4-5-20250929", - "modelRedirectsSourceRequired": "源模型名称不能为空", - "modelRedirectsTargetModel": "实际转发的模型", - "modelRedirectsTargetPlaceholder": "例如: glm-4.6", - "modelRedirectsTargetRequired": "目标模型名称不能为空", - "modelWhitelist": "模型白名单", - "modelWhitelistAllowAll": "允许所有 {type} 模型", - "modelWhitelistAllowAllClause": "允许所有 Claude 模型", - "modelWhitelistAllowAllOpenAI": "允许所有 OpenAI 模型", - "modelWhitelistClear": "清空", - "modelWhitelistDesc": "限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。", - "modelWhitelistLabel": "允许的模型", - "modelWhitelistLoading": "加载中...", - "modelWhitelistManualAdd": "手动添加模型", - "modelWhitelistManualDesc": "支持添加任意模型名称(不限于价格表中的模型)", - "modelWhitelistManualPlaceholder": "输入模型名称(如 gpt-5-turbo)", - "modelWhitelistNotFound": "未找到模型", - "modelWhitelistSearchPlaceholder": "搜索模型名称...", - "modelWhitelistSelectAll": "全选 ({count})", - "modelWhitelistSelected": "已选择 {count} 个模型", - "modelWhitelistSelectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。", - "name": { - "label": "供應商名稱 *", - "placeholder": "例如:智譜" - }, - "namePlaceholder": "輸入供應商名稱", - "openDuration": "熔断时长(分钟)", - "openDurationDesc": "熔断后多久自动进入半开状态", - "openDurationPlaceholder": "30", - "priority": "優先級", - "priorityDesc": "同優先級內,按成本倍率從低到高排序", - "priorityLabel": "优先级", - "priorityPlaceholder": "0", - "providerGroupDesc": "供应商分组标签。只有用戶的 providerGroup 与此值匹配时,该用戶才能使用此供应商。示例:设置为 \"premium\" 表示只供 providerGroup=\"premium\" 的用戶使用", - "providerGroupLabel": "供应商分组", - "providerGroupPlaceholder": "例如: premium, economy", - "providerName": "服务商名称", - "providerNamePlaceholder": "例如: 智谱", - "providerNameRequired": "服务商名称 *", - "providerType": "供應商類型", - "providerTypeDesc": "选择供应商的 API 格式类型。", - "providerTypeDisabledNote": "注:Gemini CLI 和 OpenAI Compatible 类型功能正在开发中,暂不可用", - "proxy": "代理", - "proxyAddressFormats": "支持格式:", - "proxyAddressLabel": "代理地址", - "proxyAddressOptional": "(可选)", - "proxyAddressPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", - "proxyConfig": "代理配置", - "proxyConfigDesc": "配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)", - "proxyConfigNone": "未配置", - "proxyConfigSummary": "已配置代理", - "proxyConfigSummaryFallback": " (启用降级)", - "proxyConfigured": "已設定代理", - "proxyFallback": "代理失敗降級", - "proxyFallbackDesc": "代理連線失敗時自動降級到直連", - "proxyFallbackLabel": "代理失败时降级到直连", - "proxyNotConfigured": "未設定", - "proxyTestButton": "测试连接", - "proxyTestDesc": "测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)", - "proxyTestFailed": "连接失败", - "proxyTestFillUrl": "请先填写供应商 URL", - "proxyTestLabel": "连接测试", - "proxyTestNetworkError": "网络错误: {error}", - "proxyTestProxyError": "代理错误: {error}", - "proxyTestResponseTime": "响应时间: {time}", - "proxyTestResultConnectionMethod": "连接方式: {via}", - "proxyTestResultConnectionMethodDirect": "直连", - "proxyTestResultConnectionMethodProxy": "代理", - "proxyTestResultErrorType": "错误类型: {type}", - "proxyTestResultFailed": "连接失败", - "proxyTestResultMessage": "{message}", - "proxyTestResultResponseTime": "响应时间: {time}ms", - "proxyTestResultStatusCode": "状态码: {code}", - "proxyTestResultSuccess": "连接成功 {via}", - "proxyTestStatusCode": "| 状态码: {code}", - "proxyTestSuccess": "连接成功", - "proxyTestTesting": "测试中...", - "proxyTestTimeout": "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确", - "proxyTestViaDirect": "(直连)", - "proxyTestViaProxy": "(通过代理)", - "proxyUrl": "代理位址", - "proxyUrlPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", - "rateLimitConfig": "限流配置", - "rateLimitConfigNone": "无限制", - "rateLimitConfigSummary": "5h: ${fiveHour}, 周: ${weekly}, 月: ${monthly}, 并发: {concurrent}", - "remark": "備註", - "remarkPlaceholder": "可選:新增說明...", - "removeRedirect": "移除重定向", - "routingConfig": "路由配置", - "routingConfigNone": "未配置", - "routingConfigSummary": "{models} 个模型白名单, {redirects} 个重定向", - "scheduleParams": "调度参数", - "searchClear": "清除搜索", - "searchPlaceholder": "搜尋供應商名稱、URL、備註...", - "selectProviderType": "選擇供應商類型", - "sort": "排序供應商", - "sortByCost": "按成本排序", - "sortByCreated": "按建立時間 (新-舊)", - "sortByName": "按名稱 (A-Z)", - "sortByPriority": "按優先級 (高-低)", - "sortByWeight": "按權重 (高-低)", - "sourceModel": "來源模型名稱", - "sourceModelPlaceholder": "例如: claude-sonnet-4-5-20250929", - "sourceModelRequired": "來源模型名稱不能為空", - "successThreshold": "恢复阈值(次)", - "successThresholdDesc": "半开状态下成功多少次后完全恢复", - "successThresholdPlaceholder": "2", - "targetModel": "目標模型名稱", - "targetModelPlaceholder": "例如: glm-4.6", - "targetModelRequired": "目標模型名稱不能為空", - "testProxy": "測試連線", - "testProxyFailed": "測試代理連線失敗", - "testProxyFailedError": "測試連線失敗:", - "testProxySuccess": "代理連線成功", - "validUrlRequired": "请输入有效的 API 地址", - "websiteUrl": { - "label": "供應商官網", - "placeholder": "https://example.com", - "desc": "供應商官方網站,便於快速跳轉管理" - }, - "websiteUrlDesc": "供应商官网地址,用于快速跳转管理", - "websiteUrlInvalid": "请输入有效的供应商官网地址", - "websiteUrlPlaceholder": "https://example.com", - "weight": "權重", - "weightDesc": "加權隨機概率。同優先級內,權重越高被選中概率越大。", - "weightLabel": "权重", - "weightPlaceholder": "1", - "title": { - "create": "新增供應商", - "edit": "編輯供應商" - }, - "dialogDescription": "設定供應商資訊與進階設定。", - "url": { - "label": "API 位址 *", - "placeholder": "例如:https://open.bigmodel.cn/api/anthropic" - }, - "key": { - "label": "API 金鑰", - "leaveEmpty": "(留空不變更)", - "placeholder": "輸入 API 金鑰", - "leaveEmptyDesc": "留空則不變更金鑰", - "currentKey": "當前金鑰:{key}" - }, - "buttons": { - "expandAll": "展開全部進階設定", - "collapseAll": "收合全部進階設定", - "submit": "確認新增", - "submitting": "新增中...", - "update": "確認更新", - "updating": "更新中...", - "delete": "刪除" - }, - "common": { - "core": "核心" - }, - "sections": { - "routing": { - "title": "路由設定", - "summary": { - "models": "{count} 個允許模型", - "redirects": "{count} 個重定向", - "none": "未設定" - }, - "providerType": { - "label": "供應商類型", - "desc": "(決定調度策略)", - "placeholder": "選擇供應商類型" - }, - "providerTypeDesc": "選擇供應商的 API 格式類型。", - "providerTypeDisabledNote": "註:OpenAI Compatible 類型開發中,暫不可用", - "modelRedirects": { - "label": "模型重定向設定", - "optional": "(選填)" - }, - "joinClaudePool": { - "label": "加入 Claude 調度池", - "desc": "啟用後,此供應商將與 Claude 類型供應商共同參與負載均衡", - "help": "僅當存在映射至 claude-* 的規則時可用。當用戶請求 claude-* 模型時,此供應商也會被納入調度。" - }, - "preserveClientIp": { - "label": "透傳客戶端 IP", - "desc": "向上游轉發 x-forwarded-for / x-real-ip,可能暴露真實來源 IP", - "help": "預設關閉以保護隱私;僅在需要上游感知終端 IP 時開啟。" - }, - "modelWhitelist": { - "title": "模型允許清單", - "desc": "限制此供應商可處理的模型。預設可處理該類型下的所有模型。", - "label": "允許的模型", - "optional": "(選填)", - "allowAll": "✓ 允許所有模型(建議)", - "selectedOnly": "僅允許所選的 {count} 個模型。其他模型將不會被路由到此供應商。", - "moreModels": "+{count} 更多" - }, - "scheduleParams": { - "title": "調度參數", - "priority": { - "label": "優先級", - "placeholder": "0", - "desc": "數值越小,優先級越高(0 最高)。系統只會從最高優先級的供應商中選擇。建議:主力=0,備用=1,緊急備援=2" - }, - "weight": { - "label": "權重", - "placeholder": "1", - "desc": "加權隨機。同一優先級內,權重越高被選中機率越大。例如 1:2:3 約為 16%:33%:50%" - }, - "costMultiplier": { - "label": "成本倍率", - "placeholder": "1.0", - "desc": "成本計算倍率。官方供應商=1.0,便宜 20%=0.8,貴 20%=1.2(最多 4 位小數)" - }, - "group": { - "label": "供應商分組", - "placeholder": "例如:premium, economy", - "desc": "分組標籤。僅供 providerGroup 與此值相符的用戶使用。例:設為「premium」表示僅供 providerGroup=\"premium\" 的用戶使用" - } - }, - "cacheTtl": { - "label": "Cache TTL 覆寫", - "options": { - "inherit": "不覆寫(跟隨客戶端)", - "5m": "5 分鐘", - "1h": "1 小時" - }, - "desc": "強制設定 prompt cache TTL;僅影響包含 cache_control 的請求。" - }, - "context1m": { - "label": "1M 上下文視窗", - "options": { - "inherit": "跟隨客戶端", - "forceEnable": "強制啟用", - "disabled": "停用" - }, - "desc": "設定 1M 上下文視窗支援。僅對 Sonnet 模型生效(claude-sonnet-4-5、claude-sonnet-4)。啟用後將套用階梯定價。" - }, - "codexOverrides": { - "reasoningEffort": { - "label": "推理等級覆寫", - "help": "控制模型在輸出前用於推理的強度(推理 token 數量)。選擇「跟隨客戶端」表示不改寫請求;選擇其他值則強制覆寫 reasoning.effort。注意:none 僅 GPT-5.1 系列支援;xhigh 僅 GPT-5.1-Codex-Max 支援,模型不支援會返回錯誤。", - "options": { - "inherit": "不覆寫(跟隨客戶端)", - "minimal": "minimal(更省、更快)", - "low": "low", - "medium": "medium(預設)", - "high": "high(更強推理)", - "xhigh": "xhigh(僅 GPT-5.1-Codex-Max)", - "none": "none(僅 GPT-5.1)" - } - }, - "reasoningSummary": { - "label": "推理摘要覆寫", - "help": "控制是否回傳推理摘要。auto 回傳精簡摘要,detailed 回傳更詳細摘要;「跟隨客戶端」不改寫 reasoning.summary。", - "options": { - "inherit": "不覆寫(跟隨客戶端)", - "auto": "auto(精簡)", - "detailed": "detailed(更詳細)" - } - }, - "textVerbosity": { - "label": "輸出冗長度覆寫", - "help": "控制模型輸出的詳細程度。low 較精簡,high 較詳細;「跟隨客戶端」不改寫 text.verbosity。", - "options": { - "inherit": "不覆寫(跟隨客戶端)", - "low": "low(精簡)", - "medium": "medium(預設)", - "high": "high(更詳細)" - } - }, - "parallelToolCalls": { - "label": "並行工具呼叫覆寫", - "help": "控制是否允許並行 tool calls。關閉可能降低工具呼叫並發能力;「跟隨客戶端」不改寫 parallel_tool_calls。", - "options": { - "inherit": "不覆寫(跟隨客戶端)", - "true": "強制開啟", - "false": "強制關閉" - } - } - } - }, - "rateLimit": { - "title": "流量限制", - "summary": { - "fiveHour": "5h:${amount}", - "weekly": "週:${amount}", - "monthly": "月:${amount}", - "total": "總:${amount}", - "concurrent": "並發:{count}", - "none": "無限制" - }, - "limit5h": { - "label": "5 小時消費上限 (USD)", - "placeholder": "留空表示無限制" - }, - "limitWeekly": { - "label": "週消費上限 (USD)", - "placeholder": "留空表示無限制" - }, - "limitMonthly": { - "label": "月消費上限 (USD)", - "placeholder": "留空表示無限制" - }, - "limitTotal": { - "label": "總消費上限 (USD)", - "placeholder": "留空表示無限制" - }, - "limitConcurrent": { - "label": "並發 Session 上限", - "placeholder": "0 表示無限制" - } - }, - "circuitBreaker": { - "title": "斷路器設定", - "summary": "{failureThreshold} 次失敗 / {openDuration} 分鐘熔斷 / {successThreshold} 次成功恢復 / 每個供應商最多 {maxRetryAttempts} 次嘗試", - "desc": "連續失敗時自動熔斷,避免影響整體服務品質", - "failureThreshold": { - "label": "失敗閾值(次)", - "placeholder": "5", - "desc": "連續失敗多少次後觸發熔斷" - }, - "openDuration": { - "label": "熔斷時長(分鐘)", - "placeholder": "30", - "desc": "熔斷後多久自動進入半開狀態" - }, - "successThreshold": { - "label": "恢復閾值(次)", - "placeholder": "2", - "desc": "半開狀態下成功多少次後完全恢復" - }, - "maxRetryAttempts": { - "label": "單供應商最大嘗試次數", - "placeholder": "2", - "desc": "包含首次呼叫在內,單一供應商最多嘗試次數,超過即切換。留空使用系統預設值。" - } - }, - "proxy": { - "title": "代理設定", - "summary": { - "configured": "已設定代理", - "fallback": "(啟用降級)", - "none": "未設定" - }, - "desc": "設定代理伺服器以改善連線(支援 HTTP、HTTPS、SOCKS4、SOCKS5)", - "url": { - "label": "代理位址", - "optional": "(選填)", - "placeholder": "例如:http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", - "formats": "支援格式:" - }, - "fallback": { - "label": "代理失敗時降級為直連", - "desc": "啟用後,代理連線失敗時自動嘗試直接連線供應商" - }, - "test": { - "label": "連線測試", - "desc": "透過代理測試訪問供應商 URL(HEAD 請求,不消耗額度)" - } - }, - "timeout": { - "title": "超時配置", - "summary": "首字: {streaming}s | 串流間隔: {idle}s | 非串流: {nonStreaming}s", - "desc": "設定請求超時時間,0 表示禁用超時", - "streamingFirstByte": { - "label": "串流首字節超時(秒)", - "placeholder": "30", - "desc": "串流請求首字節超時,範圍 1-120 秒,預設 30 秒", - "core": "true" - }, - "streamingIdle": { - "label": "串流靜默期超時(秒)", - "placeholder": "60", - "desc": "串流請求靜默期超時,範圍 60-600 秒,填 0 禁用(防止中途卡住)", - "core": "true" - }, - "nonStreamingTotal": { - "label": "非串流總超時(秒)", - "placeholder": "600", - "desc": "非串流請求總超時,範圍 60-1200 秒,預設 600 秒(10 分鐘)", - "core": "true" - }, - "disableHint": "設為 0 表示禁用該超時(僅用於灰度回退場景,不推薦)" - }, - "apiTest": { - "title": "供應商模型測試", - "summary": "驗證供應商與模型連通性", - "desc": "測試供應商模型是否可用,預設與路由設定中選擇的供應商類型保持一致。", - "testLabel": "供應商模型測試" - }, - "codexStrategy": { - "title": "Codex Instructions 策略", - "summary": { - "auto": "自動(建議)", - "force": "強制官方", - "keep": "原樣透傳" - }, - "desc": "控制如何處理 Codex 請求的 instructions 欄位,影響與上游中轉站的相容性", - "select": { - "label": "策略選擇", - "placeholder": "選擇策略", - "auto": { - "label": "自動(建議)", - "desc": "透傳客戶端 instructions,400 錯誤時自動重試官方 prompt" - }, - "force": { - "label": "強制官方", - "desc": "始終使用官方 Codex CLI instructions(約 4000+ 字)" - }, - "keep": { - "label": "原樣透傳", - "desc": "始終透傳客戶端 instructions,不自動重試(適用於寬鬆中轉站)" - } - }, - "hint": "提示:部分嚴格的 Codex 中轉站(如 88code、foxcode)需要官方 instructions,請選擇「自動」或「強制官方」策略" - }, - "mcpPassthrough": { - "title": "MCP 透傳配置", - "summary": { - "none": "不啟用", - "minimax": "Minimax", - "glm": "智譜 GLM", - "custom": "自定義 (預留)" - }, - "desc": "啟用後,將 MCP 工具調用透傳到指定的 AI 服務商(如 minimax 的圖片識別、聯網搜索)", - "select": { - "label": "透傳類型", - "none": { - "label": "不啟用", - "desc": "不啟用 MCP 透傳功能(默認)" - }, - "minimax": { - "label": "Minimax", - "desc": "透傳到 minimax MCP 服務(支持圖片識別、聯網搜索等工具)" - }, - "glm": { - "label": "智譜 GLM", - "desc": "透傳到智譜 GLM MCP 服務(支持圖片分析、視頻分析等工具)" - }, - "custom": { - "label": "自定義", - "desc": "透傳到自定義 MCP 服務(預留,暫未實現)" - }, - "placeholder": "選擇透傳類型" - }, - "hint": "提示: MCP 透傳功能允許 Claude Code 客戶端使用第三方 AI 服務商提供的工具能力(如圖片識別、聯網搜索)", - "urlLabel": "MCP 透傳 URL", - "urlPlaceholder": "https://api.minimaxi.com", - "urlDesc": "MCP 服務的基礎 URL。留空則自動從提供商 URL 提取基礎域名", - "urlAuto": "自動提取: {url}" - } - }, - "providerTypes": { - "claude": "Claude (Anthropic Messages API)", - "claudeAuth": "Claude (Anthropic Auth Token)", - "codex": "Codex (Response API)", - "gemini": "Gemini (Google Gemini API)", - "geminiCli": "Gemini CLI", - "geminiCliDisabled": "", - "openaiCompatible": "OpenAI Compatible", - "openaiCompatibleDisabled": " - 功能開發中" - }, - "deleteDialog": { - "title": "刪除供應商", - "description": "確定要刪除供應商「{name}」嗎?此操作不可復原。", - "cancel": "取消", - "confirm": "確認刪除" - }, - "failureThresholdConfirmDialog": { - "title": "確認特殊設定", - "descriptionDisabledPrefix": "您將熔斷失敗閾值設定為 ", - "descriptionDisabledValue": "0", - "descriptionDisabledMiddle": ",這表示", - "descriptionDisabledAction": "停用熔斷器", - "descriptionDisabledSuffix": ",供應商將不會因為連續失敗而被熔斷。", - "descriptionHighValuePrefix": "您將熔斷失敗閾值設定為 ", - "descriptionHighValueSuffix": ",這是一個較高的值,可能會導致供應商在大量失敗後才被熔斷。", - "confirmQuestion": "是否確認儲存此設定?", - "cancel": "取消", - "confirm": "確認儲存" - }, - "errors": { - "invalidUrl": "請輸入有效的 API 位址", - "invalidWebsiteUrl": "請輸入有效的供應商官網", - "groupTagTooLong": "分組標籤總長度不能超過 {max} 個字元", - "addFailed": "新增供應商失敗", - "updateFailed": "更新供應商失敗", - "deleteFailed": "刪除供應商失敗" - }, - "success": { - "created": "新增供應商成功", - "createdDesc": "供應商「{name}」已新增" - } - }, - "guide": { - "after": "过滤后:", - "before": "过滤前:", - "bestPracticesConcurrent": "• 并发控制:根据供应商 API 限制设置 Session 并发数", - "bestPracticesCost": "• 成本倍率:官方倍率为 1.0,自建服务可设置为 0.8-1.2", - "bestPracticesLimit": "• 限额设置:根据预算设置 5 小时、7 天、30 天限额", - "bestPracticesPriority": "• 优先级设置:核心供应商设为 0,备用供应商设为 1-3", - "bestPracticesTitle": "最佳实践建议", - "bestPracticesWeight": "• 权重配置:根据供应商容量设置权重(容量大 = 权重高)", - "circuitBreaker": "熔斷器檢查", - "circuitBreakerOpen": "A 被篩選,剩餘:B, C, D", - "circuitBreakerRecovery": "A 在 60 秒後自動恢復到半開狀態", - "circuitBreakerRecovery5h": "5 小時視窗滑動後自動恢復", - "costOptimize": "2️⃣ 成本优化:同优先级内,成本倍率低的供应商有更高概率", - "costSort": "成本排序降級", - "costSortExample": "所有供應商:A (default), B (premium), C (premium), D (economy)", - "costSortProb": "成本更低的 C 有更高的被選中概率", - "costSortResult": "排序後:C (0.8x), A (1.0x)", - "decision": "决策:", - "group": "使用者分組篩選", - "groupDesc": "如果使用者指定了供應商組,系統會優先從該組中選擇", - "groupDowngrade": "記錄警告並從全域供應商池中選擇", - "groupExample": "使用者設定了 providerGroup = 'premium'", - "groupFallback": "如果使用者組內沒有可用供應商,降級到所有供應商", - "groupFiltered": "只從 A 和 C 中選擇,B 和 D 被篩選", - "groupUnavailable": "使用者組 'vip' 內的供應商全部停用或超限", - "health": "健康度篩選(熔斷器 + 限流)", - "healthCheck": "檢查 B 是否啟用且健康", - "healthCheckAmountLimit": "檢查 5 小時、7 天、30 天的消費額度是否超限", - "healthCheckAmountLimitExample": "供應商 B 的 5 小時限額 $10,已消耗 $9.8", - "healthCheckCircuit": "供應商 A 連續失敗 5 次,熔斷器狀態:open", - "healthCheckConcurrent": "檢查目前活躍 Session 數是否超過設定的並行限制", - "healthCheckConcurrentExample": "供應商 C 並行限制 2,目前活躍 Session 數:2", - "healthFilter": "3️⃣ 健康过滤:自动跳过熔断或超限的供应商", - "healthFiltered": "B 被篩選(接近限額),剩餘:C, D", - "healthFiltered2": "C 被篩選(已滿),剩餘:D", - "history": "檢查歷史請求", - "historyDesc": "查詢該 API Key 最近 10 秒內使用的供應商", - "priority": "優先級分層選擇", - "priorityExample": "有 4 個已啟用的供應商,優先級各不相同", - "priorityFirst": "1️⃣ 优先级优先:只从最高优先级(数值最小)的供应商中选择", - "priorityResult": "篩選出最高優先級(0)的供應商:A, C", - "priorityStep": "系統首先按優先級篩選,只從最高優先級的供應商中選擇", - "randomResult": "最終隨機選擇了 C", - "randomSelect": "加權隨機", - "reset": "手動解除熔斷", - "resetSuccess": "熔斷器已重置", - "scenario1Desc": "系统首先按优先级过滤,只从最高优先级的供应商中选择", - "scenario1Step1": "初始状态", - "scenario1Step1After": "筛选出最高优先级(0)的供应商:A, C", - "scenario1Step1Before": "供应商 A (优先级 0), B (优先级 1), C (优先级 0), D (优先级 2)", - "scenario1Step1Decision": "只从 A 和 C 中选择,B 和 D 被过滤", - "scenario1Step1Desc": "有 4 个已启用的供应商,优先级各不相同", - "scenario1Step2": "成本排序", - "scenario1Step2After": "排序后:C (0.8x), A (1.0x)", - "scenario1Step2Before": "A (成本 1.0x), C (成本 0.8x)", - "scenario1Step2Decision": "成本更低的 C 有更高的被选中概率", - "scenario1Step2Desc": "在同优先级内,按成本倍率从低到高排序", - "scenario1Step3": "加权随机", - "scenario1Step3After": "C 被选中概率 75%, A 被选中概率 25%", - "scenario1Step3Before": "C (权重 3), A (权重 1)", - "scenario1Step3Decision": "最终随机选择了 C", - "scenario1Step3Desc": "使用权重进行随机选择,权重越高被选中概率越大", - "scenario1Title": "优先级分层选择", - "scenario2Desc": "如果用戶指定了供应商组,系统会优先从该组中选择", - "scenario2Step1": "检查用戶分组", - "scenario2Step1After": "过滤出 'premium' 组:B, C", - "scenario2Step1Before": "所有供应商:A (default), B (premium), C (premium), D (economy)", - "scenario2Step1Decision": "只从 B 和 C 中选择", - "scenario2Step1Desc": "用戶配置了 providerGroup = 'premium'", - "scenario2Step2": "分组降级", - "scenario2Step2After": "降级到所有启用的供应商:A, B, C, D", - "scenario2Step2Before": "用戶组 'vip' 内的供应商全部禁用或超限", - "scenario2Step2Decision": "记录警告并从全局供应商池中选择", - "scenario2Step2Desc": "如果用戶组内没有可用供应商,降级到所有供应商", - "scenario2Title": "用戶分组过滤", - "scenario3Desc": "系统自动过滤掉熔断或超限的供应商", - "scenario3Step1": "熔断器检查", - "scenario3Step1After": "A 被过滤,剩余:B, C, D", - "scenario3Step1Before": "供应商 A 连续失败 5 次,熔断器状态:open", - "scenario3Step1Decision": "A 在 60 秒后自动恢复到半开状态", - "scenario3Step1Desc": "连续失败 5 次后熔断器打开,60 秒内不可用", - "scenario3Step2": "金额限流", - "scenario3Step2After": "B 被过滤(接近限额),剩余:C, D", - "scenario3Step2Before": "供应商 B 的 5 小时限额 $10,已消耗 $9.8", - "scenario3Step2Decision": "5 小时窗口滑动后自动恢复", - "scenario3Step2Desc": "检查 5 小时、7 天、30 天的消费额度是否超限", - "scenario3Step3": "并发 Session 限制", - "scenario3Step3After": "C 被过滤(已满),剩余:D", - "scenario3Step3Before": "供应商 C 并发限制 2,当前活跃 Session 数:2", - "scenario3Step3Decision": "Session 过期(5 分钟)后自动释放", - "scenario3Step3Desc": "检查当前活跃 Session 数是否超过配置的并发限制", - "scenario3Title": "健康度过滤(熔断器 + 限流)", - "scenario4Desc": "连续对话优先使用同一供应商,利用 Claude 的上下文缓存", - "scenario4Step1": "检查历史请求", - "scenario4Step1After": "检查 B 是否启用且健康", - "scenario4Step1Before": "最近一次请求使用了供应商 B", - "scenario4Step1Decision": "B 可用,直接复用,跳过随机选择", - "scenario4Step1Desc": "查询该 API Key 最近 10 秒内使用的供应商", - "scenario4Step2": "复用失效", - "scenario4Step2After": "进入正常选择流程", - "scenario4Step2Before": "上次使用的供应商 B 已被禁用或熔断", - "scenario4Step2Decision": "从其他可用供应商中选择", - "scenario4Step2Desc": "如果上次使用的供应商不可用,则重新选择", - "scenario4Title": "会话复用机制", - "scenariosTitle": "交互式场景演示", - "session": "工作階段復用機制", - "sessionDesc": "如果上次使用的供應商不可用,則重新選擇", - "sessionExample": "最近一次請求使用了供應商 B", - "sessionExpired": "Session 過期(5 分鐘)後自動釋放", - "sessionFallback": "從其他可用供應商中選擇", - "sessionLastUsed": "B 可用,直接復用,跳過隨機選擇", - "sessionReuse": "4️⃣ 会话复用:连续对话复用同一供应商,节省上下文成本", - "sessionUnavailable": "上次使用的供應商 B 已被停用或熔斷", - "step": "步骤", - "title": "核心原则", - "weight": "使用權重進行隨機選擇,權重越高被選中概率越大", - "weightCalc": "C 被選中概率 75%, A 被選中概率 25%", - "weightExample": "C (權重 3), A (權重 1)" - }, - "keyLoading": "加载中...", - "noProviders": "暂无服务商配置", - "noProvidersDesc": "添加你的第一个 API 服务商", - "notFound": "未找到相符的供應商", - "official": "官网", - "resetCircuit": "熔断器已重置", - "resetCircuitDesc": "供应商 \"{name}\" 的熔断状态已解除", - "resetCircuitFailed": "重置熔断器失败", - "scheduling": "調度策略詳解", - "schedulingDesc": "了解供應商選擇如何進行優先級分層、工作階段復用、負載均衡和故障轉移", - "searchNoResults": "未找到匹配的供应商", - "searchResults": "找到 {count} 个匹配的供应商", - "section": { - "description": "配置上游服务商的金额限流和并发限制,留空表示无限制。", - "leaderboard": "供應商排行榜", - "title": "服务商管理" - }, - "filter": { - "status": { - "all": "所有狀態", - "active": "已啟用", - "inactive": "已停用" - }, - "groups": { - "label": "分組:", - "all": "全部", - "default": "default" - }, - "circuitBroken": "熔斷" - }, - "subtitle": "服務商管理", - "subtitleDesc": "設定上游服務商的金額限流和並行限制。留空表示無限制。", - "title": "供應商管理", - "todayUsage": "今日用量", - "todayUsageCount": "{count} 次", - "toggleFailed": "状态切换失败", - "toggleSuccess": "供应商已{status}", - "toggleSuccessDesc": "供应商 \"{name}\" 状态已更新", - "updateFailed": "更新服务商失败", - "viewKey": "查看完整 API Key", - "viewKeyDesc": "请妥善保管,不要泄露给他人", - "types": { - "claude": { - "label": "Claude", - "description": "Anthropic 官方 API" - }, - "claudeAuth": { - "label": "Claude Auth", - "description": "Claude 中繼服務" - }, - "codex": { - "label": "Codex", - "description": "Codex CLI API" - }, - "gemini": { - "label": "Gemini", - "description": "Google Gemini API" - }, - "geminiCli": { - "label": "Gemini CLI", - "description": "Gemini CLI API" - }, - "openaiCompatible": { - "label": "OpenAI Compatible", - "description": "OpenAI 相容 API" - } - }, - "list": { - "priority": "優先級", - "weight": "權重", - "costMultiplier": "成本倍數", - "todayUsageLabel": "今日用量", - "todayUsageCount": "{count} 次", - "circuitBroken": "熔斷中", - "officialWebsite": "官網", - "viewFullKey": "查看完整 API Key", - "viewFullKeyDesc": "請妥善保管,不要洩露給他人", - "keyLoading": "載入中...", - "confirmDeleteTitle": "確認刪除供應商?", - "confirmDeleteMessage": "確定要刪除供應商 \"{name}\" 嗎?此操作無法撤銷。", - "deleteButton": "刪除", - "cancelButton": "取消", - "deleteSuccess": "刪除成功", - "deleteSuccessDesc": "供應商 \"{name}\" 已刪除", - "deleteFailed": "刪除失敗", - "deleteError": "操作過程中出現異常", - "unknownError": "未知錯誤", - "getKeyFailed": "獲取密鑰失敗", - "keyCopied": "密鑰已複製到剪貼板", - "copyFailed": "複製失敗", - "clipboardUnavailable": "目前環境無法使用剪貼簿,請手動選取複製。", - "resetCircuitSuccess": "熔斷器已重置", - "resetCircuitSuccessDesc": "供應商 \"{name}\" 的熔斷狀態已解除", - "resetCircuitFailed": "重置熔斷器失敗", - "resetUsageTitle": "重置總用量", - "resetUsageSuccess": "總用量已重置", - "resetUsageSuccessDesc": "供應商 \"{name}\" 的總用量已重置", - "resetUsageFailed": "重置總用量失敗", - "toggleSuccess": "供應商已{status}", - "toggleSuccessDesc": "供應商 \"{name}\" 狀態已更新", - "toggleFailed": "狀態切換失敗", - "statusEnabled": "啟用", - "statusDisabled": "禁用" - }, - "inlineEdit": { - "save": "保存", - "cancel": "取消", - "saveSuccess": "保存成功", - "saveFailed": "保存失敗", - "priorityLabel": "優先級", - "weightLabel": "權重", - "costMultiplierLabel": "成本倍數", - "priorityInvalid": "請輸入大於等於 0 的整數", - "weightInvalid": "請輸入 1-100 之間的整數", - "costMultiplierInvalid": "請輸入大於等於 0 的數字" - }, - "schedulingDialog": { - "title": "供應商調度規則說明", - "description": "了解系統如何智慧選擇上游供應商,確保高可用性和成本優化", - "triggerButton": "調度規則說明", - "step": "步驟", - "before": "過濾前:", - "after": "過濾後:", - "decision": "決策:" - }, - "sort": { - "byName": "按名稱 (A-Z)", - "byPriority": "按優先級 (高-低)", - "byWeight": "按權重 (高-低)", - "byActualPriority": "按實際選取順序", - "byCreatedAt": "按建立時間 (新-舊)", - "placeholder": "排序供應商" - }, - "search": { - "placeholder": "搜尋供應商名稱、URL、備註...", - "clear": "清除搜尋", - "found": "找到 {count} 個匹配的供應商", - "notFound": "未找到匹配的供應商", - "showing": "顯示 {filtered} / {total} 個供應商" - } - }, - "sensitiveWords": { - "add": "新增敏感詞", - "addFailed": "建立敏感詞失敗", - "addSuccess": "敏感詞建立成功", - "cacheStats": "快取統計:包含({containsCount}) 精確({exactCount}) 正則({regexCount})", - "confirmDelete": "確定要刪除敏感詞「{word}」嗎?", - "delete": "刪除敏感詞", - "deleteFailed": "刪除失敗", - "deleteSuccess": "敏感詞刪除成功", - "description": "設定敏感詞過濾規則,攔截包含敏感內容的請求。", - "dialog": { - "addDescription": "配置敏感詞過濾規則,被命中的請求將不會轉發到上游。", - "addTitle": "新增敏感詞", - "creating": "建立中...", - "descriptionLabel": "說明", - "descriptionPlaceholder": "選填:新增說明...", - "editDescription": "修改敏感詞配置,更改後將自動刷新快取。", - "editTitle": "編輯敏感詞", - "matchTypeContains": "包含匹配 - 文字中包含該詞即攔截", - "matchTypeExact": "精確匹配 - 完全匹配該詞才攔截", - "matchTypeLabel": "匹配類型 *", - "matchTypeRegex": "正則表達式 - 支援複雜模式匹配", - "saving": "儲存中...", - "wordLabel": "敏感詞 *", - "wordPlaceholder": "輸入敏感詞...", - "wordRequired": "請輸入敏感詞" - }, - "disable": "敏感詞已停用", - "edit": "編輯敏感詞", - "editFailed": "更新敏感詞失敗", - "editSuccess": "敏感詞更新成功", - "emptyState": "暫無敏感詞,點選右上角「新增敏感詞」開始配置。", - "enable": "敏感詞已啟用", - "refreshCache": "重新整理快取", - "refreshCacheFailed": "刷新快取失敗", - "refreshCacheSuccess": "快取刷新成功,已載入 {count} 個敏感詞", - "section": { - "description": "被敏感詞攔截的請求不會轉發到上游,也不會計費。支援包含匹配、精確匹配和正則表達式三種模式。", - "title": "敏感詞列表" - }, - "table": { - "actions": "操作", - "createdAt": "建立時間", - "description": "說明", - "matchType": "匹配類型", - "matchTypeContains": "包含匹配", - "matchTypeExact": "精確匹配", - "matchTypeRegex": "正則表達式", - "status": "狀態", - "word": "敏感詞" - }, - "title": "敏感詞管理", - "toggleFailed": "狀態切換失敗", - "toggleFailedError": "狀態切換失敗:" - }, - "requestFilters": { - "nav": "請求過濾", - "title": "請求過濾器", - "description": "上游轉發前先刪除/覆寫標頭或替換 Body,實現脫敏與格式化。", - "add": "新增過濾器", - "addSuccess": "建立成功", - "addFailed": "建立失敗", - "edit": "編輯過濾器", - "editSuccess": "更新成功", - "editFailed": "更新失敗", - "delete": "刪除過濾器", - "deleteSuccess": "刪除成功", - "deleteFailed": "刪除失敗", - "enable": "已啟用", - "disable": "已停用", - "confirmDelete": "確定刪除過濾器「{name}」?", - "empty": "尚無過濾器,點選右上角新增。", - "refreshCache": "刷新快取", - "refreshSuccess": "快取已刷新,載入 {count} 條過濾器", - "refreshFailed": "刷新失敗", - "dialog": { - "createTitle": "新增過濾器", - "editTitle": "編輯過濾器", - "name": "名稱", - "scope": "作用域", - "action": "動作", - "target": "目標欄位/路徑", - "replacement": "替換值 (選填)", - "description": "描述 (選填)", - "priority": "優先級", - "matchType": "匹配類型", - "matchTypeContains": "包含", - "matchTypeExact": "精確", - "matchTypeRegex": "正則", - "jsonPathPlaceholder": "例如: messages.0.content 或 data.items[0].token", - "targetPlaceholder": "Header 名稱或文字/路徑", - "replacementPlaceholder": "字串或 JSON,留空為清除", - "save": "保存", - "saving": "保存中...", - "validation": { - "fieldRequired": "名稱和目標為必填項" - }, - "bindingType": "套用範圍", - "bindingGlobal": "所有Provider(全域)", - "bindingProviders": "指定Provider", - "bindingGroups": "Provider分組", - "selectProviders": "選擇Provider...", - "selectGroups": "選擇分組...", - "searchProviders": "搜尋Provider...", - "searchGroups": "搜尋分組...", - "noProvidersFound": "找不到Provider", - "noGroupsFound": "找不到分組", - "providersSelected": "已選 {count} 個Provider", - "groupsSelected": "已選 {count} 個分組", - "loading": "載入中...", - "clear": "清空", - "selectAll": "全選" - }, - "table": { - "name": "名稱", - "scope": "作用域", - "action": "動作", - "target": "目標", - "replacement": "替換值", - "priority": "優先級", - "apply": "範圍", - "status": "狀態", - "createdAt": "建立時間", - "actions": "操作" - }, - "scopeLabel": { - "header": "Header", - "body": "Body" - }, - "actionLabel": { - "remove": "刪除 Header", - "set": "設定 Header", - "json_path": "JSON 路徑替換", - "text_replace": "文字替換" - }, - "applyToAll": "應用於所有請求", - "providers": "供應商", - "groups": "群組" - }, - "errorRules": { - "nav": "錯誤規則", - "title": "錯誤規則管理", - "description": "管理不需要自動重試的客戶端錯誤規則。配置後,命中規則的報錯將直接返回給用戶,不會觸發重試,也不會計入供應商熔斷。", - "section": { - "title": "錯誤規則列表" - }, - "tester": { - "title": "錯誤規則測試", - "description": "輸入錯誤訊息,檢查是否命中已設定的規則以及最終返回的內容。", - "inputLabel": "測試錯誤訊息", - "inputPlaceholder": "輸入要檢測的錯誤訊息...", - "testButton": "開始測試", - "testing": "測試中...", - "matched": "已命中錯誤規則", - "notMatched": "未命中任何規則", - "finalResponse": "覆寫回應", - "ruleInfo": "匹配的規則", - "noRule": "沒有匹配到任何規則", - "category": "規則類別", - "pattern": "匹配模式", - "matchType": "匹配類型", - "overrideStatusCode": "覆寫狀態碼", - "testFailed": "測試失敗,請稍後重試", - "messageRequired": "請輸入要測試的錯誤訊息", - "warnings": "設定警告", - "statusCodeOnlyOverride": "僅覆寫狀態碼,回應內容將使用上游錯誤訊息" - }, - "add": "新增錯誤規則", - "addSuccess": "錯誤規則建立成功", - "addFailed": "建立錯誤規則失敗", - "edit": "編輯錯誤規則", - "editSuccess": "錯誤規則更新成功", - "editFailed": "更新錯誤規則失敗", - "delete": "刪除錯誤規則", - "deleteSuccess": "錯誤規則刪除成功", - "deleteFailed": "刪除失敗", - "enable": "錯誤規則已啟用", - "disable": "錯誤規則已停用", - "toggleFailed": "狀態切換失敗", - "toggleFailedError": "狀態切換失敗:", - "refreshCache": "同步規則", - "refreshCacheSuccess": "規則同步成功,已載入 {count} 個錯誤規則", - "refreshCacheFailed": "同步規則失敗", - "cacheStats": "快取: 共 {totalCount} 條規則", - "emptyState": "目前沒有錯誤規則,點擊右上角「新增錯誤規則」開始設定。", - "confirmDelete": "確定要刪除錯誤規則「{pattern}」嗎?", - "dialog": { - "addTitle": "新增錯誤規則", - "addDescription": "設定錯誤訊息正則表達式模式,符合的錯誤將被識別為不可重試的客戶端錯誤。", - "editTitle": "編輯錯誤規則", - "editDescription": "修改錯誤規則設定,更改後將自動重新整理快取。", - "patternLabel": "正則表達式模式 *", - "patternPlaceholder": "輸入正則表達式...", - "patternRequired": "請輸入正則表達式模式", - "patternHint": "支援 JavaScript 正則表達式語法,例如:prompt is too long|invalid.*request", - "categoryLabel": "規則類別 *", - "categoryPlaceholder": "選擇規則類別", - "categoryRequired": "請選擇規則類別", - "categoryHint": "選擇規則所屬的錯誤類別,用於分類管理和統計", - "descriptionLabel": "說明", - "descriptionPlaceholder": "選填:新增說明...", - "invalidRegex": "正則表達式語法錯誤", - "regexTester": "正則表達式測試器", - "testMessageLabel": "測試訊息", - "testMessagePlaceholder": "輸入要測試的錯誤訊息...", - "matchSuccess": "符合成功", - "matchFailed": "未符合", - "invalidPattern": "無效的正則表達式", - "matchedText": "符合內容", - "defaultRuleHint": "預設規則的模式無法修改", - "enableOverride": "啟用錯誤覆寫", - "enableOverrideHint": "啟用後可自訂返回給客戶端的錯誤回應和狀態碼,原始錯誤仍會記錄到資料庫。當前僅支援 Claude API 錯誤格式。", - "overrideResponseLabel": "覆寫回應(JSON 格式)", - "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"您的自訂訊息\"\n }\n}", - "overrideResponseHint": "留空則僅覆寫狀態碼。", - "overrideStatusCodeLabel": "覆寫狀態碼(選填)", - "overrideStatusCodePlaceholder": "例如 400", - "overrideStatusCodeHint": "留空則使用上游狀態碼。範圍:400-599。", - "useTemplate": "Claude Error 範本", - "useTemplateConfirm": "輸入框已有內容,確定以範本覆蓋?", - "validJson": "JSON 格式正確", - "invalidJson": "JSON 格式無效", - "invalidStatusCode": "狀態碼必須在 400-599 範圍內", - "creating": "建立中...", - "saving": "儲存中..." - }, - "table": { - "pattern": "正則表達式模式", - "category": "規則類別", - "description": "說明", - "status": "狀態", - "default": "預設", - "isEnabled": "啟用狀態", - "isDefault": "預設規則", - "createdAt": "建立時間", - "actions": "操作" - }, - "form": { - "fields": { - "pattern": "正則表達式模式", - "category": "規則類別", - "description": "說明" - }, - "placeholders": { - "pattern": "例如: prompt is too long", - "category": "選擇類別", - "description": "選填:新增說明..." - }, - "labels": { - "pattern": "正則表達式模式 *", - "category": "規則類別 *", - "description": "說明", - "isEnabled": "啟用狀態" - } - }, - "actions": { - "add": "新增", - "edit": "編輯", - "delete": "刪除", - "refresh": "重新整理", - "test": "測試", - "messages": { - "success": "操作成功", - "error": "操作失敗" - } - }, - "validation": { - "patternRequired": "請輸入正則表達式模式", - "categoryRequired": "請選擇規則類別", - "patternInvalid": "正則表達式語法錯誤", - "redosRisk": "正則表達式存在 ReDoS 風險,請簡化模式", - "patternTooComplex": "正則表達式過於複雜" - }, - "categories": { - "prompt_limit": "Prompt 長度限制", - "content_filter": "內容過濾", - "pdf_limit": "PDF 頁數限制", - "thinking_error": "Thinking 格式錯誤", - "parameter_error": "參數驗證失敗", - "invalid_request": "非法請求", - "cache_limit": "快取控制限制" - }, - "regexTester": { - "title": "正則表達式測試器", - "testMessage": "測試訊息", - "testMessagePlaceholder": "輸入要測試的錯誤訊息...", - "matchResult": "匹配結果", - "matched": "匹配成功", - "notMatched": "未匹配", - "test": "測試" - }, - "defaultRules": { - "cannotDelete": "預設規則無法刪除", - "cannotDisable": "建議保留預設規則啟用狀態" - } - }, - "mcpPassthroughConfig": "MCP 透傳配置", - "mcpPassthroughConfigNone": "不啟用", - "mcpPassthroughConfigMinimax": "Minimax", - "mcpPassthroughConfigGlm": "智譜 GLM", - "mcpPassthroughConfigCustom": "自定義 (預留)", - "mcpPassthroughDesc": "啟用後,將 MCP 工具調用透傳到指定的 AI 服務商(如 minimax 的圖片識別、聯網搜索)", - "mcpPassthroughSelect": "透傳類型", - "mcpPassthroughNoneLabel": "不啟用", - "mcpPassthroughNoneDesc": "不啟用 MCP 透傳功能(默認)", - "mcpPassthroughMinimaxLabel": "Minimax", - "mcpPassthroughMinimaxDesc": "透傳到 minimax MCP 服務(支持圖片識別、聯網搜索等工具)", - "mcpPassthroughGlmLabel": "智譜 GLM", - "mcpPassthroughGlmDesc": "透傳到智譜 GLM MCP 服務(支持圖片分析、視頻分析等工具)", - "mcpPassthroughCustomLabel": "自定義", - "mcpPassthroughCustomDesc": "透傳到自定義 MCP 服務(預留,暫未實現)", - "mcpPassthroughHint": "提示: MCP 透傳功能允許 Claude Code 客戶端使用第三方 AI 服務商提供的工具能力(如圖片識別、聯網搜索)", - "mcpPassthroughUrlLabel": "MCP 透傳 URL", - "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com", - "mcpPassthroughUrlDesc": "MCP 服務的基礎 URL。留空則自動從提供商 URL 提取基礎域名", - "mcpPassthroughUrlAuto": "自動提取: {url}" -} diff --git a/messages/zh-TW/settings/clientVersions.json b/messages/zh-TW/settings/clientVersions.json new file mode 100644 index 000000000..b774b0161 --- /dev/null +++ b/messages/zh-TW/settings/clientVersions.json @@ -0,0 +1,51 @@ +{ + "description": "管理用戶端版本要求,確保使用者使用最新的穩定版本。VSCode 外掛和 CLI 作為獨立用戶端分別管理版本。", + "empty": { + "description": "過去 7 天內沒有活躍用戶使用可識別的用戶端", + "title": "暫無用戶端資料" + }, + "features": { + "activeWindow": "活躍視窗:", + "activeWindowDesc": "僅統計過去 7 天內有請求的用戶", + "autoDetect": "系統會自動偵測每種用戶端的最新穩定版本(GA 版本)", + "blockOldVersion": "使用舊版本的用戶將收到 HTTP 400 錯誤,無法繼續使用服務", + "errorMessage": "錯誤提示中包含目前版本與需升級的版本號", + "gaRule": "判定規則:", + "gaRuleDesc": "當某個版本被 1 位以上用戶使用時,視為 GA 版本", + "recommendation": "建議作法:", + "recommendationDesc": "先觀察下方的版本分佈,確認新版本穩定後再啟用。", + "title": "功能說明", + "whatHappens": "啟用後會發生什麼:" + }, + "section": { + "distribution": { + "description": "顯示過去 7 天內活躍用戶的用戶端版本資訊。每種用戶端類型獨立統計 GA 版本。", + "title": "用戶端版本分佈" + }, + "settings": { + "description": "啟用後,系統將自動偵測用戶端版本並攔截舊版本用戶的請求。", + "title": "升級提醒設定" + } + }, + "table": { + "currentGA": "目前 GA 版本:", + "internalType": "內部類型:", + "lastActive": "最後活躍時間", + "latest": "最新版", + "needsUpgrade": "需升級", + "noUsers": "暫無用戶資料", + "status": "狀態", + "unknown": "不明", + "user": "用戶", + "usersCount": "{count} 位用戶", + "version": "目前版本" + }, + "title": "用戶端升級提醒", + "toggle": { + "description": "啟用後,系統將自動偵測用戶端版本並攔截舊版本使用者的請求。", + "disableSuccess": "已關閉用戶端版本檢查", + "enable": "啟用用戶端版本檢查", + "enableSuccess": "已啟用用戶端版本檢查", + "toggleFailed": "狀態切換失敗" + } +} diff --git a/messages/zh-TW/settings/common.json b/messages/zh-TW/settings/common.json new file mode 100644 index 000000000..9ff42dc38 --- /dev/null +++ b/messages/zh-TW/settings/common.json @@ -0,0 +1,31 @@ +{ + "cancel": "關閉", + "completed": "已完成", + "confirm": "確認", + "copied": "金鑰已複製到剪貼簿", + "copy": "複製", + "copyFailed": "複製失敗", + "create": "建立", + "creating": "建立中...", + "delete": "刪除", + "disabled": "停用", + "edit": "編輯", + "empty": "未找到相符的結果", + "enabled": "啟用", + "error": "未知錯誤", + "failed": "失敗", + "loading": "載入中...", + "none": "無(暫無使用者使用此版本)", + "refresh": "重新整理", + "reset": "重設", + "save": "儲存", + "saving": "儲存中...", + "submit": "送出", + "success": "成功!", + "test": "測試", + "testing": "測試中...", + "unlimited": "無限", + "unlimited_desc": "無限制", + "update": "變更", + "updating": "變更中..." +} diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json new file mode 100644 index 000000000..83f70a5fa --- /dev/null +++ b/messages/zh-TW/settings/config.json @@ -0,0 +1,89 @@ +{ + "autoCleanup": "自動日誌清理", + "autoCleanupDesc": "定時自動清理歷史日誌資料,釋放資料庫儲存空間。", + "description": "管理系統的基礎參數,影響站台顯示和統計行為。", + "form": { + "allowGlobalView": "允許查看全站使用量", + "allowGlobalViewDesc": "關閉後,普通使用者在儀表板僅能查看自己金鑰的使用統計。", + "autoCleanupSaved": "自動清理設定已儲存", + "billingModelSource": "計費模型來源", + "billingModelSourceDesc": "設定模型重新導向時使用哪個模型進行計費。「重新導向前」使用使用者請求的原始模型計費,「重新導向後」使用實際呼叫的模型計費。", + "billingModelSourceOptions": { + "original": "重新導向前(原始模型)", + "redirected": "重新導向後(實際模型)" + }, + "billingModelSourcePlaceholder": "選擇計費模型來源", + "cleanupBatchSize": "批次大小", + "cleanupBatchSizeDesc": "每批刪除的記錄數(範圍:1000-100000,建議 10000)", + "cleanupBatchSizePlaceholder": "10000", + "cleanupBatchSizeRequired": "批次大小 *", + "cleanupRetentionDays": "保留天數", + "cleanupRetentionDaysDesc": "超過此天數的日誌將被自動清理(範圍:1-365 天)", + "cleanupRetentionDaysPlaceholder": "30", + "cleanupRetentionDaysRequired": "保留天數 *", + "cleanupSchedule": "清理週期", + "cleanupScheduleCronDesc": "Cron 表達式,預設:0 2 * * *(每天凌晨 2 點)", + "cleanupScheduleCronExample": "範例:0 3 * * 0(每週日凌晨 3 點)", + "cleanupScheduleDesc": "選擇自動清理的執行週期", + "cleanupScheduleLabel": "執行時間(Cron)", + "cleanupSchedulePlaceholder": "0 2 * * *", + "cleanupScheduleRequired": "執行時間(Cron) *", + "configUpdated": "系統設定已更新,頁面將重新整理以應用貨幣顯示變更。", + "currencies": { + "CNY": "¥ 人民幣(CNY)", + "EUR": "€ 歐元(EUR)", + "GBP": "£ 英鎊(GBP)", + "HKD": "HK$ 港幣(HKD)", + "JPY": "¥ 日圓(JPY)", + "KRW": "₩ 韓元(KRW)", + "SGD": "S$ 新加坡元(SGD)", + "TWD": "NT$ 新台幣(TWD)", + "USD": "$ 美元(USD)" + }, + "currencyDisplay": "貨幣顯示單位", + "currencyDisplayDesc": "修改後,系統所有頁面和 API 介面的金額顯示將使用對應的貨幣符號(僅修改符號,不進行匯率轉換)。", + "currencyDisplayPlaceholder": "選擇貨幣單位", + "enableAutoCleanup": "啟用自動清理", + "enableAutoCleanupDesc": "定時自動清理歷史日誌資料", + "enableHttp2": "啟用 HTTP/2", + "enableHttp2Desc": "啟用後,代理請求將優先使用 HTTP/2 協定;若 HTTP/2 失敗,將自動降級為 HTTP/1.1。", + "enableResponseFixer": "啟用回應整流", + "enableResponseFixerDesc": "自動修復上游回應中常見的編碼、SSE 與 JSON 格式問題(預設開啟)。", + "enableThinkingSignatureRectifier": "啟用 thinking 簽名整流器", + "enableThinkingSignatureRectifierDesc": "當 Anthropic 類型供應商返回 thinking 簽名不相容或非法請求等錯誤時,自動移除不相容的 thinking 相關區塊並對同一供應商重試一次(預設開啟)。", + "interceptAnthropicWarmupRequests": "攔截 Warmup 請求(Anthropic)", + "interceptAnthropicWarmupRequestsDesc": "開啟後,識別到 Claude Code 的 Warmup 探測請求將由 CCH 直接搶答短回應,避免存取上游供應商;該請求會記錄在日誌中,但不計費、不限流、不計入統計。", + "keepDays": "保留天數", + "keepDaysDesc": "清理超過此天數的歷史日誌", + "responseFixerFixEncoding": "修復編碼問題", + "responseFixerFixEncodingDesc": "移除 BOM 與空字節,並對無效 UTF-8 做相容處理。", + "responseFixerFixSseFormat": "修復 SSE 格式", + "responseFixerFixSseFormatDesc": "補齊 data: 前綴、統一換行符,並修復常見欄位格式。", + "responseFixerFixTruncatedJson": "修復截斷的 JSON", + "responseFixerFixTruncatedJsonDesc": "補齊未閉合的括號/引號,移除尾隨逗號,必要時補 null。", + "saveConfig": "儲存設定", + "saveError": "儲存失敗", + "saveFailed": "儲存失敗", + "saveSettings": "儲存設定", + "saveSuccess": "儲存成功", + "siteTitle": "站台標題", + "siteTitleDesc": "用於設定瀏覽器分頁標題以及系統預設顯示名稱。", + "siteTitlePlaceholder": "例:Claude Code Hub", + "siteTitleRequired": "站台標題不能為空", + "verboseProviderError": "詳細供應商錯誤資訊", + "verboseProviderErrorDesc": "開啟後,當所有供應商不可用時返回詳細錯誤資訊(包含供應商數量、限流原因等);關閉後僅返回簡潔錯誤碼。" + }, + "section": { + "autoCleanup": { + "description": "定時自動清理歷史日誌資料,釋放資料庫儲存空間。", + "title": "自動日誌清理" + }, + "siteParams": { + "description": "設定站台標題、貨幣顯示單位與儀表板統計展示策略。", + "title": "站台參數" + } + }, + "siteSettings": "站台參數", + "siteSettingsDesc": "設定站台標題、貨幣顯示單位與儀表板統計顯示策略。", + "title": "基礎設定" +} diff --git a/messages/zh-TW/settings/data.json b/messages/zh-TW/settings/data.json new file mode 100644 index 000000000..11fdc3110 --- /dev/null +++ b/messages/zh-TW/settings/data.json @@ -0,0 +1,133 @@ +{ + "cleanup": { + "backupRecommendation": "建議:在清理前先匯出資料庫備份,以防需要還原資料。", + "button": "清理日誌", + "cancel": "關閉", + "cleaning": "清理中...", + "confirm": "確認清理", + "confirmTitle": "確認清理日誌", + "confirmWarning": "此操作將永久刪除 {range}的所有日誌記錄,且無法復原。", + "descriptionWarning": "清理歷史日誌資料以釋放資料庫儲存空間。注意:統計資料將被保留,但日誌詳情將被永久刪除。", + "error": "清理日誌失敗", + "failed": "清理失敗", + "logsDeleted": "✗ 日誌詳情將被刪除(請求/回應內容、錯誤資訊等)", + "previewCount": "將刪除 {count} 筆日誌記錄", + "previewError": "無法取得預覽資訊", + "previewLoading": "正在統計...", + "range": { + "180days": "六個月前的日誌 (180 天)", + "30days": "一個月前的日誌 (30 天)", + "7days": "一週前的日誌 (7 天)", + "90days": "三個月前的日誌 (90 天)" + }, + "rangeDescription": { + "180days": "六個月前", + "30days": "一個月前", + "7days": "一週前", + "90days": "三個月前", + "default": "約 {days} 天前" + }, + "rangeLabel": "清理範圍", + "statisticsRetained": "✓ 統計資料將被保留(用於趨勢分析)", + "successMessage": "成功清理 {count} 筆日誌記錄({batches} 批次,耗時 {duration}秒)", + "willClean": "將清理 {range} 的所有日誌記錄" + }, + "description": "管理資料庫的備份與恢復,支援完整資料匯入匯出和日誌清理。", + "export": { + "button": "匯出資料庫", + "descriptionFull": "匯出完整的資料庫備份檔案(.dump 格式),可用於資料遷移或還原。備份檔案使用 PostgreSQL custom format,自動壓縮且相容不同版本的資料庫結構。", + "error": "匯出資料庫失敗", + "exporting": "正在匯出...", + "failed": "匯出失敗", + "successMessage": "資料庫匯出成功!" + }, + "guide": { + "items": { + "cleanup": { + "description": "物理刪除歷史日誌資料,不可恢復。統計資料(statistics 表)將被保留。建議清理前先匯出資料庫備份。", + "title": "日誌清理" + }, + "environment": { + "description": "此功能需要 Docker Compose 部署環境。本機開發環境可能無法使用。", + "title": "環境要求" + }, + "format": { + "description": "使用 PostgreSQL custom format (.dump),自動壓縮且能夠相容不同版本的資料庫結構。", + "title": "備份格式" + }, + "merge": { + "description": "保留現有資料,嘗試插入備份中的資料。如果存在主鍵衝突可能導致匯入失敗。", + "title": "合併模式" + }, + "overwrite": { + "description": "匯入前會刪除所有現有資料,確保資料庫與備份檔案完全一致。適合完整復原場景。", + "title": "覆寫模式" + }, + "safety": { + "description": "在執行匯入操作前,建議先匯出目前資料庫作為備份,避免資料遺失。", + "title": "安全建議" + } + }, + "title": "使用說明與注意事項" + }, + "import": { + "backupFile": "備份檔案:", + "backupRecommendation": "建議在執行此操作前,先匯出目前資料庫作為備份。", + "button": "匯入資料庫", + "cancel": "關閉", + "cleanFirstDescription": "匯入前刪除所有現有資料,確保資料庫與備份檔案完全一致。如果不勾選,將嘗試合併資料,但可能因主鍵衝突而失敗。", + "cleanFirstLabel": "清除現有資料(覆蓋模式)", + "confirm": "確認匯入", + "confirmMerge": "您選擇了「合併模式」,這將嘗試在保留現有資料的基礎上匯入備份。", + "confirmOverwrite": "您選擇了「覆蓋模式」,這將會刪除所有現有資料後匯入備份。", + "confirmTitle": "確認匯入資料庫", + "descriptionFull": "從備份檔案還原資料庫。支援 PostgreSQL custom format (.dump) 格式的備份檔案。", + "error": "匯入資料庫失敗", + "errorUnknown": "未知錯誤", + "failedMessage": "資料匯入失敗,請查看詳細日誌", + "fileError": "請選擇 .dump 格式的備份檔案", + "fileSelected": "已選擇:{name}({size} MB)", + "importing": "正在匯入...", + "noFileSelected": "請先選擇備份檔案", + "parseError": "解析回應資料失敗", + "progressTitle": "匯入進度", + "selectFileLabel": "選擇備份檔案", + "streamError": "無法讀取回應串流", + "streamInterrupted": "資料流意外中斷", + "streamInterruptedDesc": "匯入進度未正常完成,請檢查日誌並驗證資料完整性。如有問題,請重新匯入。", + "successCleanModeDesc": "所有資料已成功還原。如果頁面顯示異常,請重新整理瀏覽器。", + "successMergeModeDesc": "資料已成功匯入並合併。如果頁面顯示異常,請重新整理瀏覽器。", + "successMessage": "資料匯入完成!", + "successWithWarnings": "資料匯入完成(有警告)", + "successWithWarningsDesc": "資料已成功匯入,但略過了一些已存在的物件。如果頁面顯示異常,請重新整理瀏覽器或重新啟動應用。", + "warningMerge": "注意:如果存在主鍵衝突,匯入可能會失敗。", + "warningOverwrite": "警告:此操作不可逆,所有目前資料將被永久刪除!" + }, + "section": { + "cleanup": { + "description": "清理歷史日誌資料以釋放資料庫儲存空間。注意:統計資料將被保留,但日誌詳情將被永久刪除。", + "title": "日誌清理" + }, + "export": { + "description": "匯出完整的資料庫備份檔案(.dump 格式),可用於資料遷移或還原。", + "title": "資料匯出" + }, + "import": { + "description": "從備份檔案還原資料庫。支援 PostgreSQL custom format (.dump) 格式的備份檔案。", + "title": "資料匯入" + }, + "status": { + "description": "查看目前資料庫的連線狀態和基本資訊。", + "title": "資料庫狀態" + } + }, + "status": { + "connected": "資料庫連線正常", + "error": "取得資料庫狀態失敗", + "loading": "載入中...", + "retry": "重試", + "tables": "{count} 個資料表", + "unavailable": "資料庫無法使用" + }, + "title": "資料管理" +} diff --git a/messages/zh-TW/settings/errorRules.json b/messages/zh-TW/settings/errorRules.json new file mode 100644 index 000000000..afa568b81 --- /dev/null +++ b/messages/zh-TW/settings/errorRules.json @@ -0,0 +1,157 @@ +{ + "actions": { + "add": "新增", + "delete": "刪除", + "edit": "編輯", + "messages": { + "error": "操作失敗", + "success": "操作完成" + }, + "refresh": "重新整理", + "test": "測試" + }, + "add": "新增錯誤規則", + "addFailed": "建立錯誤規則失敗", + "addSuccess": "錯誤規則建立成功", + "cacheStats": "快取: 共 {totalCount} 條規則", + "categories": { + "cache_limit": "快取控制限制", + "content_filter": "內容過濾", + "invalid_request": "非法請求", + "parameter_error": "參數驗證失敗", + "pdf_limit": "PDF 頁數限制", + "prompt_limit": "Prompt 長度限制", + "thinking_error": "Thinking 格式錯誤" + }, + "confirmDelete": "確定要刪除錯誤規則「{pattern}」嗎?", + "defaultRules": { + "cannotDelete": "預設規則無法刪除", + "cannotDisable": "建議保留預設規則啟用狀態" + }, + "delete": "刪除錯誤規則", + "deleteFailed": "刪除失敗", + "deleteSuccess": "錯誤規則刪除成功", + "description": "管理不需要自動重試的客戶端錯誤規則。配置後,命中規則的報錯將直接返回給用戶,不會觸發重試,也不會計入供應商熔斷。", + "dialog": { + "addDescription": "設定錯誤訊息正則表達式模式,符合的錯誤將被識別為不可重試的客戶端錯誤。", + "addTitle": "新增錯誤規則", + "categoryHint": "選擇規則所屬的錯誤類別,用於分類管理和統計", + "categoryLabel": "規則類別 *", + "categoryPlaceholder": "選擇規則類別", + "categoryRequired": "請選擇規則類別", + "creating": "建立中...", + "defaultRuleHint": "預設規則的模式無法修改", + "descriptionLabel": "說明", + "descriptionPlaceholder": "選填:新增說明...", + "editDescription": "修改錯誤規則設定,更改後將自動重新整理快取。", + "editTitle": "編輯錯誤規則", + "enableOverride": "啟用錯誤覆寫", + "enableOverrideHint": "啟用後可自訂返回給客戶端的錯誤回應和狀態碼,原始錯誤仍會記錄到資料庫。當前僅支援 Claude API 錯誤格式。", + "invalidJson": "JSON 格式無效", + "invalidPattern": "無效的正則表達式", + "invalidRegex": "正則表達式語法錯誤", + "invalidStatusCode": "狀態碼必須在 400-599 範圍內", + "matchFailed": "未符合", + "matchSuccess": "符合成功", + "matchedText": "符合內容", + "overrideResponseHint": "留空則僅覆寫狀態碼。", + "overrideResponseLabel": "覆寫回應(JSON 格式)", + "overrideResponsePlaceholder": "{\n \"type\": \"error\",\n \"error\": {\n \"type\": \"invalid_request_error\",\n \"message\": \"您的自訂訊息\"\n }\n}", + "overrideStatusCodeHint": "留空則使用上游狀態碼。範圍:400-599。", + "overrideStatusCodeLabel": "覆寫狀態碼(選填)", + "overrideStatusCodePlaceholder": "例如:400", + "patternHint": "支援 JavaScript 正則表達式語法,例如:prompt is too long|invalid.*request", + "patternLabel": "正則表達式模式 *", + "patternPlaceholder": "輸入正則表達式...", + "patternRequired": "請輸入正則表達式模式", + "regexTester": "正則表達式測試器", + "saving": "儲存中...", + "testMessageLabel": "測試訊息", + "testMessagePlaceholder": "輸入要測試的錯誤訊息...", + "useTemplate": "Claude Error 範本", + "useTemplateConfirm": "輸入框已有內容,確定以範本覆蓋?", + "validJson": "JSON 格式正確" + }, + "disable": "錯誤規則已停用", + "edit": "編輯錯誤規則", + "editFailed": "更新錯誤規則失敗", + "editSuccess": "錯誤規則更新成功", + "emptyState": "目前沒有錯誤規則,點擊右上角「新增錯誤規則」開始設定。", + "enable": "錯誤規則已啟用", + "form": { + "fields": { + "category": "規則類別", + "description": "說明", + "pattern": "正則表達式模式" + }, + "labels": { + "category": "規則類別 *", + "description": "說明", + "isEnabled": "啟用狀態", + "pattern": "正則表達式模式 *" + }, + "placeholders": { + "category": "選擇類別", + "description": "選填:新增說明...", + "pattern": "例如:prompt is too long" + } + }, + "nav": "錯誤規則", + "refreshCache": "同步規則", + "refreshCacheFailed": "同步規則失敗", + "refreshCacheSuccess": "規則同步成功,已載入 {count} 個錯誤規則", + "regexTester": { + "matchResult": "匹配結果", + "matched": "比對成功", + "notMatched": "不符合", + "test": "測試", + "testMessage": "測試訊息", + "testMessagePlaceholder": "輸入要測試的錯誤訊息...", + "title": "正則表達式測試器" + }, + "section": { + "title": "錯誤規則列表" + }, + "table": { + "actions": "動作", + "category": "規則類別", + "createdAt": "建立時間", + "default": "預設", + "description": "說明", + "isDefault": "預設規則", + "isEnabled": "啟用狀態", + "pattern": "正則表達式模式", + "status": "狀態" + }, + "tester": { + "category": "規則類別", + "description": "輸入錯誤訊息,檢查是否命中已設定的規則以及最終返回的內容。", + "finalResponse": "覆寫回應", + "inputLabel": "測試錯誤訊息", + "inputPlaceholder": "輸入要檢測的錯誤訊息...", + "matchType": "匹配類型", + "matched": "已命中錯誤規則", + "messageRequired": "請輸入要測試的錯誤訊息", + "noRule": "沒有匹配到任何規則", + "notMatched": "未命中任何規則", + "overrideStatusCode": "覆寫狀態碼", + "pattern": "比對模式", + "ruleInfo": "匹配的規則", + "statusCodeOnlyOverride": "僅覆寫狀態碼,回應內容將使用上游錯誤訊息", + "testButton": "開始測試", + "testFailed": "測試失敗,請稍後重試", + "testing": "測試中...", + "title": "錯誤規則測試", + "warnings": "設定警告" + }, + "title": "錯誤規則管理", + "toggleFailed": "狀態切換失敗", + "toggleFailedError": "狀態切換失敗:", + "validation": { + "categoryRequired": "請選擇規則類別", + "patternInvalid": "正則表達式語法錯誤", + "patternRequired": "請輸入正則表達式模式", + "patternTooComplex": "正則表達式過於複雜", + "redosRisk": "正則表達式存在 ReDoS 風險,請簡化模式" + } +} diff --git a/messages/zh-TW/settings/errors.json b/messages/zh-TW/settings/errors.json new file mode 100644 index 000000000..e96ed5645 --- /dev/null +++ b/messages/zh-TW/settings/errors.json @@ -0,0 +1,17 @@ +{ + "addFailed": "新增服務商失敗", + "addSuccess": "新增成功", + "deleteFailed": "刪除供應商失敗", + "deleteSuccess": "刪除成功", + "editFailed": "更新服務商失敗", + "editSuccess": "更新完成", + "loadFailed": "載入通知設定失敗", + "saveFailed": "儲存失敗", + "saveFailed_error": "儲存設定失敗", + "saveSuccess": "儲存成功", + "syncFailed": "同步失敗", + "syncSuccess": "同步完成", + "testFailed": "測試失敗", + "testFailedRetry": "測試失敗,請重試", + "unknownError": "操作過程中出現異常" +} diff --git a/messages/zh-TW/settings/index.ts b/messages/zh-TW/settings/index.ts new file mode 100644 index 000000000..414b5daa4 --- /dev/null +++ b/messages/zh-TW/settings/index.ts @@ -0,0 +1,101 @@ +import clientVersions from "./clientVersions.json"; +import common from "./common.json"; +import config from "./config.json"; +import data from "./data.json"; +import errorRules from "./errorRules.json"; +import errors from "./errors.json"; +import logs from "./logs.json"; +import nav from "./nav.json"; +import notifications from "./notifications.json"; +import prices from "./prices.json"; +import requestFilters from "./requestFilters.json"; +import sensitiveWords from "./sensitiveWords.json"; +import strings from "./strings.json"; + +import providersAutoSort from "./providers/autoSort.json"; +import providersFilter from "./providers/filter.json"; +import providersGuide from "./providers/guide.json"; +import providersInlineEdit from "./providers/inlineEdit.json"; +import providersList from "./providers/list.json"; +import providersSchedulingDialog from "./providers/schedulingDialog.json"; +import providersSearch from "./providers/search.json"; +import providersSection from "./providers/section.json"; +import providersSort from "./providers/sort.json"; +import providersStrings from "./providers/strings.json"; +import providersTypes from "./providers/types.json"; + +import providersFormApiTest from "./providers/form/apiTest.json"; +import providersFormButtons from "./providers/form/buttons.json"; +import providersFormCommon from "./providers/form/common.json"; +import providersFormDeleteDialog from "./providers/form/deleteDialog.json"; +import providersFormErrors from "./providers/form/errors.json"; +import providersFormFailureThresholdConfirmDialog from "./providers/form/failureThresholdConfirmDialog.json"; +import providersFormKey from "./providers/form/key.json"; +import providersFormMaxRetryAttempts from "./providers/form/maxRetryAttempts.json"; +import providersFormModelRedirect from "./providers/form/modelRedirect.json"; +import providersFormModelSelect from "./providers/form/modelSelect.json"; +import providersFormName from "./providers/form/name.json"; +import providersFormProviderTypes from "./providers/form/providerTypes.json"; +import providersFormProxyTest from "./providers/form/proxyTest.json"; +import providersFormSections from "./providers/form/sections.json"; +import providersFormStrings from "./providers/form/strings.json"; +import providersFormSuccess from "./providers/form/success.json"; +import providersFormTitle from "./providers/form/title.json"; +import providersFormUrl from "./providers/form/url.json"; +import providersFormUrlPreview from "./providers/form/urlPreview.json"; +import providersFormWebsiteUrl from "./providers/form/websiteUrl.json"; + +const providersForm = { + ...providersFormStrings, + apiTest: providersFormApiTest, + buttons: providersFormButtons, + common: providersFormCommon, + deleteDialog: providersFormDeleteDialog, + errors: providersFormErrors, + failureThresholdConfirmDialog: providersFormFailureThresholdConfirmDialog, + key: providersFormKey, + maxRetryAttempts: providersFormMaxRetryAttempts, + modelRedirect: providersFormModelRedirect, + modelSelect: providersFormModelSelect, + name: providersFormName, + providerTypes: providersFormProviderTypes, + proxyTest: providersFormProxyTest, + sections: providersFormSections, + success: providersFormSuccess, + title: providersFormTitle, + url: providersFormUrl, + urlPreview: providersFormUrlPreview, + websiteUrl: providersFormWebsiteUrl, +}; + +const providers = { + ...providersStrings, + autoSort: providersAutoSort, + filter: providersFilter, + form: providersForm, + guide: providersGuide, + inlineEdit: providersInlineEdit, + list: providersList, + schedulingDialog: providersSchedulingDialog, + search: providersSearch, + section: providersSection, + sort: providersSort, + types: providersTypes, +}; + +export default { + nav, + common, + config, + providers, + prices, + sensitiveWords, + requestFilters, + logs, + data, + clientVersions, + notifications, + errors, + errorRules, + ...strings, +}; diff --git a/messages/zh-TW/settings/logs.json b/messages/zh-TW/settings/logs.json new file mode 100644 index 000000000..d58dc1e53 --- /dev/null +++ b/messages/zh-TW/settings/logs.json @@ -0,0 +1,54 @@ +{ + "description": "動態調整系統日誌級別,即時控制日誌輸出詳細程度。", + "form": { + "changeNotice": "目前級別為 {current},點選儲存後將切換到 {selected}", + "currentLevel": "目前日誌級別", + "effectiveImmediately": "調整日誌級別後立即生效,無需重新啟動服務。", + "failed": "設定失敗", + "failedError": "設定日誌級別失敗", + "fetchFailed": "取得日誌級別失敗", + "levelGuideDebug": "Debug(推薦開發環境): 包含詳細除錯資訊,適合排查問題時使用", + "levelGuideFatal": "Fatal/Error: 僅顯示錯誤,日誌最少,適合高負載生產環境", + "levelGuideInfo": "Info(推薦生產環境): 顯示關鍵業務事件(供應商選擇、Session 復用、價格同步)+ 警告 + 錯誤", + "levelGuideTitle": "日誌級別說明", + "levelGuideTrace": "Trace: 極詳細的追蹤資訊,包含所有細節", + "levelGuideWarn": "Warn: 包含警告(限流觸發、熔斷器開啟等)+ 錯誤", + "save": "儲存設定", + "saving": "儲存中...", + "selectLevel": "選擇日誌級別", + "success": "日誌級別已設定為: {level}" + }, + "levels": { + "debug": { + "description": "除錯資訊 + 所有級別(推薦開發環境)", + "label": "Debug" + }, + "error": { + "description": "錯誤訊息", + "label": "Error" + }, + "fatal": { + "description": "僅致命錯誤", + "label": "Fatal" + }, + "info": { + "description": "關鍵業務事件 + 警告 + 錯誤(推薦生產環境)", + "label": "Info" + }, + "trace": { + "description": "極詳細追蹤 + 所有級別", + "label": "Trace" + }, + "warn": { + "description": "警告 + 錯誤", + "label": "Warn" + } + }, + "section": { + "description": "調整後立即生效,無需重新啟動服務。", + "title": "日誌級別控制" + }, + "subtitle": "日誌級別控制", + "subtitleDesc": "調整後立即生效,無需重新啟動服務。適合生產環境排查問題時使用。", + "title": "日誌管理" +} diff --git a/messages/zh-TW/settings/nav.json b/messages/zh-TW/settings/nav.json new file mode 100644 index 000000000..2df27d88b --- /dev/null +++ b/messages/zh-TW/settings/nav.json @@ -0,0 +1,15 @@ +{ + "apiDocs": "API 文檔", + "clientVersions": "用戶端升級提醒", + "config": "設定", + "data": "資料管理", + "docs": "使用文檔", + "errorRules": "錯誤規則", + "feedback": "意見回饋", + "logs": "日誌", + "notifications": "訊息推送", + "prices": "價格表", + "providers": "供應商", + "requestFilters": "請求過濾", + "sensitiveWords": "敏感詞" +} diff --git a/messages/zh-TW/settings/notifications.json b/messages/zh-TW/settings/notifications.json new file mode 100644 index 000000000..f0096303f --- /dev/null +++ b/messages/zh-TW/settings/notifications.json @@ -0,0 +1,146 @@ +{ + "bindings": { + "advanced": "進階", + "bindTarget": "綁定目標", + "boundCount": "已綁定:{count}", + "editTemplateOverride": "編輯覆蓋", + "enable": "啟用", + "enableType": "啟用此通知", + "enabledCount": "已啟用:{count}", + "noTargets": "尚無可用推送目標", + "scheduleCron": "Cron 表達式", + "scheduleCronPlaceholder": "例如:0 9 * * *(每天 09:00)", + "scheduleTimezone": "時區", + "templateOverride": "模板覆蓋", + "templateOverrideTitle": "編輯模板覆蓋", + "title": "綁定" + }, + "circuitBreaker": { + "description": "供應商完全熔斷時立即推送告警訊息", + "enable": "啟用熔斷器告警", + "test": "測試連線", + "title": "熔斷器告警", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." + }, + "costAlert": { + "description": "偵測使用者/供應商消費超過配額閾值時觸發告警", + "enable": "啟用成本預警", + "interval": "檢查間隔(分鐘)", + "test": "測試連線", + "threshold": "預警閾值", + "thresholdHelp": "當消費達到配額的 {percent}% 時觸發告警", + "thresholdLabel": "預警閾值: {percent}%", + "title": "成本預警", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeFeishu": "飛書", + "webhookTypeUnknown": "未知平台,請使用企業微信或飛書的 Webhook URL", + "webhookTypeWeCom": "企業微信" + }, + "dailyLeaderboard": { + "description": "每天定時發送使用者消費 Top N 排行榜", + "enable": "啟用每日排行榜", + "test": "測試連線", + "time": "發送時間", + "timeError": "時間格式錯誤,應為 HH:mm", + "timePlaceholder": "09:00", + "title": "每日使用者消費排行榜", + "topN": "顯示前 N 名", + "webhook": "Webhook URL", + "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." + }, + "description": "設定 Webhook 訊息推送", + "form": { + "loadError": "載入通知設定失敗", + "loading": "載入中...", + "save": "儲存設定", + "saveError": "儲存設定失敗", + "saveFailed": "儲存失敗", + "saving": "儲存中...", + "success": "通知設定已儲存並重新排程任務", + "testError": "測試連線失敗", + "testFailed": "測試失敗", + "testFailedRetry": "測試失敗,請重試", + "testNoResult": "測試成功但未返回結果", + "testSuccess": "測試訊息已發送", + "webhookRequired": "請先填寫 Webhook URL" + }, + "global": { + "description": "啟用或停用所有訊息推送功能", + "enable": "啟用訊息推送", + "legacyModeDescription": "目前使用舊版單一 URL 推送設定。建立推送目標後將自動切換為多目標模式。", + "legacyModeTitle": "相容模式", + "title": "通知總開關" + }, + "targetDialog": { + "createTitle": "新增推送目標", + "customHeaders": "自訂 Headers(JSON)", + "customHeadersPlaceholder": "{\"X-Token\":\"...\"}", + "dingtalkSecret": "釘釘簽名密鑰", + "dingtalkSecretPlaceholder": "可選,用於簽名", + "editTitle": "編輯推送目標", + "enable": "啟用", + "errors": { + "headersInvalidJson": "Headers 不是有效的 JSON", + "headersMustBeObject": "Headers 必須是 JSON 物件", + "headersValueMustBeString": "Headers 的值必須為字串" + }, + "name": "目標名稱", + "namePlaceholder": "例如:運維群", + "proxy": { + "fallbackToDirect": "代理失敗時回退直連", + "title": "代理設定", + "toggle": "展開/收起代理設定", + "url": "代理位址", + "urlPlaceholder": "http://127.0.0.1:7890" + }, + "selectType": "請選擇平台類型", + "telegramBotToken": "Telegram Bot Token", + "telegramBotTokenPlaceholder": "例如:123456:ABCDEF…", + "telegramChatId": "Telegram Chat ID", + "telegramChatIdPlaceholder": "例如:-1001234567890(群組)", + "type": "平台類型", + "types": { + "custom": "自訂 Webhook", + "dingtalk": "釘釘", + "feishu": "飛書", + "telegram": "Telegram", + "wechat": "企業微信" + }, + "webhookUrl": "Webhook URL", + "webhookUrlPlaceholder": "https://example.com/webhook" + }, + "targets": { + "add": "新增目標", + "bindingsSaved": "綁定已儲存", + "created": "目標已新增", + "delete": "刪除", + "deleteConfirm": "確定要刪除此目標嗎?相關綁定也會被移除。", + "deleteConfirmTitle": "刪除推送目標", + "deleted": "目標已刪除", + "description": "管理推送目標,支援企業微信、飛書、釘釘、Telegram、自訂 Webhook", + "edit": "編輯", + "emptyHint": "尚無推送目標,點擊「新增目標」建立。", + "enable": "啟用目標", + "lastTestAt": "上次測試", + "lastTestFailed": "測試失敗", + "lastTestNever": "從未測試", + "lastTestSuccess": "測試成功", + "statusDisabled": "已停用", + "statusEnabled": "已啟用", + "test": "測試", + "testSelectType": "選擇測試類型", + "title": "推送目標", + "update": "儲存目標", + "updated": "目標已更新" + }, + "templateEditor": { + "insert": "插入內容", + "jsonInvalid": "JSON 格式不正確", + "placeholder": "請輸入 JSON 模板...", + "placeholders": "佔位符", + "title": "範本(JSON)" + }, + "title": "訊息推送" +} diff --git a/messages/zh-TW/settings/prices.json b/messages/zh-TW/settings/prices.json new file mode 100644 index 000000000..c8bea5261 --- /dev/null +++ b/messages/zh-TW/settings/prices.json @@ -0,0 +1,191 @@ +{ + "title": "價格表", + "description": "管理平台基礎設定與模型價格", + "section": { + "title": "模型價格", + "description": "管理 AI 模型的價格設定" + }, + "searchPlaceholder": "搜尋模型名稱...", + "filters": { + "all": "所有", + "local": "本機", + "anthropic": "Anthropic", + "openai": "OpenAI", + "vertex": "Vertex" + }, + "badges": { + "local": "本機" + }, + "capabilities": { + "assistantPrefill": "助手預填充", + "computerUse": "電腦使用", + "functionCalling": "函數呼叫", + "pdfInput": "PDF 輸入", + "promptCaching": "Prompt 快取", + "reasoning": "推理能力", + "responseSchema": "回應 Schema", + "toolChoice": "工具選擇", + "vision": "視覺", + "statusSupported": "支援", + "statusUnsupported": "不支援", + "tooltip": "{label}: {status}" + }, + "sync": { + "button": "同步雲端價格表", + "syncing": "同步中…", + "checking": "檢查衝突...", + "successWithChanges": "價格表更新: 新增 {added} 個,更新 {updated} 個,未變化 {unchanged} 個", + "successNoChanges": "價格表已是最新,無需更新", + "failed": "同步失敗", + "failedError": "同步失敗: {error}", + "failedNoResult": "價格表更新成功但未返回處理結果", + "noModels": "未找到支援的模型價格", + "partialFailure": "部分更新成功,但有 {failed} 個模型失敗", + "failedModels": "失敗模型: {models}", + "skippedConflicts": "跳過 {count} 個手動模型" + }, + "conflict": { + "title": "選擇要覆蓋的衝突項", + "description": "以下模型存在手動維護的價格,勾選後將用 LiteLLM 價格覆蓋,未勾選的保持本地不變", + "searchPlaceholder": "搜尋模型...", + "table": { + "modelName": "模型名稱", + "manualPrice": "手動價格", + "litellmPrice": "LiteLLM 價格", + "action": "動作" + }, + "viewDiff": "查看差異", + "diffTitle": "價格差異對比", + "diff": { + "field": "欄位", + "manual": "手動", + "litellm": "LiteLLM", + "inputPrice": "輸入價格", + "outputPrice": "輸出價格", + "imagePrice": "圖片價格", + "provider": "供應商", + "mode": "類型" + }, + "pagination": { + "showing": "顯示 {from}-{to} 條,共 {total} 條" + }, + "selectedCount": "已選擇 {count}/{total} 個模型", + "noMatch": "未找到符合的模型", + "noConflicts": "無衝突項", + "applyOverwrite": "套用覆蓋", + "applying": "套用中..." + }, + "table": { + "modelName": "模型名稱", + "provider": "供應商", + "capabilities": "功能", + "price": "價格", + "inputPrice": "輸入價格($/M)", + "outputPrice": "輸出價格($/M)", + "priceInput": "輸入", + "priceOutput": "輸出", + "pricePerRequest": "每次", + "cacheReadPrice": "快取讀取($/M)", + "cacheCreationPrice": "快取建立($/M)", + "cache5m": "5m", + "cache1h": "1h+", + "copyModelId": "複製模型 ID", + "updatedAt": "更新時間", + "actions": "動作", + "typeChat": "對話", + "typeImage": "圖像生成", + "typeCompletion": "補全", + "typeUnknown": "未知類型", + "loading": "載入中...", + "noMatch": "未找到符合的模型", + "noDataTitle": "暫無價格資料", + "noDataHint": "系統已內置價格表,請透過上方按鈕同步或更新" + }, + "pagination": { + "showing": "顯示 {from}-{to} 條,共 {total} 條", + "previous": "上一頁", + "next": "下一頁", + "perPageLabel": "每頁", + "perPage": "每頁 {size} 條" + }, + "stats": { + "totalModels": "共 {count} 個模型", + "searchResults": "搜尋到 {count} 個結果", + "lastUpdated": "最後更新: {time}" + }, + "dialog": { + "title": "更新模型價格表", + "description": "選擇包含模型價格資料的 JSON 或 TOML 檔案並上傳", + "selectFile": "點擊選擇 JSON/TOML 檔案或拖曳到此處", + "fileSizeLimit": "檔案大小不能超過 10MB", + "fileSizeLimitSmall": "檔案大小不超過 10MB", + "invalidFileType": "請選擇 JSON 或 TOML 格式的檔案", + "fileTooLarge": "檔案大小超過 10MB 限制", + "upload": "上傳並更新", + "uploading": "上傳中...", + "updatePriceTable": "更新價格表", + "updating": "更新中…", + "selectJson": "選擇檔案", + "updateSuccess": "價格表更新成功,共更新 {count} 個模型", + "updateFailed": "更新失敗", + "systemHasBuiltIn": "系統已內置價格表", + "manualDownload": "你也可以手動下載", + "latestPriceTable": "雲端價格表", + "andUploadViaButton": ",並透過上方按鈕上傳", + "cloudModelCountLoading": "雲端模型數量載入中...", + "cloudModelCountFailed": "雲端模型數量載入失敗", + "supportedModels": "目前支援 {count} 個模型", + "results": { + "title": "更新結果", + "total": "總計: {total} 個模型", + "success": "成功:{success}", + "failed": "失敗: {failed}", + "skipped": "跳過: {skipped}", + "more": " (+{count})", + "details": "詳細資訊", + "viewDetails": "檢視詳細記錄" + } + }, + "addModel": "新增模型", + "editModel": "編輯模型", + "deleteModel": "刪除模型", + "addModelDescription": "手動新增模型價格設定", + "editModelDescription": "編輯模型的價格設定", + "deleteConfirm": "確定要刪除模型 {name} 嗎?此操作無法復原。", + "form": { + "modelName": "模型識別碼", + "modelNamePlaceholder": "例如:gpt-5.2-codex", + "modelNameRequired": "模型 ID 為必填", + "displayName": "顯示名稱(選填)", + "displayNamePlaceholder": "例如:GPT-5.2 Codex", + "type": "類型", + "provider": "供應商", + "providerPlaceholder": "例如:openai", + "requestPrice": "按次呼叫價格($/request)", + "inputPrice": "輸入價格($/M tokens)", + "outputPrice": "輸出價格($/M tokens)", + "outputPriceImage": "輸出價格($/張圖)", + "cacheReadPrice": "快取讀取價格($/M tokens)", + "cacheCreationPrice5m": "快取建立價格(5m,$/M tokens)", + "cacheCreationPrice1h": "快取建立價格(1h+,$/M tokens)" + }, + "drawer": { + "prefillLabel": "搜尋現有模型並預填充", + "prefillEmpty": "找不到符合的模型", + "prefillFailed": "搜尋失敗", + "promptCachingHint": "僅當模型支援快取時開啟,並配置下方快取價格", + "cachePricingTitle": "快取價格" + }, + "actions": { + "edit": "編輯", + "more": "更多動作", + "delete": "刪除" + }, + "toast": { + "createSuccess": "模型已新增", + "updateSuccess": "模型已更新完成", + "deleteSuccess": "模型已刪除", + "saveFailed": "儲存失敗", + "deleteFailed": "刪除失敗" + } +} diff --git a/messages/zh-TW/settings/providers/autoSort.json b/messages/zh-TW/settings/providers/autoSort.json new file mode 100644 index 000000000..922d1dabf --- /dev/null +++ b/messages/zh-TW/settings/providers/autoSort.json @@ -0,0 +1,16 @@ +{ + "button": "自動排序優先級", + "changeCount": "{count} 個供應商將被更新", + "changesTitle": "變更詳情", + "confirm": "應用變更", + "costMultiplierHeader": "成本倍數", + "dialogDescription": "根據成本倍數自動分配優先級(低成本 = 高優先級)", + "dialogTitle": "自動排序供應商優先級", + "error": "更新優先級失敗", + "noChanges": "無需更改(已排序)", + "priorityChangeHeader": "優先級變更", + "priorityHeader": "優先級", + "providerHeader": "供應商", + "providersHeader": "供應商", + "success": "已更新 {count} 個供應商的優先級" +} diff --git a/messages/zh-TW/settings/providers/filter.json b/messages/zh-TW/settings/providers/filter.json new file mode 100644 index 000000000..fcd227647 --- /dev/null +++ b/messages/zh-TW/settings/providers/filter.json @@ -0,0 +1,13 @@ +{ + "circuitBroken": "熔斷", + "groups": { + "all": "所有", + "default": "default", + "label": "分組:" + }, + "status": { + "active": "已啟用", + "all": "所有狀態", + "inactive": "已停用" + } +} diff --git a/messages/zh-TW/settings/providers/form/apiTest.json b/messages/zh-TW/settings/providers/form/apiTest.json new file mode 100644 index 000000000..0724624dc --- /dev/null +++ b/messages/zh-TW/settings/providers/form/apiTest.json @@ -0,0 +1,157 @@ +{ + "apiFormat": "供應商類型", + "apiFormatDesc": "預設與路由設定同步,除非手動修改", + "chunksCount": "已接收 {count} 個區塊({format})", + "chunksReceived": "已接收區塊", + "close": "關閉", + "copyFailed": "複製失敗", + "copyFormat": { + "errorDetails": "錯誤詳情", + "message": "訊息", + "testResult": "測試結果" + }, + "copyResult": "複製結果", + "copySuccess": "已複製到剪貼簿", + "customConfig": "自訂", + "customPayloadDesc": "輸入自訂 JSON payload,將覆蓋預設請求主體", + "customPayloadPlaceholder": "{\"model\": \"...\", \"messages\": [...]}", + "disclaimer": { + "confirmConfig": "請確認供應商 URL、API 金鑰及模型設定正確", + "realRequest": "測試將向供應商發送真實請求,可能消耗少量額度", + "resultReference": "【重要】因各家供應商情況不同,測試結果僅供參考,不代表實際呼叫效果", + "title": "注意事項" + }, + "error": "錯誤訊息", + "failed": "失敗", + "fillKeyFirst": "請先填寫 API 金鑰", + "fillUrlFirst": "請先填寫供應商 URL", + "formatAnthropicMessages": "Claude(Anthropic Messages API)", + "formatOpenAIChat": "OpenAI Compatible", + "formatOpenAIResponses": "Codex(Response API)", + "geminiAuthFallback": { + "desc": "實際代理轉發僅使用 Header 認證,可能導致請求失敗", + "warning": "Header 認證失敗,使用了 URL 參數認證" + }, + "invalidUrl": "供應商 URL 無效,僅支援 http/https", + "model": "Model", + "noResult": "測試成功但未返回結果", + "presetConfig": "預置配置", + "presetDesc": "預置範本包含真實 CLI 請求特徵,用於通過中轉服務驗證", + "requestConfig": "請求配置", + "response": "回應內容", + "responseModel": "回應模型", + "responseTime": "回應時間", + "resultCard": { + "copyText": { + "contentCheck": "內容驗證", + "error": "錯誤", + "httpCheck": "HTTP 檢查", + "httpStatus": "HTTP 狀態", + "inputOutput": "輸入 {input} / 輸出 {output} tokens", + "latency": "延遲", + "latencyCheck": "延遲檢查", + "message": "訊息", + "model": "Model", + "response": "回應", + "status": "狀態", + "testedAt": "測試時間", + "usage": "使用量", + "validationDetails": "驗證詳情" + }, + "dialogTitle": "供應商測試詳情", + "errorDetails": { + "title": "錯誤詳情", + "type": "錯誤類型" + }, + "judgment": "判定結果", + "labels": { + "content": "內容", + "error": "錯誤", + "firstByte": "首位元組", + "http": "HTTP", + "latency": "延遲", + "model": "Model", + "responsePreview": "回應預覽", + "totalLatency": "總延遲" + }, + "rawResponse": { + "hint": "此處顯示原始回應內容,您可以在此檢查關鍵字是否存在於回應中", + "title": "完整回應內容" + }, + "status": { + "green": "可用中", + "red": "不可用中", + "yellow": "波動" + }, + "streamInfo": { + "chunksCount": "資料區塊數", + "isStreaming": "串流回應", + "no": "不是", + "title": "串流回應資訊", + "yes": "是的" + }, + "timing": { + "firstByte": "首位元組", + "testedAt": "測試時間", + "title": "時間資訊", + "totalLatency": "總延遲" + }, + "tokenUsage": { + "cacheCreation": "快取建立", + "cacheRead": "快取讀取", + "input": "輸入", + "output": "輸出", + "title": "Token 使用量" + }, + "validation": { + "content": { + "failed": "未找到目標", + "passed": "包含目標字串", + "target": "目標", + "title": "Tier 3: 內容驗證" + }, + "failed": "失敗", + "http": { + "failed": "4xx/5xx 失敗", + "passed": "2xx/3xx 通過", + "statusCode": "狀態碼", + "title": "Tier 1: HTTP 狀態" + }, + "latency": { + "actual": "實際延遲", + "failed": "超過閾值", + "passed": "在閾值內", + "title": "Tier 2: 延遲閾值" + }, + "passed": "通過", + "timeout": "逾時", + "title": "三層驗證詳情" + } + }, + "selectApiFormat": "選擇要測試的供應商類型", + "selectPreset": "選擇預置範本", + "streamFormat": "串流格式", + "streamInfo": "串流響應資訊", + "streamResponse": "串流響應", + "success": "成功!", + "successContains": "成功檢測詞", + "successContainsDesc": "回應需包含此內容才算測試成功", + "successContainsPlaceholder": "pong", + "testApi": "供應商模型測試", + "testFailed": "測試失敗", + "testFailedRetry": "測試失敗,請重試", + "testModel": "測試模型", + "testModelDesc": "可手動輸入,留空則使用預設模型", + "testSuccess": "模型測試成功", + "testing": "測試中...", + "timeout": { + "desc": "測試請求的最大等待時間(5-120 秒)", + "geminiHint": ",Gemini Thinking 模型建議 60 秒以上", + "label": "逾時時間(秒)" + }, + "truncatedBrief": "顯示前 {length} 個字元,點擊「查看詳情」以查看更多", + "truncatedPreview": "顯示前 {length} 個字元,複製可查看完整內容", + "unknown": "不明", + "usage": "Token 使用量", + "viewDetails": "檢視詳情" +} diff --git a/messages/zh-TW/settings/providers/form/buttons.json b/messages/zh-TW/settings/providers/form/buttons.json new file mode 100644 index 000000000..6386df8c3 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/buttons.json @@ -0,0 +1,9 @@ +{ + "collapseAll": "收合全部進階設定", + "delete": "刪除", + "expandAll": "展開全部進階設定", + "submit": "確認新增", + "submitting": "新增中...", + "update": "確認更新", + "updating": "更新處理中..." +} diff --git a/messages/zh-TW/settings/providers/form/common.json b/messages/zh-TW/settings/providers/form/common.json new file mode 100644 index 000000000..56671c4d8 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/common.json @@ -0,0 +1,3 @@ +{ + "core": "核心設定" +} diff --git a/messages/zh-TW/settings/providers/form/deleteDialog.json b/messages/zh-TW/settings/providers/form/deleteDialog.json new file mode 100644 index 000000000..2386b3cc1 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/deleteDialog.json @@ -0,0 +1,6 @@ +{ + "cancel": "關閉", + "confirm": "確認刪除", + "description": "確定要刪除供應商「{name}」嗎?此操作不可復原。", + "title": "刪除供應商" +} diff --git a/messages/zh-TW/settings/providers/form/errors.json b/messages/zh-TW/settings/providers/form/errors.json new file mode 100644 index 000000000..acb3182be --- /dev/null +++ b/messages/zh-TW/settings/providers/form/errors.json @@ -0,0 +1,8 @@ +{ + "addFailed": "新增供應商失敗", + "deleteFailed": "刪除供應商失敗", + "groupTagTooLong": "分組標籤總長度不能超過 {max} 個字元", + "invalidUrl": "請輸入有效的 API 位址", + "invalidWebsiteUrl": "請輸入有效的供應商官網", + "updateFailed": "更新供應商失敗" +} diff --git a/messages/zh-TW/settings/providers/form/failureThresholdConfirmDialog.json b/messages/zh-TW/settings/providers/form/failureThresholdConfirmDialog.json new file mode 100644 index 000000000..409892617 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/failureThresholdConfirmDialog.json @@ -0,0 +1,13 @@ +{ + "cancel": "關閉", + "confirm": "確認儲存", + "confirmQuestion": "是否確認儲存此設定?", + "descriptionDisabledAction": "停用熔斷器", + "descriptionDisabledMiddle": ",這表示", + "descriptionDisabledPrefix": "您將熔斷失敗閾值設定為 ", + "descriptionDisabledSuffix": ",供應商將不會因為連續失敗而被熔斷。", + "descriptionDisabledValue": "0", + "descriptionHighValuePrefix": "您將熔斷失敗閾值設定為 ", + "descriptionHighValueSuffix": ",這是一個較高的值,可能會導致供應商在大量失敗後才被熔斷。", + "title": "確認特殊設定" +} diff --git a/messages/zh-TW/settings/providers/form/key.json b/messages/zh-TW/settings/providers/form/key.json new file mode 100644 index 000000000..5ff355db8 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/key.json @@ -0,0 +1,7 @@ +{ + "currentKey": "當前金鑰:{key}", + "label": "API 金鑰", + "leaveEmpty": "(留空不變更)", + "leaveEmptyDesc": "留空則不變更金鑰", + "placeholder": "輸入 API 金鑰" +} diff --git a/messages/zh-TW/settings/providers/form/maxRetryAttempts.json b/messages/zh-TW/settings/providers/form/maxRetryAttempts.json new file mode 100644 index 000000000..f046612db --- /dev/null +++ b/messages/zh-TW/settings/providers/form/maxRetryAttempts.json @@ -0,0 +1,5 @@ +{ + "desc": "包含首次呼叫在內,單一供應商最多嘗試幾次後切換。留空將使用系統預設值。", + "label": "單一供應商最大嘗試次數", + "placeholder": "2" +} diff --git a/messages/zh-TW/settings/providers/form/modelRedirect.json b/messages/zh-TW/settings/providers/form/modelRedirect.json new file mode 100644 index 000000000..779c7bd00 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/modelRedirect.json @@ -0,0 +1,14 @@ +{ + "add": "新增", + "addNewRule": "新增規則", + "alreadyExists": "模型「{model}」已存在重新導向規則", + "currentRules": "目前規則 ({count})", + "description": "將 Claude Code 客戶端請求的模型(如 claude-sonnet-4.5)重新導向到上游供應商實際支援的模型(如 glm-4.6、gemini-pro)。用於成本最佳化或接入第三方 AI 服務。", + "emptyState": "目前無重新導向規則。新增規則後,系統將自動重寫請求中的模型名稱。", + "sourceEmpty": "來源模型名稱不能為空", + "sourceModel": "使用者請求的模型", + "sourcePlaceholder": "例如:claude-sonnet-4-5-20250929", + "targetEmpty": "目標模型名稱不能為空", + "targetModel": "實際轉發的模型", + "targetPlaceholder": "例如:glm-4.6" +} diff --git a/messages/zh-TW/settings/providers/form/modelSelect.json b/messages/zh-TW/settings/providers/form/modelSelect.json new file mode 100644 index 000000000..9a4186b49 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/modelSelect.json @@ -0,0 +1,20 @@ +{ + "allowAllModels": "允許所有 {type} 模型", + "claude": "Claude", + "clear": "清除", + "gemini": "Gemini", + "loading": "載入中...", + "manualAdd": "手動新增模型", + "manualDesc": "支援新增任意模型名稱(不限於價格表中的模型)", + "manualPlaceholder": "輸入模型名稱(例如 gpt-5-turbo)", + "notFound": "找不到模型", + "openai": "OpenAI", + "refresh": "重新整理模型列表", + "searchPlaceholder": "搜尋模型名稱...", + "selectAll": "全選 ({count})", + "selectedCount": "已選擇 {count} 個模型", + "sourceFallback": "本機", + "sourceFallbackDesc": "使用本地價格表中的模型列表(上游獲取失敗或不支援)", + "sourceUpstream": "上游來源", + "sourceUpstreamDesc": "模型列表來自上游服務商 API" +} diff --git a/messages/zh-TW/settings/providers/form/name.json b/messages/zh-TW/settings/providers/form/name.json new file mode 100644 index 000000000..f62ae57ca --- /dev/null +++ b/messages/zh-TW/settings/providers/form/name.json @@ -0,0 +1,4 @@ +{ + "label": "供應商名稱 *", + "placeholder": "例如:智譜" +} diff --git a/messages/zh-TW/settings/providers/form/providerTypes.json b/messages/zh-TW/settings/providers/form/providerTypes.json new file mode 100644 index 000000000..9faa993c2 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/providerTypes.json @@ -0,0 +1,10 @@ +{ + "claude": "Claude (Anthropic Messages API)", + "claudeAuth": "Claude (Anthropic Auth Token)", + "codex": "Codex (Response API)", + "gemini": "Gemini (Google Gemini API)", + "geminiCli": "Gemini CLI", + "geminiCliDisabled": "", + "openaiCompatible": "OpenAI Compatible", + "openaiCompatibleDisabled": " - 功能開發中" +} diff --git a/messages/zh-TW/settings/providers/form/proxyTest.json b/messages/zh-TW/settings/providers/form/proxyTest.json new file mode 100644 index 000000000..9042da3a0 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/proxyTest.json @@ -0,0 +1,21 @@ +{ + "connectionFailed": "連線失敗", + "connectionMethod": "連線方式:", + "connectionSuccess": "連線成功", + "direct": "直連", + "errorType": "錯誤類型:", + "fillUrlFirst": "請先填寫供應商 URL", + "networkError": "網路錯誤:", + "noResult": "測試成功但未返回結果", + "proxy": "代理連線", + "proxyError": "代理錯誤:", + "responseTime": "回應時間:", + "statusCode": "狀態碼:", + "testConnection": "測試連線", + "testFailed": "測試失敗", + "testFailedRetry": "測試失敗,請重試", + "testing": "測試中...", + "timeoutError": "連線逾時(5秒)。請檢查:\n1. 代理伺服器是否可存取\n2. 代理位址和連接埠是否正確\n3. 代理認證資訊是否正確", + "viaDirect": "(直連)", + "viaProxy": "(透過代理)" +} diff --git a/messages/zh-TW/settings/providers/form/sections.json b/messages/zh-TW/settings/providers/form/sections.json new file mode 100644 index 000000000..8ac597e64 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/sections.json @@ -0,0 +1,313 @@ +{ + "apiTest": { + "desc": "測試供應商模型是否可用,預設與路由設定中選擇的供應商類型保持一致。", + "summary": "驗證供應商與模型連通性", + "testLabel": "供應商模型測試", + "title": "供應商模型測試" + }, + "circuitBreaker": { + "desc": "連續失敗時自動熔斷,避免影響整體服務品質", + "failureThreshold": { + "desc": "連續失敗多少次後觸發熔斷", + "label": "失敗閾值(次)", + "placeholder": "5" + }, + "maxRetryAttempts": { + "desc": "包含首次呼叫在內,單一供應商最多嘗試次數,超過即切換。留空使用系統預設值。", + "label": "單供應商最大嘗試次數", + "placeholder": "2" + }, + "openDuration": { + "desc": "熔斷後多久自動進入半開狀態", + "label": "熔斷時長(分鐘)", + "placeholder": "30" + }, + "successThreshold": { + "desc": "半開狀態下成功多少次後完全恢復", + "label": "恢復閾值(次)", + "placeholder": "2" + }, + "summary": "{failureThreshold} 次失敗 / {openDuration} 分鐘熔斷 / {successThreshold} 次成功恢復 / 每個供應商最多 {maxRetryAttempts} 次嘗試", + "title": "斷路器設定" + }, + "codexStrategy": { + "desc": "控制如何處理 Codex 請求的 instructions 欄位,影響與上游中轉站的相容性", + "hint": "提示:部分嚴格的 Codex 中轉站(如 88code、foxcode)需要官方 instructions,請選擇「自動」或「強制官方」策略", + "select": { + "auto": { + "desc": "透傳客戶端 instructions,400 錯誤時自動重試官方 prompt", + "label": "自動(建議)" + }, + "force": { + "desc": "始終使用官方 Codex CLI instructions(約 4000+ 字)", + "label": "強制官方" + }, + "keep": { + "desc": "始終透傳客戶端 instructions,不自動重試(適用於寬鬆中轉站)", + "label": "原樣透傳" + }, + "label": "策略選擇", + "placeholder": "選擇策略" + }, + "summary": { + "auto": "自動(建議)", + "force": "強制官方", + "keep": "原樣透傳" + }, + "title": "Codex Instructions 策略設定" + }, + "mcpPassthrough": { + "desc": "啟用後,將 MCP 工具調用透傳到指定的 AI 服務商(如 minimax 的圖片識別、聯網搜索)", + "hint": "提示: MCP 透傳功能允許 Claude Code 客戶端使用第三方 AI 服務商提供的工具能力(如圖片識別、聯網搜索)", + "select": { + "custom": { + "desc": "透傳到自定義 MCP 服務(預留,暫未實現)", + "label": "自定義" + }, + "glm": { + "desc": "透傳到智譜 GLM MCP 服務(支持圖片分析、視頻分析等工具)", + "label": "智譜 GLM" + }, + "label": "透傳類型", + "minimax": { + "desc": "透傳到 minimax MCP 服務(支持圖片識別、聯網搜索等工具)", + "label": "Minimax" + }, + "none": { + "desc": "不啟用 MCP 透傳功能(默認)", + "label": "不啟用" + }, + "placeholder": "選擇透傳類型" + }, + "summary": { + "custom": "自定義(預留)", + "glm": "智譜 GLM", + "minimax": "Minimax", + "none": "不啟用" + }, + "title": "MCP 透傳配置", + "urlAuto": "自動提取: {url}", + "urlDesc": "MCP 服務的基礎 URL。留空則自動從提供商 URL 提取基礎域名", + "urlLabel": "MCP 透傳 URL", + "urlPlaceholder": "https://api.minimaxi.com" + }, + "proxy": { + "desc": "設定代理伺服器以改善連線(支援 HTTP、HTTPS、SOCKS4、SOCKS5)", + "fallback": { + "desc": "啟用後,代理連線失敗時自動嘗試直接連線供應商", + "label": "代理失敗時降級為直連" + }, + "summary": { + "configured": "已設定代理", + "fallback": "(啟用降級)", + "none": "未設定" + }, + "test": { + "desc": "透過代理測試訪問供應商 URL(HEAD 請求,不消耗額度)", + "label": "連線測試" + }, + "title": "代理設定", + "url": { + "formats": "支援格式:", + "label": "代理位址", + "optional": "(選填)", + "placeholder": "例如:http://proxy.example.com:8080 或 socks5://127.0.0.1:1080" + } + }, + "rateLimit": { + "dailyResetMode": { + "desc": { + "fixed": "每天固定時間點重置額度", + "rolling": "從首次呼叫開始計算,24 小時後重置" + }, + "label": "每日重設模式", + "options": { + "fixed": "固定時間重置", + "rolling": "滾動視窗(24 小時)" + } + }, + "dailyResetTime": { + "label": "每日重設時間(HH:mm)" + }, + "limit5h": { + "label": "5 小時消費上限(USD)", + "placeholder": "留空表示無限制" + }, + "limitConcurrent": { + "label": "並發 Session 上限", + "placeholder": "0 表示無限制" + }, + "limitDaily": { + "label": "每日消費上限(USD)", + "placeholder": "留空表示無限制" + }, + "limitMonthly": { + "label": "月消費上限(USD)", + "placeholder": "留空表示無限制" + }, + "limitTotal": { + "label": "總消費上限(USD)", + "placeholder": "留空表示無限制" + }, + "limitWeekly": { + "label": "週消費上限(USD)", + "placeholder": "留空表示無限制" + }, + "summary": { + "concurrent": "並發:{count}", + "daily": "日: {amount}(重置 {resetTime})", + "fiveHour": "5h:${amount}", + "monthly": "月:${amount}", + "none": "無限制", + "total": "總:${amount}", + "weekly": "週:${amount}" + }, + "title": "流量限制" + }, + "routing": { + "cacheTtl": { + "desc": "強制設定 prompt cache TTL;僅影響包含 cache_control 的請求。", + "label": "Cache TTL 覆寫", + "options": { + "1h": "1 小時", + "5m": "5 分鐘", + "inherit": "不覆寫(跟隨客戶端)" + } + }, + "codexOverrides": { + "parallelToolCalls": { + "help": "控制是否允許並行 tool calls。關閉可能降低工具呼叫並發能力;「跟隨客戶端」不改寫 parallel_tool_calls。", + "label": "並行工具呼叫覆寫", + "options": { + "false": "強制關閉", + "inherit": "不覆寫(跟隨客戶端)", + "true": "強制開啟" + } + }, + "reasoningEffort": { + "help": "控制模型在輸出前用於推理的強度(推理 token 數量)。選擇「跟隨客戶端」表示不改寫請求;選擇其他值則強制覆寫 reasoning.effort。注意:none 僅 GPT-5.1 系列支援;xhigh 僅 GPT-5.1-Codex-Max 支援,模型不支援會返回錯誤。", + "label": "推理等級覆寫", + "options": { + "high": "high(更強推理)", + "inherit": "不覆寫(跟隨客戶端)", + "low": "low", + "medium": "medium(預設)", + "minimal": "minimal(更省資源、更快)", + "none": "none(僅 GPT-5.1)", + "xhigh": "xhigh(僅 GPT-5.1-Codex-Max)" + } + }, + "reasoningSummary": { + "help": "控制是否回傳推理摘要。auto 回傳精簡摘要,detailed 回傳更詳細摘要;「跟隨客戶端」不改寫 reasoning.summary。", + "label": "推理摘要覆寫", + "options": { + "auto": "auto(精簡)", + "detailed": "detailed(更詳細)", + "inherit": "不覆寫(跟隨客戶端)" + } + }, + "textVerbosity": { + "help": "控制模型輸出的詳細程度。low 較精簡,high 較詳細;「跟隨客戶端」不改寫 text.verbosity。", + "label": "輸出冗長度覆寫", + "options": { + "high": "high(更詳細)", + "inherit": "不覆寫(跟隨客戶端)", + "low": "low(精簡)", + "medium": "medium(預設)" + } + } + }, + "context1m": { + "desc": "設定 1M 上下文視窗支援。僅對 Sonnet 模型生效(claude-sonnet-4-5、claude-sonnet-4)。啟用後將套用階梯定價。", + "label": "1M 上下文視窗", + "options": { + "disabled": "停用", + "forceEnable": "強制啟用", + "inherit": "跟隨客戶端" + } + }, + "joinClaudePool": { + "desc": "啟用後,此供應商將與 Claude 類型供應商共同參與負載均衡", + "help": "僅當存在映射至 claude-* 的規則時可用。當用戶請求 claude-* 模型時,此供應商也會被納入調度。", + "label": "加入 Claude 調度池" + }, + "modelRedirects": { + "label": "模型重定向設定", + "optional": "(選填)" + }, + "modelWhitelist": { + "allowAll": "✓ 允許所有模型(建議)", + "desc": "限制此供應商可處理的模型。預設可處理該類型下的所有模型。", + "label": "允許的模型", + "moreModels": "+{count} 個以上", + "optional": "(選填)", + "selectedOnly": "僅允許所選的 {count} 個模型。其他模型將不會被路由到此供應商。", + "title": "模型允許清單" + }, + "preserveClientIp": { + "desc": "向上游轉發 x-forwarded-for / x-real-ip,可能暴露真實來源 IP", + "help": "預設關閉以保護隱私;僅在需要上游感知終端 IP 時開啟。", + "label": "透傳客戶端 IP" + }, + "providerType": { + "desc": "(決定調度策略)", + "label": "供應商類型", + "placeholder": "選擇供應商類型" + }, + "providerTypeDesc": "選擇供應商的 API 格式類型。", + "providerTypeDisabledNote": "註:OpenAI Compatible 類型開發中,暫不可用", + "scheduleParams": { + "costMultiplier": { + "desc": "成本計算倍數。官方供應商=1.0,便宜 20%=0.8,貴 20%=1.2(最多 4 位小數)", + "label": "成本倍數", + "placeholder": "1.0" + }, + "group": { + "desc": "分組標籤。僅供 providerGroup 與此值相符的用戶使用。例:設為「premium」表示僅供 providerGroup=\"premium\" 的用戶使用", + "label": "供應商分組", + "placeholder": "例如:premium, economy" + }, + "priority": { + "desc": "數值越小,優先級越高(0 最高)。系統只會從最高優先級的供應商中選擇。建議:主力=0,備用=1,緊急備援=2", + "label": "優先級", + "placeholder": "0" + }, + "title": "調度參數", + "weight": { + "desc": "加權隨機。同一優先級內,權重越高被選中機率越大。例如 1:2:3 約為 16%:33%:50%", + "label": "權重", + "placeholder": "1" + } + }, + "summary": { + "models": "{count} 個允許模型", + "none": "未設定", + "redirects": "{count} 個重定向" + }, + "title": "路由設定" + }, + "timeout": { + "desc": "設定請求超時時間,0 表示禁用超時", + "disableHint": "設為 0 表示禁用該超時(僅用於灰度回退場景,不推薦)", + "nonStreamingTotal": { + "core": "true", + "desc": "非串流請求總超時,範圍 60-1200 秒,預設 600 秒(10 分鐘)", + "label": "非串流總超時(秒)", + "placeholder": "600" + }, + "streamingFirstByte": { + "core": "true", + "desc": "串流請求首字節超時,範圍 1-120 秒,預設 30 秒", + "label": "串流首字節超時(秒)", + "placeholder": "30" + }, + "streamingIdle": { + "core": "true", + "desc": "串流請求靜默期超時,範圍 60-600 秒,填 0 禁用(防止中途卡住)", + "label": "串流靜默期超時(秒)", + "placeholder": "60" + }, + "summary": "首字: {streaming}s | 串流間隔: {idle}s | 非串流: {nonStreaming}s", + "title": "超時配置" + } +} diff --git a/messages/zh-TW/settings/providers/form/strings.json b/messages/zh-TW/settings/providers/form/strings.json new file mode 100644 index 000000000..fe50f6269 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/strings.json @@ -0,0 +1,204 @@ +{ + "addRedirect": "新增重定向", + "allowAllModels": "✓ 允許所有模型(建議)", + "apiAddress": "API 位址", + "apiAddressPlaceholder": "例如:https://open.bigmodel.cn/api/anthropic", + "apiAddressRequired": "API 位址 *", + "apiKey": "API 金鑰", + "apiKeyCurrent": "目前金鑰:", + "apiKeyLeaveEmpty": "(留空不變更)", + "apiKeyLeaveEmptyDesc": "留空則不變更金鑰", + "apiKeyOptional": "留空則不更改金鑰", + "apiKeyPlaceholder": "輸入 API 金鑰", + "apiKeyRequired": "API 金鑰 *", + "baseUrl": "基礎 URL", + "baseUrlPlaceholder": "例如:https://open.bigmodel.cn/api/anthropic", + "baseUrlRequired": "請先填寫供應商 URL", + "circuitBreakerConfig": "斷路器設定", + "circuitBreakerConfigSummary": "{failureThreshold} 次失敗 / {openDuration} 分鐘熔斷 / {successThreshold} 次成功恢復 / 每個供應商最多 {maxRetryAttempts} 次嘗試", + "circuitBreakerDesc": "連續失敗時自動熔斷,避免影響整體服務品質", + "clearSearch": "清除搜尋", + "codexInstructions": "Codex Instructions 策略", + "codexInstructionsAuto": "自動(建議)", + "codexInstructionsDesc": "(決定調度策略)", + "codexInstructionsForce": "強制官方", + "codexInstructionsKeep": "保留原值", + "codexStrategyAutoDesc": "透傳客戶端 instructions,400 錯誤時自動重試官方 prompt", + "codexStrategyAutoLabel": "自動(建議)", + "codexStrategyConfig": "Codex Instructions 策略", + "codexStrategyConfigAuto": "自動(建議)", + "codexStrategyConfigForce": "強制官方", + "codexStrategyConfigKeep": "原樣透傳", + "codexStrategyDesc": "控制如何處理 Codex 請求的 instructions 欄位,影響與上游中轉站的相容性", + "codexStrategyForceDesc": "始終使用官方 Codex CLI instructions(約 4000+ 字)", + "codexStrategyForceLabel": "強制官方", + "codexStrategyHint": "提示:部分嚴格的 Codex 中轉站(如 88code、foxcode)需要官方 instructions,請選擇「自動」或「強制官方」策略", + "codexStrategyKeepDesc": "始終透傳客戶端 instructions,不自動重試(適用於寬鬆中轉站)", + "codexStrategyKeepLabel": "原樣透傳", + "codexStrategySelect": "策略選擇", + "collapseAll": "摺疊全部進階設定", + "confirmAdd": "確認新增", + "confirmAddPending": "新增中...", + "confirmUpdate": "確認更新", + "confirmUpdatePending": "更新中…", + "costMultiplier": "成本倍數", + "costMultiplierDesc": "例如:A(成本 1.0x)、C(成本 0.8x)", + "costMultiplierLabel": "成本倍率", + "costMultiplierPlaceholder": "1.0", + "deleteButton": "刪除", + "dialogDescription": "設定供應商資訊與進階設定。", + "enabled": "啟用", + "expandAll": "展開全部進階設定", + "failureThreshold": "失敗閾值(次)", + "failureThresholdDesc": "連續失敗多少次後觸發熔斷", + "failureThresholdPlaceholder": "5", + "filterAllProviders": "全部供應商", + "filterByType": "篩選供應商類型", + "filterProvider": "篩選供應商類型", + "group": "分組", + "groupPlaceholder": "例如:premium, economy", + "joinClaudePool": "加入 Claude 調度池", + "joinClaudePoolDesc": "啟用後,此供應商將與 Claude 類型供應商共同參與負載均衡", + "joinClaudePoolHelp": "僅當存在映射至 claude-* 的規則時可用。當用戶請求 claude-* 模型時,此供應商也會被納入調度。", + "leaveEmpty": "留空表示無限制", + "limit0Means": "0 表示無限制", + "limit5hLabel": "5 小時消費上限(USD)", + "limitAmount5h": "5 小時消費上限(USD)", + "limitAmount5hDesc": "例如: 供應商 B 的 5 小時限額 $10,已消耗 $9.8", + "limitAmountMonthly": "月消費上限(USD)", + "limitAmountWeekly": "週消費上限(USD)", + "limitConcurrent": "並行 Session 限制", + "limitConcurrentDesc": "例如: 供應商 C 並行限制 2,目前活躍 Session 數:2", + "limitConcurrentLabel": "並行 Session 上限", + "limitMonthlyLabel": "月消費上限(USD)", + "limitPlaceholder0": "0 表示無限制", + "limitPlaceholderUnlimited": "留空表示無限制", + "limitWeeklyLabel": "週消費上限(USD)", + "modelRedirects": "模型重新導向", + "modelRedirectsAddNew": "新增新规则", + "modelRedirectsCurrentRules": "目前规则 ({count})", + "modelRedirectsDesc": "將 Claude 模型請求重定向到其他供應商支援的模型", + "modelRedirectsEmpty": "目前無重新導向規則。新增規則後,系統將自動重寫請求中的模型名稱。", + "modelRedirectsExists": "模型 \"{model}\" 已存在重新導向規則", + "modelRedirectsLabel": "模型重新導向設定", + "modelRedirectsOptional": "(可選)", + "modelRedirectsSourceModel": "用戶請求的模型", + "modelRedirectsSourcePlaceholder": "例如:claude-sonnet-4-5-20250929", + "modelRedirectsSourceRequired": "來源模型名稱不能為空", + "modelRedirectsTargetModel": "實際轉發的模型", + "modelRedirectsTargetPlaceholder": "例如:glm-4.6", + "modelRedirectsTargetRequired": "目標模型名稱不能為空", + "modelWhitelist": "模型允許清單", + "modelWhitelistAllowAll": "允許所有 {type} 模型", + "modelWhitelistAllowAllClause": "允許所有 Claude 模型", + "modelWhitelistAllowAllOpenAI": "允許所有 OpenAI 模型", + "modelWhitelistClear": "清空", + "modelWhitelistDesc": "限制此供應商可處理的模型。預設可處理該類型下的所有模型。", + "modelWhitelistLabel": "允許的模型", + "modelWhitelistLoading": "載入中...", + "modelWhitelistManualAdd": "手動新增模型", + "modelWhitelistManualDesc": "支援新增任意模型名稱(不限於價格表中的模型)", + "modelWhitelistManualPlaceholder": "輸入模型名稱(例如 gpt-5-turbo)", + "modelWhitelistNotFound": "未找到模型", + "modelWhitelistSearchPlaceholder": "搜尋模型名稱...", + "modelWhitelistSelectAll": "全選({count})", + "modelWhitelistSelected": "已選擇 {count} 個模型", + "modelWhitelistSelectedOnly": "僅允許所選的 {count} 個模型。其他模型將不會被路由到此供應商。", + "namePlaceholder": "輸入供應商名稱", + "openDuration": "熔斷時長(分鐘)", + "openDurationDesc": "熔斷後多久自動進入半開狀態", + "openDurationPlaceholder": "30", + "priority": "優先級", + "priorityDesc": "同優先級內,按成本倍率從低到高排序", + "priorityLabel": "優先級", + "priorityPlaceholder": "0", + "providerGroupDesc": "分組標籤。僅供 providerGroup 與此值相符的用戶使用。例:設為「premium」表示僅供 providerGroup=\"premium\" 的用戶使用", + "providerGroupLabel": "供應商分組", + "providerGroupPlaceholder": "例如:premium, economy", + "providerName": "供應商名稱", + "providerNamePlaceholder": "例如:智譜", + "providerNameRequired": "供應商名稱 *", + "providerType": "供應商類型", + "providerTypeDesc": "選擇供應商的 API 格式類型。", + "providerTypeDisabledNote": "註:OpenAI Compatible 類型開發中,暫不可用", + "proxy": "代理", + "proxyAddressFormats": "支援格式:", + "proxyAddressLabel": "代理位址", + "proxyAddressOptional": "(選填)", + "proxyAddressPlaceholder": "例如:http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", + "proxyConfig": "代理設定", + "proxyConfigDesc": "設定代理伺服器以改善連線(支援 HTTP、HTTPS、SOCKS4、SOCKS5)", + "proxyConfigNone": "未設定", + "proxyConfigSummary": "已設定代理", + "proxyConfigSummaryFallback": " (啟用降級)", + "proxyConfigured": "已設定代理", + "proxyFallback": "代理失敗降級", + "proxyFallbackDesc": "代理連線失敗時自動降級到直連", + "proxyFallbackLabel": "代理失敗時降級為直連", + "proxyNotConfigured": "未設定", + "proxyTestButton": "測試連線", + "proxyTestDesc": "透過代理測試訪問供應商 URL(HEAD 請求,不消耗額度)", + "proxyTestFailed": "連線失敗", + "proxyTestFillUrl": "請先填寫供應商 URL", + "proxyTestLabel": "連線測試", + "proxyTestNetworkError": "網路錯誤:{error}", + "proxyTestProxyError": "代理錯誤:{error}", + "proxyTestResponseTime": "回應時間:{time}", + "proxyTestResultConnectionMethod": "連線方式:{via}", + "proxyTestResultConnectionMethodDirect": "直連", + "proxyTestResultConnectionMethodProxy": "代理", + "proxyTestResultErrorType": "錯誤類型: {type}", + "proxyTestResultFailed": "連線失敗", + "proxyTestResultMessage": "{message}", + "proxyTestResultResponseTime": "回應時間: {time}ms", + "proxyTestResultStatusCode": "狀態碼:{code}", + "proxyTestResultSuccess": "連線成功 {via}", + "proxyTestStatusCode": "| 狀態碼:{code}", + "proxyTestSuccess": "連線成功", + "proxyTestTesting": "測試中...", + "proxyTestTimeout": "連線超時(5秒)。請檢查:\n1. 代理伺服器是否可訪問\n2. 代理位址和連接埠是否正確\n3. 代理認證資訊是否正確", + "proxyTestViaDirect": "(直連)", + "proxyTestViaProxy": "(透過代理)", + "proxyUrl": "代理位址", + "proxyUrlPlaceholder": "例如:http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", + "rateLimitConfig": "限流設定", + "rateLimitConfigNone": "無限制", + "rateLimitConfigSummary": "5h: ${fiveHour}, 週: ${weekly}, 月: ${monthly}, 並行: {concurrent}", + "remark": "備註", + "remarkPlaceholder": "可選:新增說明...", + "removeRedirect": "移除重新導向", + "routingConfig": "路由設定", + "routingConfigNone": "未設定", + "routingConfigSummary": "{models} 個模型白名單,{redirects} 個重新導向", + "scheduleParams": "調度參數", + "searchClear": "清除搜尋", + "searchPlaceholder": "搜尋供應商名稱、URL、備註...", + "selectProviderType": "選擇供應商類型", + "sort": "排序供應商", + "sortByCost": "按成本排序", + "sortByCreated": "按建立時間(新-舊)", + "sortByName": "按名稱(A-Z)", + "sortByPriority": "按優先級(高-低)", + "sortByWeight": "按權重(高-低)", + "sourceModel": "來源模型名稱", + "sourceModelPlaceholder": "例如: claude-sonnet-4-5-20250929", + "sourceModelRequired": "來源模型名稱不能為空", + "successThreshold": "恢復閾值(次)", + "successThresholdDesc": "半開狀態下成功多少次後完全恢復", + "successThresholdPlaceholder": "2", + "targetModel": "目標模型名稱", + "targetModelPlaceholder": "例如: glm-4.6", + "targetModelRequired": "目標模型名稱不能為空", + "testProxy": "測試連線", + "testProxyFailed": "測試代理連線失敗", + "testProxyFailedError": "測試連線失敗:", + "testProxySuccess": "代理連線成功", + "validUrlRequired": "請輸入有效的 API 位址", + "websiteUrlDesc": "供應商官網位址,用於快速前往管理", + "websiteUrlInvalid": "請輸入有效的供應商官網位址", + "websiteUrlPlaceholder": "https://example.com", + "weight": "權重", + "weightDesc": "加權隨機。同一優先級內,權重越高被選中機率越大。例如 1:2:3 約為 16%:33%:50%", + "weightLabel": "權重", + "weightPlaceholder": "1" +} diff --git a/messages/zh-TW/settings/providers/form/success.json b/messages/zh-TW/settings/providers/form/success.json new file mode 100644 index 000000000..f6bb1c35a --- /dev/null +++ b/messages/zh-TW/settings/providers/form/success.json @@ -0,0 +1,4 @@ +{ + "created": "新增供應商成功", + "createdDesc": "供應商「{name}」已新增" +} diff --git a/messages/zh-TW/settings/providers/form/title.json b/messages/zh-TW/settings/providers/form/title.json new file mode 100644 index 000000000..d12f98740 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/title.json @@ -0,0 +1,4 @@ +{ + "create": "新增供應商", + "edit": "編輯供應商" +} diff --git a/messages/zh-TW/settings/providers/form/url.json b/messages/zh-TW/settings/providers/form/url.json new file mode 100644 index 000000000..10f2d0b7f --- /dev/null +++ b/messages/zh-TW/settings/providers/form/url.json @@ -0,0 +1,4 @@ +{ + "label": "API 位址 *", + "placeholder": "例如:https://open.bigmodel.cn/api/anthropic" +} diff --git a/messages/zh-TW/settings/providers/form/urlPreview.json b/messages/zh-TW/settings/providers/form/urlPreview.json new file mode 100644 index 000000000..101b00a65 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/urlPreview.json @@ -0,0 +1,9 @@ +{ + "copy": "複製", + "copyFailed": "複製失敗", + "copySuccess": "已複製 {name} 到剪貼簿", + "duplicatePath": "檢測到重複路徑", + "invalidUrl": "無效的 URL 格式", + "invalidUrlDesc": "請輸入有效的 HTTP/HTTPS 地址", + "title": "URL 拼接預覽" +} diff --git a/messages/zh-TW/settings/providers/form/websiteUrl.json b/messages/zh-TW/settings/providers/form/websiteUrl.json new file mode 100644 index 000000000..40306e6c4 --- /dev/null +++ b/messages/zh-TW/settings/providers/form/websiteUrl.json @@ -0,0 +1,5 @@ +{ + "desc": "供應商官方網站,便於快速跳轉管理", + "label": "供應商官網", + "placeholder": "https://example.com" +} diff --git a/messages/zh-TW/settings/providers/guide.json b/messages/zh-TW/settings/providers/guide.json new file mode 100644 index 000000000..6510c4e7c --- /dev/null +++ b/messages/zh-TW/settings/providers/guide.json @@ -0,0 +1,120 @@ +{ + "after": "篩選後:", + "before": "篩選前:", + "bestPracticesConcurrent": "• 並行控制:根據供應商 API 限制設定 Session 並行數", + "bestPracticesCost": "• 成本倍率:官方倍率為 1.0,自建服務可設定為 0.8-1.2", + "bestPracticesLimit": "• 限額設定:根據預算設定 5 小時、7 天、30 天限額", + "bestPracticesPriority": "• 優先級設定:核心供應商設為 0,備用供應商設為 1-3", + "bestPracticesTitle": "最佳實務建議", + "bestPracticesWeight": "• 權重配置:根據供應商容量設定權重(容量大 = 權重高)", + "circuitBreaker": "熔斷器檢查", + "circuitBreakerOpen": "A 被過濾,剩餘:B、C、D", + "circuitBreakerRecovery": "A 在 60 秒後自動恢復到半開狀態", + "circuitBreakerRecovery5h": "5 小時滑動視窗後自動恢復", + "costOptimize": "2. 成本優化:同一優先級內,成本倍率較低的供應商被選中的機率較高", + "costSort": "成本排序降級", + "costSortExample": "所有供應商:A(default)、B(premium)、C(premium)、D(economy)", + "costSortProb": "成本更低的 C 有更高的被選中機率", + "costSortResult": "排序後:C(0.8x)、A(1.0x)", + "decision": "決策:", + "group": "使用者分組篩選", + "groupDesc": "如果使用者指定了供應商組,系統會優先從該組中選擇", + "groupDowngrade": "記錄警告並從全域供應商池中選擇", + "groupExample": "使用者設定了 providerGroup = 'premium'", + "groupFallback": "如果使用者組內沒有可用供應商,降級到所有供應商", + "groupFiltered": "只從 A 和 C 中選擇,B 和 D 被過濾", + "groupUnavailable": "使用者組 'vip' 內的供應商全部停用或超限", + "health": "健康度篩選(熔斷器 + 限流)", + "healthCheck": "檢查 B 是否啟用且健康", + "healthCheckAmountLimit": "檢查 5 小時、7 天、30 天的消費額度是否超限", + "healthCheckAmountLimitExample": "供應商 B 的 5 小時限額 $10,已消耗 $9.8", + "healthCheckCircuit": "供應商 A 連續失敗 5 次,熔斷器狀態:open", + "healthCheckConcurrent": "檢查目前活躍 Session 數是否超過設定的並行限制", + "healthCheckConcurrentExample": "供應商 C 並行限制 2,目前活躍 Session 數:2", + "healthFilter": "3. 健康篩選:自動跳過熔斷或超限的供應商", + "healthFiltered": "B 被過濾(接近限額),剩餘:C、D", + "healthFiltered2": "C 被過濾(已滿),剩餘:D", + "history": "檢查歷史請求", + "historyDesc": "查詢該 API Key 最近 10 秒內使用的供應商", + "priority": "優先級分層選擇", + "priorityExample": "有 4 個已啟用的供應商,優先級各不相同", + "priorityFirst": "1. 優先級優先:只從最高優先級(數值最小)的供應商中選擇", + "priorityResult": "篩選出最高優先級(0)的供應商:A、C", + "priorityStep": "系統首先按優先級篩選,只從最高優先級的供應商中選擇", + "randomResult": "最終隨機選擇了 C", + "randomSelect": "加權隨機", + "reset": "手動解除熔斷", + "resetSuccess": "熔斷器已重置", + "scenario1Desc": "系統首先按優先級篩選,只從最高優先級的供應商中選擇", + "scenario1Step1": "初始狀態", + "scenario1Step1After": "篩選出最高優先級(0)的供應商:A、C", + "scenario1Step1Before": "供應商 A(優先級 0)、B(優先級 1)、C(優先級 0)、D(優先級 2)", + "scenario1Step1Decision": "只從 A 和 C 中選擇,B 和 D 被過濾", + "scenario1Step1Desc": "有 4 個已啟用的供應商,優先級各不相同", + "scenario1Step2": "按成本排序", + "scenario1Step2After": "排序後:C(0.8x)、A(1.0x)", + "scenario1Step2Before": "A(成本 1.0x)、C(成本 0.8x)", + "scenario1Step2Decision": "成本更低的 C 有更高的被選中機率", + "scenario1Step2Desc": "在同一優先級內,按成本倍率從低到高排序", + "scenario1Step3": "加權隨機", + "scenario1Step3After": "C 被選中機率 75%,A 被選中機率 25%", + "scenario1Step3Before": "C(權重 3)、A(權重 1)", + "scenario1Step3Decision": "最終隨機選擇了 C", + "scenario1Step3Desc": "使用權重進行隨機選擇,權重越高被選中機率越大", + "scenario1Title": "優先級分層選擇", + "scenario2Desc": "如果使用者指定了供應商組,系統會優先從該組中選擇", + "scenario2Step1": "檢查使用者分組", + "scenario2Step1After": "過濾出 'premium' 組:B、C", + "scenario2Step1Before": "所有供應商:A(default)、B(premium)、C(premium)、D(economy)", + "scenario2Step1Decision": "只從 B 和 C 中選擇", + "scenario2Step1Desc": "使用者設定了 providerGroup = 'premium'", + "scenario2Step2": "分組降級", + "scenario2Step2After": "降級到所有啟用的供應商:A、B、C、D", + "scenario2Step2Before": "使用者組 'vip' 內的供應商全部停用或超限", + "scenario2Step2Decision": "記錄警告並從全域供應商池中選擇", + "scenario2Step2Desc": "如果使用者組內沒有可用供應商,降級到所有供應商", + "scenario2Title": "使用者分組篩選", + "scenario3Desc": "系統自動過濾掉熔斷或超限的供應商", + "scenario3Step1": "熔斷器檢查", + "scenario3Step1After": "A 被過濾,剩餘:B、C、D", + "scenario3Step1Before": "供應商 A 連續失敗 5 次,熔斷器狀態:open", + "scenario3Step1Decision": "A 在 60 秒後自動恢復到半開狀態", + "scenario3Step1Desc": "連續失敗 5 次後熔斷器打開,60 秒內不可用", + "scenario3Step2": "金額限流", + "scenario3Step2After": "B 被過濾(接近限額),剩餘:C、D", + "scenario3Step2Before": "供應商 B 的 5 小時限額 $10,已消耗 $9.8", + "scenario3Step2Decision": "5 小時滑動視窗後自動恢復", + "scenario3Step2Desc": "檢查 5 小時、7 天、30 天的消費額度是否超限", + "scenario3Step3": "並行 Session 限制", + "scenario3Step3After": "C 被過濾(已滿),剩餘:D", + "scenario3Step3Before": "供應商 C 並行限制 2,目前活躍 Session 數:2", + "scenario3Step3Decision": "Session 過期(5 分鐘)後自動釋放", + "scenario3Step3Desc": "檢查目前活躍 Session 數是否超過設定的並行限制", + "scenario3Title": "健康度篩選(熔斷器 + 限流)", + "scenario4Desc": "連續對話優先使用同一供應商,利用 Claude 的上下文快取", + "scenario4Step1": "檢查歷史請求", + "scenario4Step1After": "檢查 B 是否啟用且健康", + "scenario4Step1Before": "最近一次請求使用了供應商 B", + "scenario4Step1Decision": "B 可用,直接復用,跳過隨機選擇", + "scenario4Step1Desc": "查詢該 API Key 最近 10 秒內使用的供應商", + "scenario4Step2": "復用失效", + "scenario4Step2After": "進入正常選擇流程", + "scenario4Step2Before": "上次使用的供應商 B 已被停用或熔斷", + "scenario4Step2Decision": "從其他可用供應商中選擇", + "scenario4Step2Desc": "如果上次使用的供應商不可用,則重新選擇", + "scenario4Title": "Session 復用機制", + "scenariosTitle": "互動式場景演示", + "session": "Session 復用機制", + "sessionDesc": "如果上次使用的供應商不可用,則重新選擇", + "sessionExample": "最近一次請求使用了供應商 B", + "sessionLastUsed": "B 可用,直接復用,跳過隨機選擇", + "sessionExpired": "Session 過期(5 分鐘)後自動釋放", + "sessionFallback": "從其他可用供應商中選擇", + "sessionReuse": "4. Session 復用:連續對話復用同一供應商,節省上下文成本", + "sessionUnavailable": "上次使用的供應商 B 已被停用或熔斷", + "step": "步驟", + "title": "核心原則", + "weight": "使用權重進行隨機選擇,權重越高被選中機率越大", + "weightCalc": "C 被選中機率 75%,A 被選中機率 25%", + "weightExample": "C(權重 3)、A(權重 1)" +} diff --git a/messages/zh-TW/settings/providers/inlineEdit.json b/messages/zh-TW/settings/providers/inlineEdit.json new file mode 100644 index 000000000..f976de0e7 --- /dev/null +++ b/messages/zh-TW/settings/providers/inlineEdit.json @@ -0,0 +1,12 @@ +{ + "cancel": "放棄", + "costMultiplierInvalid": "請輸入大於等於 0 的數字", + "costMultiplierLabel": "成本倍數", + "priorityInvalid": "請輸入大於等於 0 的整數", + "priorityLabel": "優先級", + "save": "儲存", + "saveFailed": "儲存失敗", + "saveSuccess": "儲存成功", + "weightInvalid": "請輸入 1-100 之間的整數", + "weightLabel": "權重" +} diff --git a/messages/zh-TW/settings/providers/list.json b/messages/zh-TW/settings/providers/list.json new file mode 100644 index 000000000..bcc07e938 --- /dev/null +++ b/messages/zh-TW/settings/providers/list.json @@ -0,0 +1,37 @@ +{ + "cancelButton": "關閉", + "circuitBroken": "熔斷中", + "clipboardUnavailable": "目前環境無法使用剪貼簿,請手動選取複製。", + "confirmDeleteMessage": "確定要刪除供應商 \"{name}\" 嗎?此操作無法撤銷。", + "confirmDeleteTitle": "確認刪除供應商?", + "copyFailed": "複製失敗", + "costMultiplier": "成本倍數", + "deleteButton": "刪除", + "deleteError": "操作過程中出現異常", + "deleteFailed": "刪除失敗", + "deleteSuccess": "刪除成功", + "deleteSuccessDesc": "供應商 \"{name}\" 已刪除", + "getKeyFailed": "取得金鑰失敗", + "keyCopied": "金鑰已複製到剪貼簿", + "keyLoading": "載入中...", + "officialWebsite": "官網", + "priority": "優先級", + "resetCircuitFailed": "重置熔斷器失敗", + "resetCircuitSuccess": "熔斷器已重置", + "resetCircuitSuccessDesc": "供應商 \"{name}\" 的熔斷狀態已解除", + "resetUsageFailed": "重置總用量失敗", + "resetUsageSuccess": "總用量已重置", + "resetUsageSuccessDesc": "供應商 \"{name}\" 的總用量已重置", + "resetUsageTitle": "重置總用量", + "statusDisabled": "停用", + "statusEnabled": "啟用", + "todayUsageCount": "{count} 次呼叫", + "todayUsageLabel": "今日使用量", + "toggleFailed": "狀態切換失敗", + "toggleSuccess": "供應商已{status}", + "toggleSuccessDesc": "供應商 \"{name}\" 狀態已更新", + "unknownError": "未知錯誤", + "viewFullKey": "查看完整 API 金鑰", + "viewFullKeyDesc": "請妥善保管,不要洩露給他人", + "weight": "權重" +} diff --git a/messages/zh-TW/settings/providers/schedulingDialog.json b/messages/zh-TW/settings/providers/schedulingDialog.json new file mode 100644 index 000000000..339b7e411 --- /dev/null +++ b/messages/zh-TW/settings/providers/schedulingDialog.json @@ -0,0 +1,9 @@ +{ + "after": "過濾後:", + "before": "過濾前:", + "decision": "決策:", + "description": "了解系統如何智慧選擇上游供應商,確保高可用性和成本優化", + "step": "步驟", + "title": "供應商調度規則說明", + "triggerButton": "調度規則說明" +} diff --git a/messages/zh-TW/settings/providers/search.json b/messages/zh-TW/settings/providers/search.json new file mode 100644 index 000000000..7a8786747 --- /dev/null +++ b/messages/zh-TW/settings/providers/search.json @@ -0,0 +1,7 @@ +{ + "clear": "清除搜尋", + "found": "找到 {count} 個匹配的供應商", + "notFound": "未找到匹配的供應商", + "placeholder": "搜尋供應商名稱、URL、備註...", + "showing": "顯示 {filtered} / {total} 個供應商" +} diff --git a/messages/zh-TW/settings/providers/section.json b/messages/zh-TW/settings/providers/section.json new file mode 100644 index 000000000..5ad0c044a --- /dev/null +++ b/messages/zh-TW/settings/providers/section.json @@ -0,0 +1,5 @@ +{ + "description": "設定上游供應商的金額限流和並行限制,留空表示無限制。", + "leaderboard": "供應商排行榜", + "title": "供應商管理" +} diff --git a/messages/zh-TW/settings/providers/sort.json b/messages/zh-TW/settings/providers/sort.json new file mode 100644 index 000000000..70fbdd42a --- /dev/null +++ b/messages/zh-TW/settings/providers/sort.json @@ -0,0 +1,8 @@ +{ + "byActualPriority": "按實際選取順序", + "byCreatedAt": "按建立時間 (新-舊)", + "byName": "按名稱 (A-Z)", + "byPriority": "按優先級 (高-低)", + "byWeight": "按權重 (高-低)", + "placeholder": "排序供應商" +} diff --git a/messages/zh-TW/settings/providers/strings.json b/messages/zh-TW/settings/providers/strings.json new file mode 100644 index 000000000..740562585 --- /dev/null +++ b/messages/zh-TW/settings/providers/strings.json @@ -0,0 +1,47 @@ +{ + "add": "新增供應商", + "addFailed": "新增供應商失敗", + "addProvider": "新增供應商", + "addSuccess": "新增供應商成功", + "circuitBroken": "熔斷中", + "clone": "複製供應商", + "cloneFailed": "複製失敗", + "confirmDelete": "確定要刪除這個供應商嗎?", + "confirmDeleteDesc": "確定要刪除供應商「{name}」嗎?此操作無法復原。", + "confirmDeleteProvider": "確認刪除供應商?", + "confirmDeleteProviderDesc": "確定要刪除供應商「{name}」嗎?此操作無法復原。", + "createProvider": "新增供應商", + "delete": "刪除供應商", + "deleteFailed": "刪除供應商失敗", + "deleteSuccess": "刪除成功", + "description": "設定 API 供應商並維護可用狀態。", + "disabledStatus": "停用", + "displayCount": "顯示 {filtered} / {total} 個供應商", + "edit": "編輯供應商", + "editFailed": "更新供應商失敗", + "editProvider": "編輯供應商", + "enabledStatus": "啟用", + "keyLoading": "載入中...", + "noProviders": "尚未設定供應商", + "noProvidersDesc": "新增你的第一個 API 供應商", + "notFound": "未找到相符的供應商", + "official": "官方網站", + "resetCircuit": "熔斷器已重置", + "resetCircuitDesc": "供應商「{name}」的熔斷狀態已解除", + "resetCircuitFailed": "重置熔斷器失敗", + "scheduling": "調度策略詳解", + "schedulingDesc": "了解供應商選擇如何進行優先級分層、工作階段復用、負載均衡和故障轉移", + "searchNoResults": "找不到匹配的供應商", + "searchResults": "找到 {count} 個匹配的供應商", + "subtitle": "供應商管理", + "subtitleDesc": "設定上游供應商的金額限流和並行限制。留空表示無限制。", + "title": "供應商管理", + "todayUsage": "今日使用量", + "todayUsageCount": "{count} 回", + "toggleFailed": "狀態切換失敗", + "toggleSuccess": "供應商已{status}", + "toggleSuccessDesc": "供應商「{name}」狀態已更新", + "updateFailed": "更新供應商失敗", + "viewKey": "查看完整的 API Key", + "viewKeyDesc": "請妥善保管,不要外泄給他人" +} diff --git a/messages/zh-TW/settings/providers/types.json b/messages/zh-TW/settings/providers/types.json new file mode 100644 index 000000000..f9a5d6ad6 --- /dev/null +++ b/messages/zh-TW/settings/providers/types.json @@ -0,0 +1,26 @@ +{ + "claude": { + "description": "Anthropic 官方 API(原生)", + "label": "Claude" + }, + "claudeAuth": { + "description": "Claude 中繼服務", + "label": "Claude Auth" + }, + "codex": { + "description": "Codex CLI API", + "label": "Codex" + }, + "gemini": { + "description": "Google Gemini API", + "label": "Gemini" + }, + "geminiCli": { + "description": "Gemini CLI API", + "label": "Gemini CLI" + }, + "openaiCompatible": { + "description": "OpenAI 相容 API", + "label": "OpenAI Compatible" + } +} diff --git a/messages/zh-TW/settings/requestFilters.json b/messages/zh-TW/settings/requestFilters.json new file mode 100644 index 000000000..70abe4f4a --- /dev/null +++ b/messages/zh-TW/settings/requestFilters.json @@ -0,0 +1,84 @@ +{ + "actionLabel": { + "json_path": "JSON 路徑替換", + "remove": "刪除 Header", + "set": "設定 Header", + "text_replace": "文字替換" + }, + "add": "新增過濾器", + "addFailed": "建立失敗", + "addSuccess": "建立成功", + "applyToAll": "應用於所有請求", + "confirmDelete": "確定刪除過濾器「{name}」?", + "delete": "刪除過濾器", + "deleteFailed": "刪除失敗", + "deleteSuccess": "刪除成功", + "description": "上游轉發前先刪除/覆寫標頭或替換 Body,實現脫敏與格式化。", + "dialog": { + "action": "動作", + "bindingGlobal": "所有Provider(全域)", + "bindingGroups": "Provider分組", + "bindingProviders": "指定 Provider", + "bindingType": "套用範圍", + "clear": "清除", + "createTitle": "新增過濾器", + "description": "描述(選填)", + "editTitle": "編輯過濾器", + "groupsSelected": "已選 {count} 個分組", + "jsonPathPlaceholder": "例如:messages.0.content 或 data.items[0].token", + "loading": "載入中...", + "matchType": "匹配類型", + "matchTypeContains": "含有", + "matchTypeExact": "精確", + "matchTypeRegex": "正則", + "name": "名稱", + "noGroupsFound": "找不到分組", + "noProvidersFound": "找不到Provider", + "priority": "優先級", + "providersSelected": "已選 {count} 個Provider", + "replacement": "替換值(選填)", + "replacementPlaceholder": "字串或 JSON,留空為清除", + "save": "儲存", + "saving": "儲存中...", + "scope": "範圍", + "searchGroups": "搜尋分組...", + "searchProviders": "搜尋Provider...", + "selectAll": "全選", + "selectGroups": "選擇分組...", + "selectProviders": "選擇Provider...", + "target": "目標欄位/路徑", + "targetPlaceholder": "Header 名稱或文字/路徑", + "validation": { + "fieldRequired": "名稱和目標為必填項" + } + }, + "disable": "已停用", + "edit": "編輯過濾器", + "editFailed": "更新失敗", + "editSuccess": "更新完成", + "empty": "尚無過濾器,點選右上角新增。", + "enable": "已啟用", + "groups": "群組", + "nav": "請求過濾", + "providers": "供應商", + "refreshCache": "刷新快取", + "refreshFailed": "刷新失敗", + "refreshSuccess": "快取已刷新,載入 {count} 條過濾器", + "scopeLabel": { + "body": "Body", + "header": "Header" + }, + "table": { + "action": "動作", + "actions": "動作", + "apply": "範圍", + "createdAt": "建立時間", + "name": "名稱", + "priority": "優先級", + "replacement": "替換值", + "scope": "範圍", + "status": "狀態", + "target": "目標" + }, + "title": "請求過濾器" +} diff --git a/messages/zh-TW/settings/sensitiveWords.json b/messages/zh-TW/settings/sensitiveWords.json new file mode 100644 index 000000000..5f586acce --- /dev/null +++ b/messages/zh-TW/settings/sensitiveWords.json @@ -0,0 +1,55 @@ +{ + "add": "新增敏感詞", + "addFailed": "建立敏感詞失敗", + "addSuccess": "敏感詞建立成功", + "cacheStats": "快取統計:包含({containsCount}) 精確({exactCount}) 正則({regexCount})", + "confirmDelete": "確定要刪除敏感詞「{word}」嗎?", + "delete": "刪除敏感詞", + "deleteFailed": "刪除失敗", + "deleteSuccess": "敏感詞刪除成功", + "description": "設定敏感詞過濾規則,攔截包含敏感內容的請求。", + "dialog": { + "addDescription": "配置敏感詞過濾規則,被命中的請求將不會轉發到上游。", + "addTitle": "新增敏感詞", + "creating": "建立中...", + "descriptionLabel": "說明", + "descriptionPlaceholder": "選填:新增說明...", + "editDescription": "修改敏感詞配置,更改後將自動刷新快取。", + "editTitle": "編輯敏感詞", + "matchTypeContains": "包含比對 - 文字中包含該詞即攔截", + "matchTypeExact": "精確比對 - 完全比對該詞才攔截", + "matchTypeLabel": "比對類型 *", + "matchTypeRegex": "正則表達式 - 支援複雜模式比對", + "saving": "儲存中...", + "wordLabel": "敏感詞 *", + "wordPlaceholder": "輸入敏感詞...", + "wordRequired": "請輸入敏感詞" + }, + "disable": "敏感詞已停用", + "edit": "編輯敏感詞", + "editFailed": "更新敏感詞失敗", + "editSuccess": "敏感詞更新成功", + "emptyState": "暫無敏感詞,點選右上角「新增敏感詞」開始配置。", + "enable": "敏感詞已啟用", + "refreshCache": "重新整理快取", + "refreshCacheFailed": "刷新快取失敗", + "refreshCacheSuccess": "快取刷新成功,已載入 {count} 個敏感詞", + "section": { + "description": "被敏感詞攔截的請求不會轉發到上游,也不會計費。支援包含比對、精確比對與正則表達式三種模式。", + "title": "敏感詞列表" + }, + "table": { + "actions": "動作", + "createdAt": "建立時間", + "description": "說明", + "matchType": "比對類型", + "matchTypeContains": "包含比對", + "matchTypeExact": "精確比對", + "matchTypeRegex": "正則表達式", + "status": "狀態", + "word": "敏感詞" + }, + "title": "敏感詞管理", + "toggleFailed": "狀態切換失敗", + "toggleFailedError": "狀態切換失敗:" +} diff --git a/messages/zh-TW/settings/strings.json b/messages/zh-TW/settings/strings.json new file mode 100644 index 000000000..111beab87 --- /dev/null +++ b/messages/zh-TW/settings/strings.json @@ -0,0 +1,22 @@ +{ + "mcpPassthroughConfig": "MCP 透傳配置", + "mcpPassthroughConfigCustom": "自定義 (預留)", + "mcpPassthroughConfigGlm": "智譜 GLM", + "mcpPassthroughConfigMinimax": "Minimax", + "mcpPassthroughConfigNone": "不啟用", + "mcpPassthroughCustomDesc": "透傳到自定義 MCP 服務(預留,暫未實現)", + "mcpPassthroughCustomLabel": "自定義", + "mcpPassthroughDesc": "啟用後,將 MCP 工具調用透傳到指定的 AI 服務商(如 minimax 的圖片識別、聯網搜索)", + "mcpPassthroughGlmDesc": "透傳到智譜 GLM MCP 服務(支持圖片分析、視頻分析等工具)", + "mcpPassthroughGlmLabel": "智譜 GLM", + "mcpPassthroughHint": "提示: MCP 透傳功能允許 Claude Code 客戶端使用第三方 AI 服務商提供的工具能力(如圖片識別、聯網搜索)", + "mcpPassthroughMinimaxDesc": "透傳到 minimax MCP 服務(支持圖片識別、聯網搜索等工具)", + "mcpPassthroughMinimaxLabel": "Minimax", + "mcpPassthroughNoneDesc": "不啟用 MCP 透傳功能(默認)", + "mcpPassthroughNoneLabel": "不啟用", + "mcpPassthroughSelect": "透傳類型", + "mcpPassthroughUrlAuto": "自動提取: {url}", + "mcpPassthroughUrlDesc": "MCP 服務的基礎 URL。留空則自動從提供商 URL 提取基礎域名", + "mcpPassthroughUrlLabel": "MCP 透傳 URL", + "mcpPassthroughUrlPlaceholder": "https://api.minimaxi.com" +} diff --git a/package.json b/package.json index 13c3dfdd7..14d6e219e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,11 @@ "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", - "validate:migrations": "node scripts/validate-migrations.js" + "validate:migrations": "node scripts/validate-migrations.js", + "i18n:audit-placeholders": "node scripts/audit-settings-placeholders.js --scope=settings,dashboard,myUsage --format=tsv", + "i18n:audit-placeholders:fail": "node scripts/audit-settings-placeholders.js --scope=settings,dashboard,myUsage --format=tsv --fail", + "i18n:audit-messages-no-emoji": "node scripts/audit-messages-no-emoji.js --format=tsv", + "i18n:audit-messages-no-emoji:fail": "node scripts/audit-messages-no-emoji.js --format=tsv --fail" }, "dependencies": { "@bull-board/api": "^6", diff --git a/scripts/audit-messages-emoji.js b/scripts/audit-messages-emoji.js new file mode 100644 index 000000000..9d9201080 --- /dev/null +++ b/scripts/audit-messages-emoji.js @@ -0,0 +1,212 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const EMOJI_RE = + /(\p{Extended_Pictographic}|\p{Regional_Indicator}{2}|[0-9#*]\uFE0F?\u20E3)/gu; + +function isObject(v) { + return v && typeof v === "object" && !Array.isArray(v); +} + +function flattenLeafStrings(value, prefix = "") { + if (typeof value === "string") return [{ key: prefix, value }]; + if (!value || typeof value !== "object") return []; + + if (Array.isArray(value)) { + return value.flatMap((v, index) => { + const key = prefix ? `${prefix}.${index}` : String(index); + return flattenLeafStrings(v, key); + }); + } + + return Object.entries(value).flatMap(([k, v]) => { + const key = prefix ? `${prefix}.${k}` : k; + if (isObject(v) || Array.isArray(v)) return flattenLeafStrings(v, key); + return flattenLeafStrings(v, key); + }); +} + +function listJsonFiles(dir) { + const out = []; + const walk = (d) => { + for (const entry of fs.readdirSync(d, { withFileTypes: true })) { + const full = path.join(d, entry.name); + if (entry.isDirectory()) { + walk(full); + continue; + } + if (entry.isFile() && entry.name.endsWith(".json")) out.push(full); + } + }; + if (fs.existsSync(dir)) walk(dir); + return out.sort((a, b) => a.localeCompare(b)); +} + +function fileToKeyPrefix(relFile) { + const segs = relFile.replace(/\.json$/, "").split(path.sep); + if (segs[segs.length - 1] === "strings") return segs.slice(0, -1).join("."); + return segs.join("."); +} + +function loadJson(p) { + return JSON.parse(fs.readFileSync(p, "utf8")); +} + +function countEmojiCodepoints(s) { + EMOJI_RE.lastIndex = 0; + let count = 0; + // eslint-disable-next-line no-unused-vars + for (const _ of s.matchAll(EMOJI_RE)) count += 1; + return count; +} + +function maskEmoji(s) { + EMOJI_RE.lastIndex = 0; + return s.replace(EMOJI_RE, ""); +} + +function normalizeLocales(messagesRoot, locales) { + if (typeof locales === "string") return normalizeLocales(messagesRoot, [locales]); + if (Array.isArray(locales) && locales.length > 0) { + return locales + .flatMap((s) => String(s).split(",")) + .map((s) => s.trim()) + .filter(Boolean); + } + + const dirs = fs.readdirSync(messagesRoot, { withFileTypes: true }); + return dirs + .filter((d) => d.isDirectory() && !d.name.startsWith(".")) + .map((d) => d.name) + .sort((a, b) => a.localeCompare(b)); +} + +function findMessagesEmoji({ messagesDir, locales }) { + const root = messagesDir || path.join(process.cwd(), "messages"); + const targets = normalizeLocales(root, locales); + const rows = []; + + for (const locale of targets) { + const localeDir = path.join(root, locale); + if (!fs.existsSync(localeDir) || !fs.statSync(localeDir).isDirectory()) continue; + + const files = listJsonFiles(localeDir); + for (const file of files) { + const relFile = path.relative(localeDir, file); + const keyPrefix = fileToKeyPrefix(relFile); + const obj = loadJson(file); + + for (const leaf of flattenLeafStrings(obj)) { + if (typeof leaf.value !== "string") continue; + const emojiCount = countEmojiCodepoints(leaf.value); + if (emojiCount === 0) continue; + + const fullKey = keyPrefix + ? leaf.key + ? `${keyPrefix}.${leaf.key}` + : keyPrefix + : leaf.key; + + rows.push({ + locale, + relFile: relFile.replaceAll(path.sep, "/"), + key: fullKey, + emojiCount, + preview: maskEmoji(leaf.value), + }); + } + } + } + + const byLocaleCount = {}; + const byFileCount = {}; + let totalEmojiCount = 0; + + for (const r of rows) { + byLocaleCount[r.locale] = (byLocaleCount[r.locale] || 0) + 1; + const fileKey = `${r.locale}/${r.relFile}`; + byFileCount[fileKey] = (byFileCount[fileKey] || 0) + 1; + totalEmojiCount += r.emojiCount; + } + + const sortedRows = rows.sort((a, b) => { + const c0 = a.locale.localeCompare(b.locale); + if (c0 !== 0) return c0; + const c1 = a.relFile.localeCompare(b.relFile); + if (c1 !== 0) return c1; + return a.key.localeCompare(b.key); + }); + + return { + rows: sortedRows, + totalRowCount: sortedRows.length, + totalEmojiCount, + byLocaleCount, + byFileCount, + }; +} + +function topFiles(byFileCount, limit = 10) { + return Object.entries(byFileCount) + .map(([k, v]) => ({ key: k, count: v })) + .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)) + .slice(0, limit) + .map(({ key, count }) => { + const [locale, ...rest] = key.split("/"); + return { locale, relFile: rest.join("/"), count }; + }); +} + +function run(argv) { + const messagesDirArg = argv.find((a) => a.startsWith("--messagesDir=")); + const messagesDir = messagesDirArg ? messagesDirArg.split("=", 2)[1] : undefined; + const localesArg = argv.find((a) => a.startsWith("--locales=")); + const locales = localesArg ? localesArg.split("=", 2)[1] : undefined; + const formatArg = argv.find((a) => a.startsWith("--format=")); + const format = formatArg ? formatArg.split("=", 2)[1] : "text"; + + const report = findMessagesEmoji({ messagesDir, locales }); + const total = report.totalRowCount; + + if (total === 0) { + return { exitCode: 0, lines: ["OK: no emoji found in messages JSON."] }; + } + + if (format === "json") { + return { exitCode: 0, lines: [JSON.stringify(report.rows, null, 2)] }; + } + + if (format === "tsv") { + const lines = ["locale\trelFile\tkey\temojiCount\tpreview"]; + for (const r of report.rows) { + lines.push(`${r.locale}\t${r.relFile}\t${r.key}\t${r.emojiCount}\t${r.preview}`); + } + return { exitCode: 0, lines }; + } + + const lines = [ + `Found ${total} messages strings containing emoji (${report.totalEmojiCount} total emoji codepoints).`, + "Top files by row count (locale\trelFile\trows):", + ...topFiles(report.byFileCount).map((r) => `${r.locale}\t${r.relFile}\t${r.count}`), + "Rows (locale\trelFile\tkey):", + ...report.rows.map((r) => `${r.locale}\t${r.relFile}\t${r.key}`), + ]; + + return { exitCode: 0, lines }; +} + +module.exports = { + countEmojiCodepoints, + fileToKeyPrefix, + findMessagesEmoji, + flattenLeafStrings, + listJsonFiles, + maskEmoji, + run, +}; + +if (require.main === module) { + const out = run(process.argv.slice(2)); + for (const line of out.lines) console.log(line); // eslint-disable-line no-console + process.exit(out.exitCode); +} diff --git a/scripts/audit-messages-no-emoji.js b/scripts/audit-messages-no-emoji.js new file mode 100644 index 000000000..be6190862 --- /dev/null +++ b/scripts/audit-messages-no-emoji.js @@ -0,0 +1,137 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const emojiAudit = require("./audit-messages-emoji.js"); + +const EMOJI_RE = + /(\p{Extended_Pictographic}|\p{Regional_Indicator}{2}|[0-9#*]\uFE0F?\u20E3)/gu; + +function loadJson(p) { + return JSON.parse(fs.readFileSync(p, "utf8")); +} + +function normalizeLocales(messagesRoot, locales) { + if (typeof locales === "string") return normalizeLocales(messagesRoot, [locales]); + if (Array.isArray(locales) && locales.length > 0) { + return locales + .flatMap((s) => String(s).split(",")) + .map((s) => s.trim()) + .filter(Boolean); + } + + const dirs = fs.readdirSync(messagesRoot, { withFileTypes: true }); + return dirs + .filter((d) => d.isDirectory() && !d.name.startsWith(".")) + .map((d) => d.name) + .sort((a, b) => a.localeCompare(b)); +} + +function toCodepoint(cp) { + const hex = cp.toString(16).toUpperCase(); + return `U+${hex.padStart(4, "0")}`; +} + +function listEmojiCodepoints(value) { + EMOJI_RE.lastIndex = 0; + const out = []; + for (const m of value.matchAll(EMOJI_RE)) { + const seq = Array.from(m[0]) + .map((ch) => ch.codePointAt(0)) + .filter((cp) => typeof cp === "number") + .map((cp) => toCodepoint(cp)) + .join("+"); + if (seq) out.push(seq); + } + return out; +} + +function findMessagesEmojiMatches({ messagesDir, locales }) { + const root = messagesDir || path.join(process.cwd(), "messages"); + const targets = normalizeLocales(root, locales); + const rows = []; + + for (const locale of targets) { + const localeDir = path.join(root, locale); + if (!fs.existsSync(localeDir) || !fs.statSync(localeDir).isDirectory()) continue; + + const files = emojiAudit.listJsonFiles(localeDir); + for (const file of files) { + const relFileNative = path.relative(localeDir, file); + const relFilePosix = relFileNative.replaceAll(path.sep, "/"); + const keyPrefix = emojiAudit.fileToKeyPrefix(relFileNative); + const obj = loadJson(file); + + for (const leaf of emojiAudit.flattenLeafStrings(obj)) { + if (typeof leaf.value !== "string") continue; + const emojiCount = emojiAudit.countEmojiCodepoints(leaf.value); + if (emojiCount === 0) continue; + + const fullKey = keyPrefix + ? leaf.key + ? `${keyPrefix}.${leaf.key}` + : keyPrefix + : leaf.key; + + const codepoints = Array.from(new Set(listEmojiCodepoints(leaf.value))).sort((a, b) => + a.localeCompare(b) + ); + + rows.push({ + file: path.posix.join("messages", locale, relFilePosix), + key: fullKey, + emojiCount, + codepoints, + }); + } + } + } + + return rows.sort((a, b) => a.file.localeCompare(b.file) || a.key.localeCompare(b.key)); +} + +function run(argv) { + const fail = argv.includes("--fail"); + const messagesDirArg = argv.find((a) => a.startsWith("--messagesDir=")); + const messagesDir = messagesDirArg ? messagesDirArg.split("=", 2)[1] : undefined; + const localesArg = argv.find((a) => a.startsWith("--locales=")); + const locales = localesArg ? localesArg.split("=", 2)[1] : undefined; + const formatArg = argv.find((a) => a.startsWith("--format=")); + const format = formatArg ? formatArg.split("=", 2)[1] : "text"; + + const rows = findMessagesEmojiMatches({ messagesDir, locales }); + const total = rows.length; + + if (total === 0) { + return { exitCode: 0, lines: ["OK: no emoji found in messages JSON."] }; + } + + const exitCode = fail ? 1 : 0; + + if (format === "json") { + return { exitCode, lines: [JSON.stringify(rows, null, 2)] }; + } + + if (format === "tsv") { + const lines = ["file\tkey\temojiCount\tcodepoints"]; + for (const r of rows) { + lines.push(`${r.file}\t${r.key}\t${r.emojiCount}\t${r.codepoints.join(",")}`); + } + return { exitCode, lines }; + } + + const lines = [`Found ${total} messages strings containing emoji:`]; + for (const r of rows) lines.push(`${r.file}\t${r.key}\t${r.codepoints.join(",")}`); + return { exitCode, lines }; +} + +module.exports = { + findMessagesEmojiMatches, + listEmojiCodepoints, + run, + toCodepoint, +}; + +if (require.main === module) { + const out = run(process.argv.slice(2)); + for (const line of out.lines) console.log(line); // eslint-disable-line no-console + process.exit(out.exitCode); +} diff --git a/scripts/audit-settings-placeholders.allowlist.json b/scripts/audit-settings-placeholders.allowlist.json new file mode 100644 index 000000000..47f9ff4d5 --- /dev/null +++ b/scripts/audit-settings-placeholders.allowlist.json @@ -0,0 +1,41 @@ +{ + "entries": [ + { + "key": "providers.form.codexInstructions", + "reason": "zh-cn==zh-tw title term" + }, + { + "key": "providers.form.codexInstructionsKeep", + "reason": "zh-cn==zh-tw common ui term" + }, + { + "key": "providers.form.codexStrategyConfig", + "reason": "zh-cn==zh-tw title term" + }, + { + "key": "providers.form.costMultiplierLabel", + "reason": "zh-cn==zh-tw common ui term" + }, + { + "key": "providers.form.modelWhitelistClear", + "reason": "zh-cn==zh-tw common ui term" + }, + { + "key": "providers.form.modelWhitelistNotFound", + "reason": "zh-cn==zh-tw common ui term" + }, + { + "key": "providers.form.proxy", + "reason": "zh-cn==zh-tw common ui term" + }, + { + "key": "providers.form.proxyTestResultConnectionMethodProxy", + "reason": "zh-cn==zh-tw common ui term" + }, + { + "key": "providers.form.sortByCost", + "reason": "zh-cn==zh-tw common ui term" + } + ], + "glossary": [] +} diff --git a/scripts/audit-settings-placeholders.js b/scripts/audit-settings-placeholders.js new file mode 100644 index 000000000..b0fc256e7 --- /dev/null +++ b/scripts/audit-settings-placeholders.js @@ -0,0 +1,279 @@ +/* + * Audit for zh-CN placeholder strings accidentally copied into other locales' split settings. + * + * Rule: + * - For each non-canonical locale, if a leaf string equals the canonical (zh-CN) leaf string at + * the same key path, consider it a "placeholder candidate". + * + * Output includes: + * - locale + * - relFile (relative to messages/, e.g. settings/config.json or dashboard.json) + * - key (full key path, prefixed by file name, e.g. config.form.enableHttp2Desc) + * - value (target value, equals zh-CN) + * - reason (stable machine-readable string) + */ +const fs = require("node:fs"); +const path = require("node:path"); +const sync = require("./sync-settings-keys.js"); + +const CANONICAL = "zh-CN"; +const DEFAULT_TARGET_LOCALES = ["en", "ja", "ru", "zh-TW"]; +const SCOPES = ["settings", "dashboard", "myUsage"]; + +function isObject(v) { + return v && typeof v === "object" && !Array.isArray(v); +} + +function flatten(obj, prefix = "") { + const out = {}; + for (const [k, v] of Object.entries(obj || {})) { + const key = prefix ? `${prefix}.${k}` : k; + if (isObject(v)) Object.assign(out, flatten(v, key)); + else out[key] = v; + } + return out; +} + +function listJsonFiles(dir) { + const out = []; + const walk = (d) => { + for (const entry of fs.readdirSync(d, { withFileTypes: true })) { + const full = path.join(d, entry.name); + if (entry.isDirectory()) { + walk(full); + continue; + } + if (entry.isFile() && entry.name.endsWith(".json")) out.push(full); + } + }; + if (fs.existsSync(dir)) walk(dir); + return out; +} + +function fileToKeyPrefix(relFile) { + const segs = relFile.replace(/\.json$/, "").split(path.sep); + if (segs[segs.length - 1] === "strings") return segs.slice(0, -1).join("."); + return segs.join("."); +} + +function loadJson(p) { + return JSON.parse(fs.readFileSync(p, "utf8")); +} + +function hasHanChars(s) { + return /[\u4E00-\u9FFF]/.test(s); +} + +function loadAllowlist(allowlistPath) { + if (!allowlistPath) return null; + if (!fs.existsSync(allowlistPath)) return null; + const data = loadJson(allowlistPath); + const entries = Array.isArray(data?.entries) ? data.entries : []; + const glossary = Array.isArray(data?.glossary) ? data.glossary : []; + return { entries, glossary }; +} + +function isAllowedByAllowlist(row, allowlist) { + if (!allowlist) return { allowed: false, allowReason: null }; + + for (const term of allowlist.glossary || []) { + if (typeof term === "string" && term.length > 0 && row.value.includes(term)) { + return { allowed: true, allowReason: `glossary:${term}` }; + } + } + + for (const entry of allowlist.entries || []) { + if (!entry || typeof entry !== "object") continue; + const reason = typeof entry.reason === "string" && entry.reason ? entry.reason : "allowlisted"; + + if (typeof entry.key === "string" && entry.key === row.key) { + return { allowed: true, allowReason: `key:${reason}` }; + } + if (typeof entry.keyPrefix === "string" && row.key.startsWith(entry.keyPrefix)) { + return { allowed: true, allowReason: `keyPrefix:${reason}` }; + } + if (typeof entry.keyRegex === "string") { + const re = new RegExp(entry.keyRegex); + if (re.test(row.key)) return { allowed: true, allowReason: `keyRegex:${reason}` }; + } + if (typeof entry.valueRegex === "string") { + const re = new RegExp(entry.valueRegex); + if (re.test(row.value)) return { allowed: true, allowReason: `valueRegex:${reason}` }; + } + } + + return { allowed: false, allowReason: null }; +} + +function normalizeScopes(scopes) { + if (typeof scopes === "string") return normalizeScopes([scopes]); + if (!scopes || scopes.length === 0) return ["settings"]; + const normalized = scopes + .flatMap((s) => String(s).split(",")) + .map((s) => s.trim()) + .filter(Boolean); + const unknown = normalized.filter((s) => !SCOPES.includes(s)); + if (unknown.length > 0) { + throw new Error(`Unknown scope(s): ${unknown.join(", ")} (supported: ${SCOPES.join(", ")})`); + } + return normalized; +} + +function normalizeLocales(locales) { + if (typeof locales === "string") return normalizeLocales([locales]); + if (!locales || locales.length === 0) return DEFAULT_TARGET_LOCALES; + return locales + .flatMap((s) => String(s).split(",")) + .map((s) => s.trim()) + .filter(Boolean); +} + +function listCanonicalFilesForScope(messagesRoot, scope) { + if (scope === "settings") { + const cnDir = path.join(messagesRoot, CANONICAL, "settings"); + return listJsonFiles(cnDir).map((p) => { + const rel = path.relative(cnDir, p); + return { + relFile: rel, + canonicalPath: p, + keyPrefix: fileToKeyPrefix(rel), + }; + }); + } + + const file = `${scope}.json`; + const canonicalPath = path.join(messagesRoot, CANONICAL, file); + if (!fs.existsSync(canonicalPath)) return []; + return [{ relFile: file, canonicalPath, keyPrefix: scope }]; +} + +function findSettingsPlaceholders({ messagesDir, locales, scopes, allowlistPath }) { + const root = messagesDir || path.join(process.cwd(), "messages"); + const targets = normalizeLocales(locales); + const scopeList = normalizeScopes(scopes); + const allowlist = loadAllowlist(allowlistPath); + let allowlistedCount = 0; + + const rows = []; + for (const locale of targets) { + for (const scope of scopeList) { + const files = listCanonicalFilesForScope(root, scope); + for (const f of files) { + const tPath = + scope === "settings" + ? path.join(root, locale, "settings", f.relFile) + : path.join(root, locale, f.relFile); + if (!fs.existsSync(tPath)) continue; + + const cnObj = loadJson(f.canonicalPath); + const tObj = loadJson(tPath); + const cnFlat = flatten(cnObj); + const tFlat = flatten(tObj); + + for (const [leafKey, cnVal] of Object.entries(cnFlat)) { + const tVal = tFlat[leafKey]; + if (typeof cnVal !== "string" || typeof tVal !== "string") continue; + if (!hasHanChars(cnVal)) continue; + if (tVal !== cnVal) continue; + + const fullKey = f.keyPrefix + ? leafKey + ? `${f.keyPrefix}.${leafKey}` + : f.keyPrefix + : leafKey; + + rows.push({ + locale, + relFile: f.relFile, + key: fullKey, + value: tVal, + reason: "same_as_zh-CN", + }); + } + } + } + } + + const filtered = []; + for (const row of rows) { + const res = isAllowedByAllowlist(row, allowlist); + if (res.allowed) { + allowlistedCount += 1; + continue; + } + filtered.push(row); + } + + return { + rows: filtered, + allowlistedCount, + byLocaleCount: filtered.reduce( + (acc, r) => ((acc[r.locale] = (acc[r.locale] || 0) + 1), acc), + {} + ), + }; +} + +function run(argv) { + const fail = argv.includes("--fail"); + const messagesDirArg = argv.find((a) => a.startsWith("--messagesDir=")); + const messagesDir = messagesDirArg ? messagesDirArg.split("=", 2)[1] : undefined; + const localesArg = argv.find((a) => a.startsWith("--locales=")); + const locales = localesArg ? localesArg.split("=", 2)[1] : undefined; + const scopeArg = argv.find((a) => a.startsWith("--scope=")); + const scopes = scopeArg ? scopeArg.split("=", 2)[1] : undefined; + const formatArg = argv.find((a) => a.startsWith("--format=")); + const format = formatArg ? formatArg.split("=", 2)[1] : "text"; + const allowlistArg = argv.find((a) => a.startsWith("--allowlist=")); + const allowlistPath = allowlistArg ? allowlistArg.split("=", 2)[1] : path.join(process.cwd(), "scripts", "audit-settings-placeholders.allowlist.json"); + + const report = findSettingsPlaceholders({ messagesDir, locales, scopes, allowlistPath }); + const total = report.rows.length; + + if (total === 0) { + return { + exitCode: 0, + lines: ["OK: no zh-CN placeholder candidates found in split settings."], + }; + } + + if (format === "json") { + return { + exitCode: fail ? 1 : 0, + lines: [JSON.stringify(report.rows, null, 2)], + }; + } + + if (format === "tsv") { + const lines = ["locale\trelFile\tkey\tvalue\treason"]; + for (const r of report.rows) { + lines.push(`${r.locale}\t${r.relFile}\t${r.key}\t${r.value}\t${r.reason}`); + } + return { exitCode: fail ? 1 : 0, lines }; + } + + const lines = [`Found ${total} zh-CN placeholder candidates:`]; + for (const r of report.rows) { + lines.push(`${r.locale}\t${r.relFile}\t${r.key}`); + } + + return { exitCode: fail ? 1 : 0, lines }; +} + +module.exports = { + findSettingsPlaceholders, + flatten, + listJsonFiles, + fileToKeyPrefix, + run, +}; + +if (require.main === module) { + // Validate script API compatibility: sync script must remain require()-able. + if (!sync || typeof sync.loadSplitSettings !== "function") { + throw new Error("scripts/sync-settings-keys.js exports are not available (expected loadSplitSettings)"); + } + const out = run(process.argv.slice(2)); + for (const line of out.lines) console.log(line); // eslint-disable-line no-console + process.exit(out.exitCode); +} diff --git a/scripts/sync-settings-keys.js b/scripts/sync-settings-keys.js index 792479e02..e9e592133 100644 --- a/scripts/sync-settings-keys.js +++ b/scripts/sync-settings-keys.js @@ -1,17 +1,26 @@ /* - * Synchronize keys of settings.json across locales using zh-CN as canonical. - * - Ensures every locale has exactly the same set of nested keys + * Synchronize keys of settings messages across locales using zh-CN as canonical. + * + * Supports both layouts: + * - legacy: messages//settings.json + * - split: messages//settings/ (recursive .json files, assembled by messages//settings/index.ts) + * + * Behavior: + * - Ensures every locale has exactly the same set of nested keys as canonical (zh-CN) * - Keeps existing translations where keys exist * - Fills missing keys with zh-CN text as placeholder - * - Drops extra keys not present in zh-CN (notably for en, but applies consistently) + * - Drops extra keys not present in zh-CN (applies consistently to all locales) */ const fs = require("node:fs"); const path = require("node:path"); -const ROOT = process.cwd(); -const MESSAGES_DIR = path.join(ROOT, "messages"); const LOCALES = ["en", "ja", "ru", "zh-TW"]; const CANONICAL = "zh-CN"; +const DEFAULT_MESSAGES_DIR = path.join(process.cwd(), "messages"); + +function getMessagesDir(messagesDir) { + return messagesDir || DEFAULT_MESSAGES_DIR; +} function isObject(v) { return v && typeof v === "object" && !Array.isArray(v); @@ -64,12 +73,115 @@ function saveJSON(p, data) { fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); } -function ensureSettings(locale) { - const cnPath = path.join(MESSAGES_DIR, CANONICAL, "settings.json"); - const targetPath = path.join(MESSAGES_DIR, locale, "settings.json"); +function listJsonFiles(dir) { + const out = []; + const walk = (d) => { + for (const entry of fs.readdirSync(d, { withFileTypes: true })) { + const full = path.join(d, entry.name); + if (entry.isDirectory()) { + walk(full); + continue; + } + if (entry.isFile() && entry.name.endsWith(".json")) out.push(full); + } + }; + if (fs.existsSync(dir)) walk(dir); + return out; +} - const cn = loadJSON(cnPath); - const t = loadJSON(targetPath); +function getPath(obj, segments) { + let cur = obj; + for (const s of segments) { + if (!isObject(cur) || !Object.hasOwn(cur, s)) return undefined; + cur = cur[s]; + } + return cur; +} + +function loadSplitSettings(locale, messagesDir) { + const settingsDir = path.join(getMessagesDir(messagesDir), locale, "settings"); + if (!fs.existsSync(settingsDir)) return null; + + const top = {}; + for (const file of fs.readdirSync(settingsDir, { withFileTypes: true })) { + if (file.isDirectory()) continue; + if (!file.name.endsWith(".json")) continue; + const name = file.name.replace(/\.json$/, ""); + const v = loadJSON(path.join(settingsDir, file.name)); + if (name === "strings") Object.assign(top, v); + else top[name] = v; + } + + const providersDir = path.join(settingsDir, "providers"); + const providers = {}; + if (fs.existsSync(providersDir)) { + for (const file of fs.readdirSync(providersDir, { withFileTypes: true })) { + if (file.isDirectory()) continue; + if (!file.name.endsWith(".json")) continue; + const name = file.name.replace(/\.json$/, ""); + const v = loadJSON(path.join(providersDir, file.name)); + if (name === "strings") Object.assign(providers, v); + else providers[name] = v; + } + + const formDir = path.join(providersDir, "form"); + const form = {}; + if (fs.existsSync(formDir)) { + for (const file of fs.readdirSync(formDir, { withFileTypes: true })) { + if (file.isDirectory()) continue; + if (!file.name.endsWith(".json")) continue; + const name = file.name.replace(/\.json$/, ""); + const v = loadJSON(path.join(formDir, file.name)); + if (name === "strings") Object.assign(form, v); + else form[name] = v; + } + } + providers.form = form; + } + + top.providers = providers; + return top; +} + +function saveSplitSettingsFromCanonical(locale, merged, messagesDir) { + const root = getMessagesDir(messagesDir); + const cnSettingsDir = path.join(root, CANONICAL, "settings"); + const targetSettingsDir = path.join(root, locale, "settings"); + + const cnFiles = listJsonFiles(cnSettingsDir).map((p) => path.relative(cnSettingsDir, p)); + for (const rel of cnFiles) { + const cnPath = path.join(cnSettingsDir, rel); + const targetPath = path.join(targetSettingsDir, rel); + const segs = rel.replace(/\.json$/, "").split(path.sep); + + // special: strings.json means "spread into parent object" + if (segs[segs.length - 1] === "strings") { + const tmpl = loadJSON(cnPath); + const parentSegs = segs.slice(0, -1); + const parent = parentSegs.length ? getPath(merged, parentSegs) : merged; + const out = {}; + for (const k of Object.keys(tmpl)) out[k] = parent?.[k]; + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + saveJSON(targetPath, sortKeysDeep(out)); + continue; + } + + const v = getPath(merged, segs); + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + saveJSON(targetPath, sortKeysDeep(v)); + } +} + +function ensureSettings(locale, messagesDir) { + const root = getMessagesDir(messagesDir); + const cnSplit = loadSplitSettings(CANONICAL, root); + const useSplit = Boolean(cnSplit); + const cnPath = path.join(root, CANONICAL, "settings.json"); + const targetPath = path.join(root, locale, "settings.json"); + + const cn = useSplit ? cnSplit : loadJSON(cnPath); + const tSplit = loadSplitSettings(locale, root); + const t = useSplit ? (tSplit ?? {}) : loadJSON(targetPath); const merged = mergeWithCanonical(cn, t); // Drop extras implicitly by not copying unknown keys; merged contains only canonical keys @@ -85,11 +197,15 @@ function ensureSettings(locale) { const missingAfter = cnKeys.filter((k) => !mergedKeys.includes(k)); const extraAfter = mergedKeys.filter((k) => !cnKeys.includes(k)); - saveJSON(targetPath, sorted); + if (useSplit) { + saveSplitSettingsFromCanonical(locale, sorted, root); + } else { + saveJSON(targetPath, sorted); + } return { locale, - targetPath, + targetPath: useSplit ? path.join(root, locale, "settings") : targetPath, cnCount: cnKeys.length, before: { count: tKeys.length, missing: missingBefore.length, extra: extraBefore.length }, after: { count: mergedKeys.length, missing: missingAfter.length, extra: extraAfter.length }, @@ -99,12 +215,13 @@ function ensureSettings(locale) { function main() { const reports = []; for (const loc of LOCALES) { - const p = path.join(MESSAGES_DIR, loc, "settings.json"); - if (!fs.existsSync(p)) { - console.error(`[skip] ${loc} has no settings.json`); + const legacy = path.join(DEFAULT_MESSAGES_DIR, loc, "settings.json"); + const split = path.join(DEFAULT_MESSAGES_DIR, loc, "settings"); + if (!fs.existsSync(legacy) && !fs.existsSync(split)) { + console.error(`[skip] ${loc} has no settings messages`); continue; } - reports.push(ensureSettings(loc)); + reports.push(ensureSettings(loc, DEFAULT_MESSAGES_DIR)); } // Print summary @@ -115,6 +232,14 @@ function main() { } } +module.exports = { + ensureSettings, + flatten, + loadSplitSettings, + mergeWithCanonical, + sortKeysDeep, +}; + if (require.main === module) { main(); } diff --git a/src/i18n/request.ts b/src/i18n/request.ts index 545134cd6..64cbab5cc 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -17,6 +17,8 @@ export default getRequestConfig(async ({ requestLocale }) => { } // Dynamically import all translation files for the current locale + // NOTE: This import expects each `messages//index.ts` to default-export the full messages object. + // The `settings` namespace is composed by `messages//settings/index.ts` so key paths stay stable. const messages = await import(`../../messages/${locale}`).then((module) => module.default); return { diff --git a/tests/unit/i18n/audit-messages-emoji-script.test.ts b/tests/unit/i18n/audit-messages-emoji-script.test.ts new file mode 100644 index 000000000..fe762b50f --- /dev/null +++ b/tests/unit/i18n/audit-messages-emoji-script.test.ts @@ -0,0 +1,182 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import audit from "../../../scripts/audit-messages-emoji.js"; + +describe("scripts/audit-messages-emoji.js", () => { + test("fileToKeyPrefix() drops trailing strings segment for stable key paths", () => { + expect(audit.fileToKeyPrefix(path.join("settings", "providers", "strings.json"))).toBe( + "settings.providers" + ); + expect(audit.fileToKeyPrefix(path.join("settings", "common.json"))).toBe("settings.common"); + }); + + test("flattenLeafStrings() flattens objects and arrays to leaf key paths", () => { + const rows = audit.flattenLeafStrings({ + a: ["x", { b: "y" }], + n: 1, + nil: null, + }); + + const keys = rows.map((r: { key: string }) => r.key).sort(); + expect(keys).toEqual(["a.0", "a.1.b"]); + }); + + test("countEmojiCodepoints()/maskEmoji() include keycap and flag sequences", () => { + const keycap = "1\uFE0F\u20E3"; + const flag = String.fromCodePoint(0x1f1fa, 0x1f1f8); + const emoji = String.fromCodePoint(0x1f600); + + const input = `a${keycap}b${flag}c${emoji}d`; + expect(audit.countEmojiCodepoints(input)).toBe(3); + expect(audit.maskEmoji(input)).toBe("abcd"); + }); + + test("findMessagesEmoji() reports leaf strings that contain emoji (stable key paths)", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-messages-emoji", + String(Date.now()) + ); + const messagesDir = path.join(tmpRoot, "messages"); + const emoji = String.fromCodePoint(0x1f600); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "en", "settings", "common.json"), { + greeting: `Hello ${emoji}`, + nested: { ok: "No emoji" }, + }); + + const report = audit.findMessagesEmoji({ messagesDir, locales: ["en"] }); + expect(report.totalRowCount).toBe(1); + expect(report.totalEmojiCount).toBe(1); + + expect(report.rows[0]).toMatchObject({ + locale: "en", + relFile: "settings/common.json", + key: "settings.common.greeting", + emojiCount: 1, + }); + expect(report.rows[0].preview).toContain(""); + expect(report.rows[0].preview).not.toContain(emoji); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("findMessagesEmoji() supports auto locale detection and text/json output", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-messages-emoji-multi", + String(Date.now()) + ); + const messagesDir = path.join(tmpRoot, "messages"); + const emojiA = String.fromCodePoint(0x1f600); + const emojiB = String.fromCodePoint(0x1f680); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + fs.mkdirSync(path.join(messagesDir, ".ignored"), { recursive: true }); + + writeJson(path.join(messagesDir, "en", "settings", "common.json"), { + arr: ["no", `${emojiA}`], + nested: { label: `Hi ${emojiB}` }, + }); + writeJson(path.join(messagesDir, "en", "settings", "providers", "strings.json"), { + title: `Providers ${emojiA}`, + }); + fs.mkdirSync(path.join(messagesDir, "ja"), { recursive: true }); + fs.writeFileSync( + path.join(messagesDir, "ja", "dashboard.json"), + `${JSON.stringify(`Dash ${emojiB}`)}\n`, + "utf8" + ); + + const report = audit.findMessagesEmoji({ messagesDir }); + expect(report.totalRowCount).toBe(4); + expect(report.byLocaleCount).toEqual({ en: 3, ja: 1 }); + + const outText = audit.run([`--messagesDir=${messagesDir}`]); + expect(outText.exitCode).toBe(0); + expect(outText.lines.join("\n")).toContain("Top files by row count"); + expect(outText.lines.join("\n")).toContain("Rows (locale\trelFile\tkey):"); + + const outJson = audit.run([`--messagesDir=${messagesDir}`, "--format=json"]); + expect(outJson.exitCode).toBe(0); + const parsed = JSON.parse(outJson.lines.join("\n")) as Array<{ preview: string }>; + expect(parsed.length).toBe(4); + for (const row of parsed) expect(row.preview).toContain(""); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("CLI prints OK and exits 0 when no emoji exist", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-messages-emoji-cli", + `ok-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "en", "settings", "common.json"), { + greeting: "Hello", + }); + + const out = audit.run([`--messagesDir=${messagesDir}`]); + expect(out.exitCode).toBe(0); + expect(out.lines.join("\n")).toContain("OK: no emoji found"); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("CLI supports --format=tsv for stable machine parsing", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-messages-emoji-cli", + `tsv-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + const emoji = String.fromCodePoint(0x1f680); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "en", "settings", "common.json"), { + greeting: `Hello ${emoji}`, + }); + + const out = audit.run([`--messagesDir=${messagesDir}`, "--format=tsv"]); + expect(out.exitCode).toBe(0); + expect(out.lines[0]).toBe("locale\trelFile\tkey\temojiCount\tpreview"); + expect(out.lines).toContain( + "en\tsettings/common.json\tsettings.common.greeting\t1\tHello " + ); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/unit/i18n/audit-messages-no-emoji-script.test.ts b/tests/unit/i18n/audit-messages-no-emoji-script.test.ts new file mode 100644 index 000000000..c53b5c672 --- /dev/null +++ b/tests/unit/i18n/audit-messages-no-emoji-script.test.ts @@ -0,0 +1,163 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import audit from "../../../scripts/audit-messages-no-emoji.js"; + +describe("scripts/audit-messages-no-emoji.js", () => { + test("CLI exits 0 and prints OK when no emoji exist", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-messages-no-emoji-cli", + `ok-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "en", "settings", "common.json"), { greeting: "Hello" }); + + const out = audit.run([`--messagesDir=${messagesDir}`]); + expect(out.exitCode).toBe(0); + expect(out.lines.join("\n")).toContain("OK: no emoji found"); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("CLI supports --fail and outputs codepoints without printing emoji characters", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-messages-no-emoji-cli", + `fail-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + const emoji = String.fromCodePoint(0x1f600); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "en", "provider-chain.json"), { + timeline: { circuitTriggered: `Warning ${emoji}` }, + }); + + const out = audit.run([`--messagesDir=${messagesDir}`, "--format=tsv", "--fail"]); + expect(out.exitCode).toBe(1); + expect(out.lines[0]).toBe("file\tkey\temojiCount\tcodepoints"); + expect(out.lines.join("\n")).toContain("messages/en/provider-chain.json"); + expect(out.lines.join("\n")).toContain("provider-chain.timeline.circuitTriggered"); + expect(out.lines.join("\n")).toContain("U+1F600"); + expect(out.lines.join("\n")).not.toContain(emoji); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("regression: detects keycap and flag emoji sequences", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-messages-no-emoji-cli", + `sequences-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + const keycap = "1\uFE0F\u20E3"; + const flag = String.fromCodePoint(0x1f1fa, 0x1f1f8); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "en", "provider-chain.json"), { + timeline: { circuitTriggered: `Warning ${keycap} ${flag}` }, + }); + + const out = audit.run([`--messagesDir=${messagesDir}`, "--format=tsv", "--fail"]); + expect(out.exitCode).toBe(1); + expect(out.lines.join("\n")).toContain("U+0031+U+FE0F+U+20E3"); + expect(out.lines.join("\n")).toContain("U+1F1FA+U+1F1F8"); + expect(out.lines.join("\n")).not.toContain(keycap); + expect(out.lines.join("\n")).not.toContain(flag); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("helpers format codepoints and list emoji codepoints in order", () => { + expect(audit.toCodepoint(0x1f600)).toBe("U+1F600"); + expect(audit.toCodepoint(0x2639)).toBe("U+2639"); + + const emojiA = String.fromCodePoint(0x1f600); + const emojiB = String.fromCodePoint(0x1f680); + expect(audit.listEmojiCodepoints(`x ${emojiA}${emojiB}${emojiA}`)).toEqual([ + "U+1F600", + "U+1F680", + "U+1F600", + ]); + + const keycap = "1\uFE0F\u20E3"; + const flag = String.fromCodePoint(0x1f1fa, 0x1f1f8); + expect(audit.listEmojiCodepoints(`a${keycap}b${flag}c`)).toEqual([ + "U+0031+U+FE0F+U+20E3", + "U+1F1FA+U+1F1F8", + ]); + }); + + test("run() supports text/json output and locales filtering", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-messages-no-emoji-cli", + `formats-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + const emoji = String.fromCodePoint(0x1f600); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "en", "settings", "common.json"), { + greeting: `Hello ${emoji}`, + }); + writeJson(path.join(messagesDir, "ja", "dashboard.json"), { + hero: { title: `Dash ${emoji}` }, + }); + + const outText = audit.run([`--messagesDir=${messagesDir}`, "--locales=en,ja"]); + expect(outText.exitCode).toBe(0); + expect(outText.lines[0]).toBe("Found 2 messages strings containing emoji:"); + + const outJson = audit.run([`--messagesDir=${messagesDir}`, "--locales=en", "--format=json"]); + expect(outJson.exitCode).toBe(0); + const parsed = JSON.parse(outJson.lines.join("\n")) as Array<{ file: string; key: string }>; + expect(parsed).toEqual([ + { + codepoints: ["U+1F600"], + emojiCount: 1, + file: "messages/en/settings/common.json", + key: "settings.common.greeting", + }, + ]); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("regression: repo messages JSON stays emoji-free", () => { + const out = audit.run(["--fail"]); + expect(out.exitCode).toBe(0); + }); +}); diff --git a/tests/unit/i18n/audit-settings-placeholders-script.test.ts b/tests/unit/i18n/audit-settings-placeholders-script.test.ts new file mode 100644 index 000000000..730ae3456 --- /dev/null +++ b/tests/unit/i18n/audit-settings-placeholders-script.test.ts @@ -0,0 +1,227 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import audit from "../../../scripts/audit-settings-placeholders.js"; + +describe("scripts/audit-settings-placeholders.js", () => { + test("findSettingsPlaceholders() reports leaf strings that equal zh-CN at the same key path", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-settings-placeholders", + String(Date.now()) + ); + const messagesDir = path.join(tmpRoot, "messages"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + // canonical (zh-CN) + writeJson(path.join(messagesDir, "zh-CN", "settings", "config.json"), { + form: { + enableHttp2: "启用 HTTP/2", + enableHttp2Desc: "启用后,代理请求将优先使用 HTTP/2 协议。", + }, + }); + writeJson( + path.join(messagesDir, "zh-CN", "settings", "providers", "form", "maxRetryAttempts.json"), + { + label: "单供应商最大尝试次数", + desc: "包含首次调用在内,单个供应商最多尝试几次后切换。", + placeholder: "2", + } + ); + + // target (en) has one placeholder leaf copied from zh-CN, and one translated value + writeJson(path.join(messagesDir, "en", "settings", "config.json"), { + form: { + enableHttp2: "Enable HTTP/2", + enableHttp2Desc: "启用后,代理请求将优先使用 HTTP/2 协议。", + }, + }); + writeJson( + path.join(messagesDir, "en", "settings", "providers", "form", "maxRetryAttempts.json"), + { + label: "single provider max retry attempts", + desc: "包含首次调用在内,单个供应商最多尝试几次后切换。", + placeholder: "2", + } + ); + + const report = audit.findSettingsPlaceholders({ messagesDir, locales: ["en"] }); + expect(report.rows.length).toBe(2); + + const keys = report.rows.map((r: { key: string }) => r.key).sort(); + expect(keys).toEqual(["config.form.enableHttp2Desc", "providers.form.maxRetryAttempts.desc"]); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("CLI prints OK and exits 0 when no placeholders exist", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-settings-placeholders-cli", + `ok-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "zh-CN", "settings", "config.json"), { + form: { enableHttp2: "启用 HTTP/2" }, + }); + writeJson(path.join(messagesDir, "en", "settings", "config.json"), { + form: { enableHttp2: "Enable HTTP/2" }, + }); + + const out = audit.run([`--messagesDir=${messagesDir}`]); + expect(out.exitCode).toBe(0); + expect(out.lines.join("\n")).toContain("OK: no zh-CN placeholder candidates"); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("CLI prints matches and exits 1 when --fail and placeholders exist", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-settings-placeholders-cli", + `fail-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "zh-CN", "settings", "config.json"), { + form: { enableHttp2Desc: "启用后,代理请求将优先使用 HTTP/2 协议。" }, + }); + writeJson(path.join(messagesDir, "en", "settings", "config.json"), { + form: { enableHttp2Desc: "启用后,代理请求将优先使用 HTTP/2 协议。" }, + }); + + const out = audit.run([`--messagesDir=${messagesDir}`, "--fail"]); + expect(out.exitCode).toBe(1); + expect(out.lines[0]).toBe("Found 1 zh-CN placeholder candidates:"); + expect(out.lines).toContain("en\tconfig.json\tconfig.form.enableHttp2Desc"); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("CLI supports --scope and --format=tsv for stable machine parsing", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-settings-placeholders-cli", + `tsv-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "zh-CN", "dashboard.json"), { + hero: { title: "仪表盘" }, + }); + writeJson(path.join(messagesDir, "en", "dashboard.json"), { + hero: { title: "仪表盘" }, + }); + + const out = audit.run([ + `--messagesDir=${messagesDir}`, + "--scope=dashboard", + "--locales=en", + "--format=tsv", + ]); + expect(out.exitCode).toBe(0); + expect(out.lines[0]).toBe("locale\trelFile\tkey\tvalue\treason"); + expect(out.lines).toContain( + "en\tdashboard.json\tdashboard.hero.title\t仪表盘\tsame_as_zh-CN" + ); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("allowlist filters false positives (exact/keyPrefix/keyRegex/valueRegex/glossary)", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-audit-settings-placeholders-allowlist", + `allowlist-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + const allowlistPath = path.join(tmpRoot, "allowlist.json"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(allowlistPath, { + entries: [ + { key: "config.form.exactKey", reason: "test-exact" }, + { keyPrefix: "config.form.prefix.", reason: "test-prefix" }, + { keyRegex: "^config\\.form\\.re\\.", reason: "test-key-regex" }, + { valueRegex: "KEEP_AS_CN$", reason: "test-value-regex" }, + ], + glossary: ["GLOSSARY_TERM"], + }); + + writeJson(path.join(messagesDir, "zh-CN", "settings", "config.json"), { + form: { + exactKey: "精确豁免", + prefix: { + a: "前缀豁免", + }, + re: { + b: "正则豁免", + }, + value: "触发 KEEP_AS_CN", + glossary: "包含 GLOSSARY_TERM 的句子", + }, + }); + writeJson(path.join(messagesDir, "en", "settings", "config.json"), { + form: { + exactKey: "精确豁免", + prefix: { + a: "前缀豁免", + }, + re: { + b: "正则豁免", + }, + value: "触发 KEEP_AS_CN", + glossary: "包含 GLOSSARY_TERM 的句子", + }, + }); + + const report = audit.findSettingsPlaceholders({ + messagesDir, + locales: ["en"], + scopes: ["settings"], + allowlistPath, + }); + expect(report.rows).toEqual([]); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/unit/i18n/ja-dashboard-parentheses.test.ts b/tests/unit/i18n/ja-dashboard-parentheses.test.ts new file mode 100644 index 000000000..7de634576 --- /dev/null +++ b/tests/unit/i18n/ja-dashboard-parentheses.test.ts @@ -0,0 +1,18 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +describe("messages/ja/dashboard.json", () => { + test("uses only halfwidth parentheses () and avoids placeholder markers", () => { + const filePath = path.join(process.cwd(), "messages", "ja", "dashboard.json"); + const text = fs.readFileSync(filePath, "utf8"); + + expect(() => JSON.parse(text)).not.toThrow(); + expect(text).not.toMatch(/[()]/); + + const bannedMarkers = ["(繁)", "[JA]", "(TW)", "(繁)", "(TW)"]; + for (const marker of bannedMarkers) { + expect(text).not.toContain(marker); + } + }); +}); diff --git a/tests/unit/i18n/ja-providers-form-strings-quality.test.ts b/tests/unit/i18n/ja-providers-form-strings-quality.test.ts new file mode 100644 index 000000000..5b3115544 --- /dev/null +++ b/tests/unit/i18n/ja-providers-form-strings-quality.test.ts @@ -0,0 +1,46 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as Record; +}; + +describe("messages/ja/settings/providers/form/strings.json", () => { + test("does not contain placeholder markers and uses halfwidth parentheses", () => { + const ja = readJson("messages/ja/settings/providers/form/strings.json"); + + for (const value of Object.values(ja)) { + expect(value).not.toContain("[JA]"); + expect(value).not.toContain("("); + expect(value).not.toContain(")"); + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("(TW)"); + expect(value).not.toContain("(TW)"); + } + }); + + test("does not keep raw English strings (except safe placeholders)", () => { + const ja = readJson("messages/ja/settings/providers/form/strings.json"); + const en = readJson("messages/en/settings/providers/form/strings.json"); + + const allowedSameAsEn = [ + "costMultiplierPlaceholder", + "failureThresholdPlaceholder", + "openDurationPlaceholder", + "priorityPlaceholder", + "proxyTestResultMessage", + "successThresholdPlaceholder", + "websiteUrlPlaceholder", + "weightPlaceholder", + ].sort(); + + const sameAsEn = Object.keys(en) + .filter((key) => ja[key] === en[key]) + .sort(); + + expect(sameAsEn).toEqual(allowedSameAsEn); + }); +}); diff --git a/tests/unit/i18n/ja-providers-guide-quality.test.ts b/tests/unit/i18n/ja-providers-guide-quality.test.ts new file mode 100644 index 000000000..75a644360 --- /dev/null +++ b/tests/unit/i18n/ja-providers-guide-quality.test.ts @@ -0,0 +1,38 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as Record; +}; + +describe("messages/ja/settings/providers/guide.json", () => { + test("does not contain placeholder markers, emoji, or fullwidth parentheses", () => { + const ja = readJson("messages/ja/settings/providers/guide.json"); + + for (const value of Object.values(ja)) { + expect(value).not.toContain("[JA]"); + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("(TW)"); + expect(value).not.toContain("(TW)"); + + expect(value).not.toContain("("); + expect(value).not.toContain(")"); + + expect(value).not.toMatch(/[1-4]\uFE0F\u20E3/); + } + }); + + test("does not keep raw English guide strings", () => { + const ja = readJson("messages/ja/settings/providers/guide.json"); + const en = readJson("messages/en/settings/providers/guide.json"); + + const sameAsEn = Object.keys(en) + .filter((key) => ja[key] === en[key]) + .sort(); + + expect(sameAsEn).toEqual([]); + }); +}); diff --git a/tests/unit/i18n/ja-providers-strings-quality.test.ts b/tests/unit/i18n/ja-providers-strings-quality.test.ts new file mode 100644 index 000000000..545fb4043 --- /dev/null +++ b/tests/unit/i18n/ja-providers-strings-quality.test.ts @@ -0,0 +1,54 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as Record; +}; + +const hasKana = /[ぁ-んァ-ン]/; + +describe("messages/ja/settings/providers/strings.json", () => { + test("does not contain placeholder markers or fullwidth parentheses", () => { + const ja = readJson("messages/ja/settings/providers/strings.json"); + + for (const value of Object.values(ja)) { + expect(value).not.toContain("[JA]"); + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("(TW)"); + expect(value).not.toContain("(TW)"); + + expect(value).not.toContain("("); + expect(value).not.toContain(")"); + } + }); + + test("key UI strings are translated to Japanese", () => { + const ja = readJson("messages/ja/settings/providers/strings.json"); + + const keys = [ + "keyLoading", + "noProviders", + "noProvidersDesc", + "official", + "resetCircuit", + "resetCircuitDesc", + "resetCircuitFailed", + "searchNoResults", + "searchResults", + "todayUsage", + "toggleFailed", + "toggleSuccess", + "toggleSuccessDesc", + "updateFailed", + "viewKey", + "viewKeyDesc", + ] as const; + + for (const key of keys) { + expect(ja[key]).toMatch(hasKana); + } + }); +}); diff --git a/tests/unit/i18n/ja-settings-small-modules-quality.test.ts b/tests/unit/i18n/ja-settings-small-modules-quality.test.ts new file mode 100644 index 000000000..eda015ae5 --- /dev/null +++ b/tests/unit/i18n/ja-settings-small-modules-quality.test.ts @@ -0,0 +1,43 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as unknown; +}; + +const flattenStrings = (value: unknown): string[] => { + if (typeof value === "string") return [value]; + if (!value || typeof value !== "object") return []; + if (Array.isArray(value)) return value.flatMap(flattenStrings); + return Object.values(value).flatMap(flattenStrings); +}; + +describe("ja settings small modules", () => { + test("do not contain placeholder markers, emoji, or fullwidth parentheses", () => { + const files = [ + "messages/ja/settings/common.json", + "messages/ja/settings/requestFilters.json", + "messages/ja/settings/prices.json", + ] as const; + + for (const file of files) { + const data = readJson(file); + for (const text of flattenStrings(data)) { + expect(text, file).not.toContain("(繁)"); + expect(text, file).not.toContain("[JA]"); + expect(text, file).not.toContain("(TW)"); + expect(text, file).not.toContain("(繁)"); + expect(text, file).not.toContain("(TW)"); + + // Non zh/zh-TW should use halfwidth parentheses only + expect(text, file).not.toContain("("); + expect(text, file).not.toContain(")"); + + expect(text, file).not.toMatch(/[1-4]\uFE0F\u20E3/); + } + } + }); +}); diff --git a/tests/unit/i18n/settings-index-modules-load.test.ts b/tests/unit/i18n/settings-index-modules-load.test.ts new file mode 100644 index 000000000..58c12422b --- /dev/null +++ b/tests/unit/i18n/settings-index-modules-load.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "vitest"; + +import enSettings from "../../../messages/en/settings"; +import jaSettings from "../../../messages/ja/settings"; +import ruSettings from "../../../messages/ru/settings"; +import zhCNSettings from "../../../messages/zh-CN/settings"; +import zhTWSettings from "../../../messages/zh-TW/settings"; + +describe("messages//settings module", () => { + test("loads and keeps expected top-level keys", () => { + const all = [enSettings, jaSettings, ruSettings, zhCNSettings, zhTWSettings]; + + for (const settings of all) { + expect(settings).toHaveProperty("providers"); + expect(settings).toHaveProperty("providers.form"); + expect(settings).toHaveProperty("mcpPassthroughConfig"); + expect(settings).toHaveProperty("mcpPassthroughUrlPlaceholder"); + } + }); +}); diff --git a/tests/unit/i18n/settings-split-guards.test.ts b/tests/unit/i18n/settings-split-guards.test.ts new file mode 100644 index 000000000..b0e1e086f --- /dev/null +++ b/tests/unit/i18n/settings-split-guards.test.ts @@ -0,0 +1,129 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const LOCALES = ["zh-CN", "zh-TW", "en", "ja", "ru"] as const; +const CANONICAL: (typeof LOCALES)[number] = "zh-CN"; + +const SETTINGS_LINE_THRESHOLD = 800; + +function isObject(v: unknown): v is Record { + return Boolean(v) && typeof v === "object" && !Array.isArray(v); +} + +function typeTag(v: unknown): string { + if (v === null) return "null"; + if (Array.isArray(v)) return "array"; + return typeof v; +} + +function listJsonFilesRecursive(dir: string): string[] { + const out: string[] = []; + const walk = (d: string) => { + for (const entry of fs.readdirSync(d, { withFileTypes: true })) { + const full = path.join(d, entry.name); + if (entry.isDirectory()) walk(full); + else if (entry.isFile() && entry.name.endsWith(".json")) out.push(full); + } + }; + walk(dir); + return out.sort(); +} + +function loadJson(filePath: string): unknown { + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} + +function loadSplitSettings(locale: (typeof LOCALES)[number]): Record { + const settingsDir = path.join(process.cwd(), "messages", locale, "settings"); + const out: Record = {}; + + for (const entry of fs.readdirSync(settingsDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith(".json")) continue; + const name = entry.name.replace(/\.json$/, ""); + const v = loadJson(path.join(settingsDir, entry.name)) as Record; + if (name === "strings") Object.assign(out, v); + else out[name] = v; + } + + const providersDir = path.join(settingsDir, "providers"); + const providers: Record = {}; + for (const entry of fs.readdirSync(providersDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith(".json")) continue; + const name = entry.name.replace(/\.json$/, ""); + const v = loadJson(path.join(providersDir, entry.name)) as Record; + if (name === "strings") Object.assign(providers, v); + else providers[name] = v; + } + + const formDir = path.join(providersDir, "form"); + const form: Record = {}; + for (const entry of fs.readdirSync(formDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith(".json")) continue; + const name = entry.name.replace(/\.json$/, ""); + const v = loadJson(path.join(formDir, entry.name)) as Record; + if (name === "strings") Object.assign(form, v); + else form[name] = v; + } + providers.form = form; + out.providers = providers; + + return out; +} + +function flattenLeafTypes( + obj: unknown, + prefix = "", + out: Record = {} +): Record { + if (!isObject(obj)) { + if (prefix) out[prefix] = typeTag(obj); + return out; + } + + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? `${prefix}.${k}` : k; + if (isObject(v)) flattenLeafTypes(v, key, out); + else out[key] = typeTag(v); + } + return out; +} + +describe("i18n settings split guards", () => { + test("split file layout matches canonical and each file <= 800 lines", () => { + const canonicalDir = path.join(process.cwd(), "messages", CANONICAL, "settings"); + const canonicalFiles = listJsonFilesRecursive(canonicalDir).map((p) => + path.relative(canonicalDir, p).replaceAll(path.sep, "/") + ); + + for (const locale of LOCALES) { + const dir = path.join(process.cwd(), "messages", locale, "settings"); + const files = listJsonFilesRecursive(dir).map((p) => + path.relative(dir, p).replaceAll(path.sep, "/") + ); + expect(files).toEqual(canonicalFiles); + + for (const file of listJsonFilesRecursive(dir)) { + const lines = fs.readFileSync(file, "utf8").split(/\r?\n/).length; + expect(lines).toBeLessThanOrEqual(SETTINGS_LINE_THRESHOLD); + } + } + }); + + test("settings key set and leaf types match canonical (zh-CN)", () => { + const canonical = loadSplitSettings(CANONICAL); + const canonicalLeaves = flattenLeafTypes(canonical); + const canonicalKeys = Object.keys(canonicalLeaves).sort(); + + for (const locale of LOCALES) { + const settings = loadSplitSettings(locale); + const leaves = flattenLeafTypes(settings); + const keys = Object.keys(leaves).sort(); + expect(keys).toEqual(canonicalKeys); + + for (const k of canonicalKeys) { + expect(leaves[k]).toBe(canonicalLeaves[k]); + } + } + }); +}); diff --git a/tests/unit/i18n/sync-settings-keys-script.test.ts b/tests/unit/i18n/sync-settings-keys-script.test.ts new file mode 100644 index 000000000..5840100b1 --- /dev/null +++ b/tests/unit/i18n/sync-settings-keys-script.test.ts @@ -0,0 +1,129 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import sync from "../../../scripts/sync-settings-keys.js"; + +describe("scripts/sync-settings-keys.js", () => { + test("flatten() flattens nested objects into dot-keys", () => { + const input = { a: { b: 1, c: { d: "x" } }, e: true }; + const out = sync.flatten(input); + expect(out).toEqual({ "a.b": 1, "a.c.d": "x", e: true }); + }); + + test("mergeWithCanonical() keeps canonical shape and preserves existing leaves", () => { + const canonical = { + a: { b: "cn", c: { d: "cn" } }, + x: "cn", + }; + const target = { + a: { b: "t", c: "wrong-type" }, + x: { y: "wrong-type" }, + extra: "should-drop", + }; + + const merged = sync.mergeWithCanonical(canonical, target); + expect(merged).toEqual({ + a: { b: "t", c: { d: "cn" } }, + x: "cn", + }); + }); + + test("loadSplitSettings() reads split settings layout for zh-CN", () => { + const settings = sync.loadSplitSettings("zh-CN"); + expect(settings).toBeTruthy(); + expect(settings).toHaveProperty("providers"); + expect(settings).toHaveProperty("mcpPassthroughConfig"); + }); + + test("ensureSettings() can generate split files from canonical", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-sync-settings-keys", + String(Date.now()) + ); + const messagesDir = path.join(tmpRoot, "messages"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + // canonical (zh-CN) + writeJson(path.join(messagesDir, "zh-CN", "settings", "a.json"), { k: "cn" }); + writeJson(path.join(messagesDir, "zh-CN", "settings", "strings.json"), { s: "cn" }); + writeJson(path.join(messagesDir, "zh-CN", "settings", "providers", "strings.json"), { + title: "cn", + }); + writeJson(path.join(messagesDir, "zh-CN", "settings", "providers", "form", "apiTest.json"), { + enabled: "cn", + }); + writeJson(path.join(messagesDir, "zh-CN", "settings", "providers", "form", "strings.json"), { + x: "cn", + }); + + const report = sync.ensureSettings("en", messagesDir); + expect(report.after.missing).toBe(0); + expect(report.after.extra).toBe(0); + + const enA = JSON.parse( + fs.readFileSync(path.join(messagesDir, "en", "settings", "a.json"), "utf8") + ); + expect(enA).toEqual({ k: "cn" }); + + const enStrings = JSON.parse( + fs.readFileSync(path.join(messagesDir, "en", "settings", "strings.json"), "utf8") + ); + expect(enStrings).toEqual({ s: "cn" }); + + const enFormApiTest = JSON.parse( + fs.readFileSync( + path.join(messagesDir, "en", "settings", "providers", "form", "apiTest.json"), + "utf8" + ) + ); + expect(enFormApiTest).toEqual({ enabled: "cn" }); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + test("ensureSettings() supports legacy settings.json layout", () => { + const tmpRoot = path.join( + process.cwd(), + "tests", + ".tmp-sync-settings-keys", + `legacy-${Date.now()}` + ); + const messagesDir = path.join(tmpRoot, "messages"); + + const writeJson = (p: string, data: unknown) => { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8"); + }; + + try { + writeJson(path.join(messagesDir, "zh-CN", "settings.json"), { + a: { b: "cn" }, + x: "cn", + }); + writeJson(path.join(messagesDir, "en", "settings.json"), { + a: { b: "en" }, + x: { y: "wrong-type" }, + extra: "drop", + }); + + const report = sync.ensureSettings("en", messagesDir); + expect(report.after.missing).toBe(0); + expect(report.after.extra).toBe(0); + + const out = JSON.parse( + fs.readFileSync(path.join(messagesDir, "en", "settings.json"), "utf8") + ); + expect(out).toEqual({ a: { b: "en" }, x: "cn" }); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/unit/i18n/zh-tw-dashboard-parentheses.test.ts b/tests/unit/i18n/zh-tw-dashboard-parentheses.test.ts new file mode 100644 index 000000000..6e38180f7 --- /dev/null +++ b/tests/unit/i18n/zh-tw-dashboard-parentheses.test.ts @@ -0,0 +1,18 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +describe("messages/zh-TW/dashboard.json", () => { + test("uses only fullwidth parentheses () and avoids placeholder markers", () => { + const filePath = path.join(process.cwd(), "messages", "zh-TW", "dashboard.json"); + const text = fs.readFileSync(filePath, "utf8"); + + expect(() => JSON.parse(text)).not.toThrow(); + expect(text).not.toMatch(/[()]/); + + const bannedMarkers = ["(繁)", "[JA]", "(TW)", "(繁)", "(TW)"]; + for (const marker of bannedMarkers) { + expect(text).not.toContain(marker); + } + }); +}); diff --git a/tests/unit/i18n/zh-tw-my-usage-parentheses.test.ts b/tests/unit/i18n/zh-tw-my-usage-parentheses.test.ts new file mode 100644 index 000000000..dea21c504 --- /dev/null +++ b/tests/unit/i18n/zh-tw-my-usage-parentheses.test.ts @@ -0,0 +1,18 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +describe("messages/zh-TW/myUsage.json", () => { + test("uses only fullwidth parentheses () and avoids placeholder markers", () => { + const filePath = path.join(process.cwd(), "messages", "zh-TW", "myUsage.json"); + const text = fs.readFileSync(filePath, "utf8"); + + expect(() => JSON.parse(text)).not.toThrow(); + expect(text).not.toMatch(/[()]/); + + const bannedMarkers = ["(繁)", "[JA]", "(TW)", "(繁)", "(TW)"]; + for (const marker of bannedMarkers) { + expect(text).not.toContain(marker); + } + }); +}); diff --git a/tests/unit/i18n/zh-tw-providers-api-test-quality.test.ts b/tests/unit/i18n/zh-tw-providers-api-test-quality.test.ts new file mode 100644 index 000000000..2a15a4c09 --- /dev/null +++ b/tests/unit/i18n/zh-tw-providers-api-test-quality.test.ts @@ -0,0 +1,36 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as Record; +}; + +const flatten = (value: unknown): string[] => { + if (typeof value === "string") return [value]; + if (!value || typeof value !== "object") return []; + if (Array.isArray(value)) return value.flatMap(flatten); + return Object.values(value).flatMap(flatten); +}; + +describe("messages/zh-TW/settings/providers/form/apiTest.json", () => { + test("does not contain placeholder markers, emoji, or halfwidth parentheses", () => { + const zhTW = readJson("messages/zh-TW/settings/providers/form/apiTest.json"); + + for (const text of flatten(zhTW)) { + expect(text).not.toContain("(繁)"); + expect(text).not.toContain("[JA]"); + expect(text).not.toContain("(TW)"); + expect(text).not.toContain("(繁)"); + expect(text).not.toContain("(TW)"); + + // zh/zh-TW should use fullwidth parentheses only + expect(text).not.toContain("("); + expect(text).not.toContain(")"); + + expect(text).not.toMatch(/[1-4]\uFE0F\u20E3/); + } + }); +}); diff --git a/tests/unit/i18n/zh-tw-providers-form-strings-quality.test.ts b/tests/unit/i18n/zh-tw-providers-form-strings-quality.test.ts new file mode 100644 index 000000000..01cafdbee --- /dev/null +++ b/tests/unit/i18n/zh-tw-providers-form-strings-quality.test.ts @@ -0,0 +1,27 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as Record; +}; + +describe("messages/zh-TW/settings/providers/form/strings.json", () => { + test("does not contain placeholder markers and uses fullwidth parentheses", () => { + const zhTW = readJson("messages/zh-TW/settings/providers/form/strings.json"); + + for (const value of Object.values(zhTW)) { + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("[JA]"); + expect(value).not.toContain("(TW)"); + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("(TW)"); + + // zh/zh-TW should use fullwidth parentheses only + expect(value).not.toContain("("); + expect(value).not.toContain(")"); + } + }); +}); diff --git a/tests/unit/i18n/zh-tw-providers-guide-quality.test.ts b/tests/unit/i18n/zh-tw-providers-guide-quality.test.ts new file mode 100644 index 000000000..49b1a5327 --- /dev/null +++ b/tests/unit/i18n/zh-tw-providers-guide-quality.test.ts @@ -0,0 +1,40 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as Record; +}; + +describe("messages/zh-TW/settings/providers/guide.json", () => { + test("does not contain placeholder markers, emoji, or halfwidth parentheses", () => { + const zhTW = readJson("messages/zh-TW/settings/providers/guide.json"); + + for (const value of Object.values(zhTW)) { + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("[JA]"); + expect(value).not.toContain("(TW)"); + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("(TW)"); + + // zh/zh-TW should use fullwidth parentheses only + expect(value).not.toContain("("); + expect(value).not.toContain(")"); + + expect(value).not.toMatch(/[1-4]\uFE0F\u20E3/); + } + }); + + test("does not keep raw English guide strings", () => { + const zhTW = readJson("messages/zh-TW/settings/providers/guide.json"); + const en = readJson("messages/en/settings/providers/guide.json"); + + const sameAsEn = Object.keys(en) + .filter((key) => zhTW[key] === en[key]) + .sort(); + + expect(sameAsEn).toEqual([]); + }); +}); diff --git a/tests/unit/i18n/zh-tw-providers-strings-quality.test.ts b/tests/unit/i18n/zh-tw-providers-strings-quality.test.ts new file mode 100644 index 000000000..d7ad796ae --- /dev/null +++ b/tests/unit/i18n/zh-tw-providers-strings-quality.test.ts @@ -0,0 +1,29 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as Record; +}; + +describe("messages/zh-TW/settings/providers/strings.json", () => { + test("does not contain placeholder markers, emoji, or halfwidth parentheses", () => { + const zhTW = readJson("messages/zh-TW/settings/providers/strings.json"); + + for (const value of Object.values(zhTW)) { + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("[JA]"); + expect(value).not.toContain("(TW)"); + expect(value).not.toContain("(繁)"); + expect(value).not.toContain("(TW)"); + + // zh/zh-TW should use fullwidth parentheses only + expect(value).not.toContain("("); + expect(value).not.toContain(")"); + + expect(value).not.toMatch(/[1-4]\uFE0F\u20E3/); + } + }); +}); diff --git a/tests/unit/i18n/zh-tw-settings-small-modules-quality.test.ts b/tests/unit/i18n/zh-tw-settings-small-modules-quality.test.ts new file mode 100644 index 000000000..f838e04d7 --- /dev/null +++ b/tests/unit/i18n/zh-tw-settings-small-modules-quality.test.ts @@ -0,0 +1,46 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; + +const readJson = (relPath: string) => { + const filePath = path.join(process.cwd(), relPath); + const text = fs.readFileSync(filePath, "utf8"); + return JSON.parse(text) as unknown; +}; + +const flattenStrings = (value: unknown): string[] => { + if (typeof value === "string") return [value]; + if (!value || typeof value !== "object") return []; + if (Array.isArray(value)) return value.flatMap(flattenStrings); + return Object.values(value).flatMap(flattenStrings); +}; + +describe("zh-TW settings small modules", () => { + test("do not contain placeholder markers, emoji, or halfwidth parentheses", () => { + const files = [ + "messages/zh-TW/settings/clientVersions.json", + "messages/zh-TW/settings/common.json", + "messages/zh-TW/settings/requestFilters.json", + "messages/zh-TW/settings/prices.json", + "messages/zh-TW/settings/errorRules.json", + "messages/zh-TW/settings/notifications.json", + ] as const; + + for (const file of files) { + const data = readJson(file); + for (const text of flattenStrings(data)) { + expect(text, file).not.toContain("(繁)"); + expect(text, file).not.toContain("[JA]"); + expect(text, file).not.toContain("(TW)"); + expect(text, file).not.toContain("(繁)"); + expect(text, file).not.toContain("(TW)"); + + // zh/zh-TW should use fullwidth parentheses only + expect(text, file).not.toContain("("); + expect(text, file).not.toContain(")"); + + expect(text, file).not.toMatch(/[1-4]\uFE0F\u20E3/); + } + } + }); +}); diff --git a/tests/unit/settings/prices/delete-model-dialog.test.tsx b/tests/unit/settings/prices/delete-model-dialog.test.tsx index 85240c013..5e8e0267f 100644 --- a/tests/unit/settings/prices/delete-model-dialog.test.tsx +++ b/tests/unit/settings/prices/delete-model-dialog.test.tsx @@ -2,14 +2,13 @@ * @vitest-environment happy-dom */ -import fs from "node:fs"; -import path from "node:path"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; import { NextIntlClientProvider } from "next-intl"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { DeleteModelDialog } from "@/app/[locale]/settings/prices/_components/delete-model-dialog"; +import { loadMessages } from "./test-messages"; const modelPricesActionMocks = vi.hoisted(() => ({ deleteSingleModelPrice: vi.fn(async () => ({ ok: true, data: null })), @@ -24,19 +23,6 @@ const sonnerMocks = vi.hoisted(() => ({ })); vi.mock("sonner", () => sonnerMocks); -function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), - }; -} - function render(node: ReactNode) { const container = document.createElement("div"); document.body.appendChild(container); diff --git a/tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx b/tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx index c9fef94c8..93ea34752 100644 --- a/tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx +++ b/tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx @@ -2,8 +2,6 @@ * @vitest-environment happy-dom */ -import fs from "node:fs"; -import path from "node:path"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; @@ -11,6 +9,7 @@ import { NextIntlClientProvider } from "next-intl"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { ModelPriceDrawer } from "@/app/[locale]/settings/prices/_components/model-price-drawer"; import type { ModelPrice } from "@/types/model-price"; +import { loadMessages } from "./test-messages"; const modelPricesActionMocks = vi.hoisted(() => ({ upsertSingleModelPrice: vi.fn(async () => ({ ok: true, data: null })), @@ -25,19 +24,6 @@ const sonnerMocks = vi.hoisted(() => ({ })); vi.mock("sonner", () => sonnerMocks); -function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), - }; -} - function render(node: ReactNode) { const container = document.createElement("div"); document.body.appendChild(container); diff --git a/tests/unit/settings/prices/price-list-interactions.test.tsx b/tests/unit/settings/prices/price-list-interactions.test.tsx index aee21e164..4876d5692 100644 --- a/tests/unit/settings/prices/price-list-interactions.test.tsx +++ b/tests/unit/settings/prices/price-list-interactions.test.tsx @@ -2,8 +2,6 @@ * @vitest-environment happy-dom */ -import fs from "node:fs"; -import path from "node:path"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; @@ -11,6 +9,7 @@ import { NextIntlClientProvider } from "next-intl"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { PriceList } from "@/app/[locale]/settings/prices/_components/price-list"; import type { ModelPrice } from "@/types/model-price"; +import { loadMessages } from "./test-messages"; const clipboardMocks = vi.hoisted(() => ({ copyToClipboard: vi.fn(async () => true), @@ -26,19 +25,6 @@ const sonnerMocks = vi.hoisted(() => ({ })); vi.mock("sonner", () => sonnerMocks); -function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), - }; -} - function render(node: ReactNode) { const container = document.createElement("div"); document.body.appendChild(container); diff --git a/tests/unit/settings/prices/price-list-ui-requirements.test.tsx b/tests/unit/settings/prices/price-list-ui-requirements.test.tsx index b8da12425..456afa91f 100644 --- a/tests/unit/settings/prices/price-list-ui-requirements.test.tsx +++ b/tests/unit/settings/prices/price-list-ui-requirements.test.tsx @@ -2,8 +2,6 @@ * @vitest-environment happy-dom */ -import fs from "node:fs"; -import path from "node:path"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; @@ -11,6 +9,7 @@ import { NextIntlClientProvider } from "next-intl"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { PriceList } from "@/app/[locale]/settings/prices/_components/price-list"; import type { ModelPrice } from "@/types/model-price"; +import { loadMessages } from "./test-messages"; const clipboardMocks = vi.hoisted(() => ({ copyToClipboard: vi.fn(async () => true), @@ -26,19 +25,6 @@ const sonnerMocks = vi.hoisted(() => ({ })); vi.mock("sonner", () => sonnerMocks); -function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), - }; -} - function render(node: ReactNode) { const container = document.createElement("div"); document.body.appendChild(container); diff --git a/tests/unit/settings/prices/price-list-zero-price-ui.test.tsx b/tests/unit/settings/prices/price-list-zero-price-ui.test.tsx index cd56a8968..2ac72a3dd 100644 --- a/tests/unit/settings/prices/price-list-zero-price-ui.test.tsx +++ b/tests/unit/settings/prices/price-list-zero-price-ui.test.tsx @@ -2,8 +2,6 @@ * @vitest-environment happy-dom */ -import fs from "node:fs"; -import path from "node:path"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; @@ -11,19 +9,7 @@ import { NextIntlClientProvider } from "next-intl"; import { describe, expect, test } from "vitest"; import { PriceList } from "@/app/[locale]/settings/prices/_components/price-list"; import type { ModelPrice } from "@/types/model-price"; - -function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), - }; -} +import { loadMessages } from "./test-messages"; function render(node: ReactNode) { const container = document.createElement("div"); diff --git a/tests/unit/settings/prices/test-messages.ts b/tests/unit/settings/prices/test-messages.ts new file mode 100644 index 000000000..0ea3d6650 --- /dev/null +++ b/tests/unit/settings/prices/test-messages.ts @@ -0,0 +1,79 @@ +import fs from "node:fs"; +import path from "node:path"; + +type JsonValue = unknown; + +function readJson(filePath: string): JsonValue { + return JSON.parse(fs.readFileSync(filePath, "utf8")) as JsonValue; +} + +function loadSplitSettings(settingsDir: string): Record { + const top: Record = {}; + + for (const entry of fs.readdirSync(settingsDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith(".json")) continue; + const name = entry.name.replace(/\.json$/, ""); + const value = readJson(path.join(settingsDir, entry.name)); + if (name === "strings" && value && typeof value === "object") { + Object.assign(top, value); + continue; + } + top[name] = value; + } + + const providersDir = path.join(settingsDir, "providers"); + const providers: Record = {}; + if (fs.existsSync(providersDir)) { + for (const entry of fs.readdirSync(providersDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith(".json")) continue; + const name = entry.name.replace(/\.json$/, ""); + const value = readJson(path.join(providersDir, entry.name)); + if (name === "strings" && value && typeof value === "object") { + Object.assign(providers, value); + continue; + } + providers[name] = value; + } + + const formDir = path.join(providersDir, "form"); + const form: Record = {}; + if (fs.existsSync(formDir)) { + for (const entry of fs.readdirSync(formDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith(".json")) continue; + const name = entry.name.replace(/\.json$/, ""); + const value = readJson(path.join(formDir, entry.name)); + if (name === "strings" && value && typeof value === "object") { + Object.assign(form, value); + continue; + } + form[name] = value; + } + } + + providers.form = form; + } + + top.providers = providers; + return top; +} + +function loadSettingsMessages(base: string): JsonValue { + const legacy = path.join(base, "settings.json"); + if (fs.existsSync(legacy)) return readJson(legacy); + + const splitDir = path.join(base, "settings"); + return loadSplitSettings(splitDir); +} + +export function loadMessages(locale: string = "en") { + const base = path.join(process.cwd(), "messages", locale); + const read = (name: string) => readJson(path.join(base, name)); + + return { + common: read("common.json"), + errors: read("errors.json"), + ui: read("ui.json"), + forms: read("forms.json"), + settings: loadSettingsMessages(base), + }; +} diff --git a/tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx b/tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx index 3ed727fe7..47cec6ff9 100644 --- a/tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx +++ b/tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx @@ -2,14 +2,13 @@ * @vitest-environment happy-dom */ -import fs from "node:fs"; -import path from "node:path"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; import { NextIntlClientProvider } from "next-intl"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { UploadPriceDialog } from "@/app/[locale]/settings/prices/_components/upload-price-dialog"; +import { loadMessages } from "./test-messages"; // 测试环境不加载 next/navigation 的真实实现(避免 Next.js 运行时依赖) vi.mock("next/navigation", () => ({ @@ -24,19 +23,6 @@ const sonnerMocks = vi.hoisted(() => ({ })); vi.mock("sonner", () => sonnerMocks); -function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), - }; -} - function render(node: ReactNode) { const container = document.createElement("div"); document.body.appendChild(container); diff --git a/tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx b/tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx index 13be52972..d2e0f1681 100644 --- a/tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx +++ b/tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx @@ -2,8 +2,6 @@ * @vitest-environment happy-dom */ -import fs from "node:fs"; -import path from "node:path"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; @@ -11,6 +9,7 @@ import { NextIntlClientProvider } from "next-intl"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { UploadPriceDialog } from "@/app/[locale]/settings/prices/_components/upload-price-dialog"; import type { PriceUpdateResult } from "@/types/model-price"; +import { loadMessages } from "./test-messages"; const modelPricesActionMocks = vi.hoisted(() => ({ uploadPriceTable: vi.fn(async () => ({ ok: true, data: null as PriceUpdateResult | null })), @@ -36,19 +35,6 @@ vi.mock("next/navigation", () => ({ useRouter: navigationMocks.useRouter, })); -function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), - }; -} - function render(node: ReactNode) { const container = document.createElement("div"); document.body.appendChild(container); diff --git a/tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx b/tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx index aae246353..5718209db 100644 --- a/tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx +++ b/tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx @@ -2,8 +2,6 @@ * @vitest-environment happy-dom */ -import fs from "node:fs"; -import path from "node:path"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; @@ -11,6 +9,7 @@ import { NextIntlClientProvider } from "next-intl"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { Dialog } from "@/components/ui/dialog"; import { ProviderForm } from "@/app/[locale]/settings/providers/_components/forms/provider-form"; +import enMessages from "../../../../messages/en"; const sonnerMocks = vi.hoisted(() => ({ toast: { @@ -33,15 +32,12 @@ const requestFiltersActionMocks = vi.hoisted(() => ({ vi.mock("@/actions/request-filters", () => requestFiltersActionMocks); function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), + common: enMessages.common, + errors: enMessages.errors, + ui: enMessages.ui, + forms: enMessages.forms, + settings: enMessages.settings, }; }