diff --git a/src/app/(dashboard)/dashboard/playground/page.tsx b/src/app/(dashboard)/dashboard/playground/page.tsx index fc20c93135..91906d705a 100644 --- a/src/app/(dashboard)/dashboard/playground/page.tsx +++ b/src/app/(dashboard)/dashboard/playground/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslations } from "next-intl"; import { Card, Button, Select, Badge } from "@/shared/components"; import { ALIAS_TO_ID } from "@/shared/constants/providers"; import { pickMaskedDisplayValue, pickDisplayValue } from "@/shared/utils/maskEmail"; @@ -31,18 +32,7 @@ interface ConnectionOption { authType: string; } -const ENDPOINT_OPTIONS = [ - { value: "chat", label: "Chat Completions" }, - { value: "responses", label: "Responses" }, - { value: "images", label: "Image Generation" }, - { value: "embeddings", label: "Embeddings" }, - { value: "speech", label: "Text to Speech" }, - { value: "transcription", label: "Audio Transcription" }, - { value: "video", label: "Video Generation" }, - { value: "music", label: "Music Generation" }, - { value: "rerank", label: "Rerank" }, - { value: "search", label: "Web Search" }, -]; +// Endpoint options will be generated dynamically with translations const DEFAULT_BODIES: Record = { chat: { @@ -154,13 +144,14 @@ async function fileToBase64(file: File): Promise { /** Render image results from OpenAI-compatible format */ function ImageResultsInline({ data }: { data: any }) { + const t = useTranslations("playground"); const images: Array<{ url?: string; b64_json?: string; revised_prompt?: string }> = data?.data || []; if (images.length === 0) return null; return (

- {images.length} image{images.length > 1 ? "s" : ""} generated + {t("imagesGenerated", { count: images.length })}

{images.map((img, i) => { @@ -171,7 +162,7 @@ function ImageResultsInline({ data }: { data: any }) { {/* eslint-disable-next-line @next/next/no-img-element */} {img.revised_prompt download - Save + {t("save")}
); @@ -191,6 +182,22 @@ function ImageResultsInline({ data }: { data: any }) { } export default function PlaygroundPage() { + const t = useTranslations("playground"); + + // Get translated endpoint options + const getEndpointOptions = () => [ + { value: "chat", label: t("endpointOptions.chat") }, + { value: "responses", label: t("endpointOptions.responses") }, + { value: "images", label: t("endpointOptions.images") }, + { value: "embeddings", label: t("endpointOptions.embeddings") }, + { value: "speech", label: t("endpointOptions.speech") }, + { value: "transcription", label: t("endpointOptions.transcription") }, + { value: "video", label: t("endpointOptions.video") }, + { value: "music", label: t("endpointOptions.music") }, + { value: "rerank", label: t("endpointOptions.rerank") }, + { value: "search", label: t("endpointOptions.search") }, + ]; + const [models, setModels] = useState([]); const [providers, setProviders] = useState([]); const [allConnections, setAllConnections] = useState([]); @@ -472,11 +479,8 @@ export default function PlaygroundPage() { science
-

Model Playground

-

- Test any model directly from the dashboard. Pick a provider, model, and endpoint type, - then send a request to see the raw response. -

+

{t("title")}

+

{t("description")}

@@ -486,12 +490,12 @@ export default function PlaygroundPage() { {/* Endpoint — always first */}
0 - ? `Auto (${providerConnections.length} accounts)` - : "No accounts", + ? t("autoAccounts", { count: providerConnections.length }) + : t("noAccounts"), }, ...providerConnections.map((c) => ({ value: c.id, @@ -558,7 +562,7 @@ export default function PlaygroundPage() {
{loading ? ( ) : ( )}
@@ -591,16 +595,16 @@ export default function PlaygroundPage() { attach_file

- {isTranscriptionEndpoint ? "Audio File" : "Attach Images (Vision)"} + {isTranscriptionEndpoint ? t("audioFile") : t("attachImages")}

{isTranscriptionEndpoint && ( - multipart/form-data + {t("multipartFormData")} )} {supportsVision && ( - up to 4 images + {t("upToImages")} )}
@@ -623,7 +627,7 @@ export default function PlaygroundPage() { {!uploadedFile && (

info - Select an audio file to transcribe (mp3, wav, m4a, ogg, flac…) + {t("selectAudioFile")}

)} @@ -664,7 +668,7 @@ export default function PlaygroundPage() { onClick={() => setUploadedImages([])} className="text-xs text-text-muted hover:text-red-500 self-center ml-1" > - Clear all + {t("clearAll")} )} @@ -684,7 +688,7 @@ export default function PlaygroundPage() { upload -

Request

+

{t("request")}

POST {ENDPOINT_PATHS[selectedEndpoint]} @@ -693,7 +697,7 @@ export default function PlaygroundPage() { @@ -704,7 +708,7 @@ export default function PlaygroundPage() { setRequestBody(JSON.stringify(template, null, 2)); }} className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors" - title="Reset to default" + title={t("resetToDefault")} > restart_alt @@ -715,8 +719,7 @@ export default function PlaygroundPage() { info - Transcription uses multipart/form-data. Upload the audio file above — JSON below - controls extra params (model, language). + {t("transcriptionHint")}

)}
@@ -748,7 +751,7 @@ export default function PlaygroundPage() { download -

Response

+

{t("response")}

{responseStatus !== null && ( handleCopy(responseBody)} className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors" - title="Copy" + title={t("copy")} > content_copy @@ -788,7 +791,7 @@ export default function PlaygroundPage() { className="inline-flex items-center gap-2 text-sm text-primary hover:underline" > download - Download audio + {t("downloadAudio")}
) : imageData ? ( @@ -796,7 +799,7 @@ export default function PlaygroundPage() { ) : transcriptionText !== null ? (

- Transcription + {t("transcription")}

{transcriptionText} @@ -806,7 +809,7 @@ export default function PlaygroundPage() { className="text-xs text-primary hover:underline flex items-center gap-1" > content_copy - Copy text + {t("copyText")}
) : ( diff --git a/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx b/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx index df1e083630..06fc5c2461 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; import { Button, Card, Modal } from "@/shared/components"; type ProxyItem = { @@ -51,6 +52,7 @@ const EMPTY_FORM = { }; export default function ProxyRegistryManager() { + const t = useTranslations("proxyRegistry"); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -124,7 +126,7 @@ export default function ProxyRegistryManager() { const res = await fetch("/api/settings/proxies"); const data = await res.json().catch(() => ({})); if (!res.ok) { - setError(data?.error?.message || "Failed to load proxy registry"); + setError(data?.error?.message || t("errorLoadFailed")); setItems([]); return; } @@ -134,7 +136,7 @@ export default function ProxyRegistryManager() { void loadHealth(); void loadAllUsage(ids); } catch (e: any) { - setError(e?.message || "Failed to load proxy registry"); + setError(e?.message || t("errorLoadFailed")); setItems([]); } finally { setLoading(false); @@ -221,7 +223,7 @@ export default function ProxyRegistryManager() { if (!res.ok) { setTestById((prev) => ({ ...prev, - [item.id]: { success: false, error: data?.error?.message || "Test failed" }, + [item.id]: { success: false, error: data?.error?.message || t("failed") }, })); return; } @@ -235,7 +237,7 @@ export default function ProxyRegistryManager() { const handleSave = async () => { if (!form.name.trim() || !form.host.trim()) { - setError("Name and host are required"); + setError(t("errorNameHostRequired")); return; } @@ -270,7 +272,7 @@ export default function ProxyRegistryManager() { }); const data = await res.json().catch(() => ({})); if (!res.ok) { - setError(data?.error?.message || "Failed to save proxy"); + setError(data?.error?.message || t("errorSaveFailed")); return; } @@ -278,7 +280,7 @@ export default function ProxyRegistryManager() { setForm(EMPTY_FORM); await load(); } catch (e: any) { - setError(e?.message || "Failed to save proxy"); + setError(e?.message || t("errorSaveFailed")); } finally { setSaving(false); } @@ -298,9 +300,7 @@ export default function ProxyRegistryManager() { const payload = await res.json().catch(() => ({})); const inUse = res.status === 409; if (inUse) { - const ok = window.confirm( - "This proxy is still assigned. Force delete and remove all assignments?" - ); + const ok = window.confirm(t("errorForceDeleteConfirm")); if (!ok) return; const forceRes = await fetch(`/api/settings/proxies?id=${encodeURIComponent(id)}&force=1`, { @@ -309,7 +309,7 @@ export default function ProxyRegistryManager() { if (!forceRes.ok) { const forcePayload = await forceRes.json().catch(() => ({})); - setError(forcePayload?.error?.message || "Failed to force delete proxy"); + setError(forcePayload?.error?.message || t("errorDeleteFailed")); return; } @@ -317,9 +317,9 @@ export default function ProxyRegistryManager() { return; } - setError(payload?.error?.message || "Failed to delete proxy"); + setError(payload?.error?.message || t("errorDeleteFailed")); } catch (e: any) { - setError(e?.message || "Failed to delete proxy"); + setError(e?.message || t("errorDeleteFailed")); } }; @@ -334,12 +334,12 @@ export default function ProxyRegistryManager() { }); const data = await res.json().catch(() => ({})); if (!res.ok) { - setError(data?.error?.message || "Failed to migrate legacy proxy config"); + setError(data?.error?.message || t("errorMigrateFailed")); return; } await load(); } catch (e: any) { - setError(e?.message || "Failed to migrate legacy proxy config"); + setError(e?.message || t("errorMigrateFailed")); } finally { setMigrating(false); } @@ -368,7 +368,7 @@ export default function ProxyRegistryManager() { }); const payload = await res.json().catch(() => ({})); if (!res.ok) { - setError(payload?.error?.message || "Failed to run bulk assignment"); + setError(payload?.error?.message || t("errorBulkFailed")); return; } @@ -376,7 +376,7 @@ export default function ProxyRegistryManager() { setBulkScopeIds(""); await load(); } catch (e: any) { - setError(e?.message || "Failed to run bulk assignment"); + setError(e?.message || t("errorBulkFailed")); } finally { setBulkSaving(false); } @@ -387,8 +387,8 @@ export default function ProxyRegistryManager() {
-

Proxy Registry

-

Store reusable proxies and track assignments.

+

{t("title")}

+

{t("description")}

@@ -428,20 +428,20 @@ export default function ProxyRegistryManager() { )} {loading ? ( -
Loading proxies...
+
{t("loading")}
) : items.length === 0 ? ( -
No saved proxies yet.
+
{t("noProxies")}
) : (
- - - - - - + + + + + + @@ -483,8 +483,10 @@ export default function ProxyRegistryManager() { ) ) : health ? ( <> - {health.successRate ?? 0}% success - {health.avgLatencyMs ?? "-"} ms avg + {t("successRate", { rate: health.successRate ?? 0 })} + + {t("avgLatency", { latency: health.avgLatencyMs ?? "-" })} + ) : ( @@ -493,8 +495,8 @@ export default function ProxyRegistryManager() { @@ -540,13 +542,13 @@ export default function ProxyRegistryManager() { onClose={() => { if (!saving) setModalOpen(false); }} - title={editingId ? "Edit Proxy" : "Create Proxy"} + title={editingId ? t("modalEditTitle") : t("modalCreateTitle")} maxWidth="lg" >
- + localhost。授权后,您的浏览器将尝试打开 localhost — 复制该完整 URL 并粘贴到下方。要完全远程使用而无需此手动步骤,配置您自己的 OAuth 凭据。", + "remoteAccessInfo": "远程访问:由于您是远程访问 OmniRoute,授权后您会看到一个错误页面(localhost 未找到)。这是正常的 — 只需从浏览器地址栏复制完整 URL 并粘贴到下方。", + "step1OpenUrl": "步骤 1:在浏览器中打开此 URL", + "copy": "复制", + "step2PasteCallback": "步骤 2:在此处粘贴回调 URL 或授权代码", + "step2Hint": "授权后,粘贴完整的回调 URL。对于 Claude Code 和 Cline,您也可以直接粘贴身份验证代码,例如 code#state。", + "connect": "连接", + "cancel": "取消", + "success": "连接成功!", + "successMessage": "您的 {providerName} 账户已连接。", + "done": "完成", + "error": "连接失败", + "tryAgain": "重试" + }, + "cursorAuthModal": { + "title": "连接 Cursor IDE", + "autoDetecting": "自动检测令牌中...", + "readingFromCursor": "正在从 Cursor IDE 或 cursor-agent 读取", + "tokensAutoDetected": "已成功从 Cursor IDE 自动检测到令牌!", + "cursorNotDetected": "未检测到 Cursor IDE。请手动粘贴您的令牌。", + "accessToken": "访问令牌", + "required": "*", + "accessTokenPlaceholder": "访问令牌将自动填充...", + "machineId": "机器 ID", + "optional": "(可选)", + "machineIdPlaceholder": "机器 ID 将自动填充...", + "importing": "正在导入...", + "importToken": "导入令牌", + "cancel": "取消", + "errorAutoDetect": "无法自动检测令牌", + "errorAutoDetectFailed": "自动检测令牌失败", + "errorEnterToken": "请输入访问令牌", + "errorImportFailed": "导入失败" + }, + "pricingModal": { + "title": "定价配置", + "loading": "正在加载定价数据...", + "pricingRatesFormat": "定价费率格式", + "ratesDescription": "所有费率均为 每百万令牌美元($/1M 令牌)。示例:输入费率为 2.50 表示每 1,000,000 个输入令牌收费 2.50 美元。", + "model": "模型", + "input": "输入", + "output": "输出", + "cached": "缓存", + "reasoning": "推理", + "cacheCreation": "缓存创建", + "noPricingData": "无定价数据可用", + "resetToDefaults": "重置为默认值", + "cancel": "取消", + "saving": "正在保存...", + "saveChanges": "保存更改", + "resetConfirm": "将所有定价重置为默认值?此操作无法撤销。", + "errorSaveFailed": "保存定价失败", + "errorResetFailed": "重置定价失败" + }, + "proxyRegistry": { + "title": "代理注册表", + "description": "存储可重用的代理并跟踪分配。", + "importLegacy": "导入旧版", + "bulkAssign": "批量分配", + "addProxy": "添加代理", + "loading": "正在加载代理...", + "noProxies": "暂无保存的代理。", + "tableName": "名称", + "tableEndpoint": "端点", + "tableStatus": "状态", + "tableHealth": "健康状态(24小时)", + "tableUsage": "使用情况", + "tableActions": "操作", + "test": "测试", + "edit": "编辑", + "delete": "删除", + "modalCreateTitle": "创建代理", + "modalEditTitle": "编辑代理", + "labelName": "名称", + "labelType": "类型", + "labelHost": "主机", + "labelPort": "端口", + "labelUsername": "用户名", + "labelPassword": "密码", + "labelRegion": "区域", + "labelStatus": "状态", + "labelNotes": "备注", + "usernamePlaceholderEdit": "留空以保留当前用户名", + "passwordPlaceholderEdit": "留空以保留当前密码", + "statusActive": "活跃", + "statusInactive": "非活跃", + "cancel": "取消", + "save": "保存", + "bulkModalTitle": "批量代理分配", + "bulkLabelScope": "范围", + "bulkLabelProxy": "代理", + "bulkClearAssignment": "(清除分配)", + "bulkLabelScopeIds": "范围 ID(逗号或换行符分隔)", + "bulkScopeIdsPlaceholder": "provider-openai,provider-anthropic", + "bulkApply": "应用", + "errorLoadFailed": "加载代理注册表失败", + "errorNameHostRequired": "名称和主机为必填项", + "errorSaveFailed": "保存代理失败", + "errorDeleteFailed": "删除代理失败", + "errorForceDeleteConfirm": "此代理仍在分配中。强制删除并移除所有分配?", + "errorMigrateFailed": "迁移旧版代理配置失败", + "errorBulkFailed": "执行批量分配失败", + "success": "✓", + "failure": "✗", + "failed": "失败", + "successRate": "{rate}% 成功", + "avgLatency": "{latency} 毫秒平均", + "assignmentsCount": "{count} 个分配", + "noData": "—", + "testSuccess": "✓ {ip}", + "testLatency": "{latency}毫秒", + "testFailure": "✗ {error}" + }, + "playground": { + "title": "模型演练场", + "description": "直接从仪表板测试任何模型。选择提供商、模型和端点类型,然后发送请求以查看原始响应。", + "endpoint": "端点", + "provider": "提供商", + "model": "模型", + "accountKey": "账户 / 密钥", + "autoAccounts": "自动({count} 个账户)", + "noAccounts": "无账户", + "send": "发送", + "cancel": "取消", + "audioFile": "音频文件", + "attachImages": "附加图片(视觉)", + "multipartFormData": "multipart/form-data", + "upToImages": "最多 4 张图片", + "selectAudioFile": "选择音频文件进行转录(mp3、wav、m4a、ogg、flac…)", + "clearAll": "清除全部", + "request": "请求", + "response": "响应", + "transcription": "转录", + "copy": "复制", + "resetToDefault": "重置为默认", + "downloadAudio": "下载音频", + "copyText": "复制文本", + "transcriptionHint": "转录使用 multipart/form-data。上传上面的音频文件 — 下面的 JSON 控制额外参数(模型、语言)。", + "imagesGenerated": "生成了 {count} 张图片", + "generatedImage": "生成图片 {index}", + "save": "保存", + "endpointOptions": { + "chat": "聊天补全", + "responses": "响应", + "images": "图片生成", + "embeddings": "Embeddings", + "speech": "文本转语音", + "transcription": "音频转录", + "video": "视频生成", + "music": "音乐生成", + "rerank": "Rerank", + "search": "网页搜索" + } + }, + "requestLogger": { + "recording": "正在录制", + "paused": "已暂停", + "pipelineLogsOn": "管道日志开启", + "pipelineLogsOff": "管道日志关闭", + "updatingPipelineLogs": "正在更新管道日志...", + "searchPlaceholder": "搜索模型、提供商、账户、API密钥、组合...", + "allProviders": "所有提供商", + "allModels": "所有模型", + "allAccounts": "所有账户", + "allApiKeys": "所有API密钥", + "total": "总计", + "ok": "成功", + "err": "错误", + "combo": "组合", + "keys": "密钥", + "shown": "显示", + "sortNewest": "最新", + "sortOldest": "最早", + "sortTokensDesc": "令牌 ↓", + "sortTokensAsc": "令牌 ↑", + "sortDurationDesc": "时长 ↓", + "sortDurationAsc": "时长 ↑", + "sortStatusDesc": "状态 ↓", + "sortStatusAsc": "状态 ↑", + "sortModelAsc": "模型 A-Z", + "sortModelDesc": "模型 Z-A", + "statusFilters": { + "all": "全部", + "error": "错误", + "success": "成功", + "combo": "组合" + }, + "columns": { + "status": "状态", + "model": "模型", + "requested": "请求", + "provider": "提供商", + "protocol": "请求协议", + "account": "账户", + "apiKey": "API密钥", + "combo": "组合", + "tokens": "Tokens", + "duration": "时长", + "time": "时间" + }, + "loadingLogs": "正在加载日志...", + "noLogs": "暂无日志记录。进行一些API调用以在此处查看它们。", + "noMatchingLogs": "没有匹配当前筛选器的日志。", + "callLogsInfo": "调用日志也保存为JSON文件到{dataDir},并根据{retentionDays}和{maxEntries}进行轮换。" } } diff --git a/src/shared/components/CursorAuthModal.tsx b/src/shared/components/CursorAuthModal.tsx index 3f05e3a34d..f27057613b 100644 --- a/src/shared/components/CursorAuthModal.tsx +++ b/src/shared/components/CursorAuthModal.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import PropTypes from "prop-types"; +import { useTranslations } from "next-intl"; import Modal from "./Modal"; import Button from "./Button"; import Input from "./Input"; @@ -11,6 +12,7 @@ import Input from "./Input"; * Auto-detect and import token from Cursor IDE's local SQLite database */ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { + const t = useTranslations("cursorAuthModal"); const [accessToken, setAccessToken] = useState(""); const [machineId, setMachineId] = useState(""); const [error, setError] = useState(null); @@ -36,10 +38,10 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { setMachineId(data.machineId || ""); setAutoDetected(true); } else { - setError(data.error || "Could not auto-detect tokens"); + setError(data.error || t("errorAutoDetect")); } } catch (err) { - setError("Failed to auto-detect tokens"); + setError(t("errorAutoDetectFailed")); } finally { setAutoDetecting(false); } @@ -50,7 +52,7 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { const handleImportToken = async () => { if (!accessToken.trim()) { - setError("Please enter an access token"); + setError(t("errorEnterToken")); return; } @@ -70,7 +72,7 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { const data = await res.json(); if (!res.ok) { - throw new Error(data.error || "Import failed"); + throw new Error(data.error || t("errorImportFailed")); } // Success - close modal and trigger refresh @@ -84,7 +86,7 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { }; return ( - +
{/* Auto-detecting state */} {autoDetecting && ( @@ -94,8 +96,8 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { progress_activity
-

Auto-detecting tokens...

-

Reading from Cursor IDE or cursor-agent

+

{t("autoDetecting")}

+

{t("readingFromCursor")}

)} @@ -110,7 +112,7 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { check_circle

- Tokens auto-detected from Cursor IDE successfully! + {t("tokensAutoDetected")}

@@ -124,7 +126,7 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { info

- Cursor IDE not detected. Please paste your tokens manually. + {t("cursorNotDetected")}

@@ -133,12 +135,12 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { {/* Access Token Input */}
NameEndpointStatusHealth (24h)UsageActions{t("tableName")}{t("tableEndpoint")}{t("tableStatus")}{t("tableHealth")}{t("tableUsage")}{t("tableActions")}
{usageById[item.id] != null - ? `${usageById[item.id].count} assignment(s)` - : "—"} + ? t("assignmentsCount", { count: usageById[item.id].count }) + : t("noData")}
@@ -505,7 +507,7 @@ export default function ProxyRegistryManager() { onClick={() => void handleTestProxy(item)} loading={testingId === item.id} > - Test + {t("test")}