diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 8fc3729e3a..1a616a45a9 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -866,7 +866,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { onClick={() => handleMoveUp(index)} disabled={index === 0} className={`p-0.5 rounded ${index === 0 ? "text-text-muted/20 cursor-not-allowed" : "text-text-muted hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"}`} - title="Move up" + title={t("moveUp")} > arrow_upward @@ -876,7 +876,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { onClick={() => handleMoveDown(index)} disabled={index === models.length - 1} className={`p-0.5 rounded ${index === models.length - 1 ? "text-text-muted/20 cursor-not-allowed" : "text-text-muted hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"}`} - title="Move down" + title={t("moveDown")} > arrow_downward @@ -889,7 +889,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index ca6963e573..1bebb37da0 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -2664,7 +2664,7 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) label={t("prefixLabel")} value={formData.prefix} onChange={(e) => setFormData({ ...formData, prefix: e.target.value })} - placeholder={isAnthropic ? "ac-prod" : "oc-prod"} + placeholder={isAnthropic ? t("anthropicPrefixPlaceholder") : t("openaiPrefixPlaceholder")} hint={t("prefixHint")} /> {!isAnthropic && ( @@ -2679,7 +2679,9 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) label={t("baseUrlLabel")} value={formData.baseUrl} onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })} - placeholder={isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"} + placeholder={ + isAnthropic ? t("anthropicBaseUrlPlaceholder") : t("openaiBaseUrlPlaceholder") + } hint={t("compatibleBaseUrlHint", { type: isAnthropic ? t("anthropic") : t("openai"), })} diff --git a/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.tsx b/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.tsx index 88ccd97b34..4b29f1b208 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.tsx @@ -9,22 +9,26 @@ */ import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslations } from "next-intl"; import { Button } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; -const STATUS_CONFIG = { - available: { icon: "check_circle", color: "#22c55e", label: "Available" }, - cooldown: { icon: "schedule", color: "#f59e0b", label: "Cooldown" }, - unavailable: { icon: "error", color: "#ef4444", label: "Unavailable" }, - unknown: { icon: "help", color: "#6b7280", label: "Unknown" }, -}; - export default function ModelAvailabilityBadge() { - const [data, setData] = useState(null); + const t = useTranslations("providers"); + const tc = useTranslations("common"); + + const STATUS_CONFIG = { + available: { icon: "check_circle", color: "#22c55e", label: t("available") }, + cooldown: { icon: "schedule", color: "#f59e0b", label: t("cooldown") }, + unavailable: { icon: "error", color: "#ef4444", label: t("unavailable") }, + unknown: { icon: "help", color: "#6b7280", label: t("unknown") }, + }; + + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [expanded, setExpanded] = useState(false); - const [clearing, setClearing] = useState(null); - const ref = useRef(null); + const [clearing, setClearing] = useState(null); + const ref = useRef(null); const notify = useNotificationStore(); const fetchStatus = useCallback(async () => { @@ -49,8 +53,8 @@ export default function ModelAvailabilityBadge() { // Close popover on outside click useEffect(() => { - const handleClick = (e) => { - if (ref.current && !ref.current.contains(e.target)) { + const handleClick = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { setExpanded(false); } }; @@ -58,7 +62,7 @@ export default function ModelAvailabilityBadge() { return () => document.removeEventListener("mousedown", handleClick); }, [expanded]); - const handleClearCooldown = async (provider, model) => { + const handleClearCooldown = async (provider: string, model: string) => { setClearing(`${provider}:${model}`); try { const res = await fetch("/api/models/availability", { @@ -67,13 +71,13 @@ export default function ModelAvailabilityBadge() { body: JSON.stringify({ action: "clearCooldown", provider, model }), }); if (res.ok) { - notify.success(`Cooldown cleared for ${model}`); + notify.success(t("cooldownCleared", { model })); await fetchStatus(); } else { - notify.error("Failed to clear cooldown"); + notify.error(t("failedClearCooldown")); } } catch { - notify.error("Failed to clear cooldown"); + notify.error(t("failedClearCooldown")); } finally { setClearing(null); } @@ -83,12 +87,12 @@ export default function ModelAvailabilityBadge() { const models = data?.models || []; const unavailableCount = - data?.unavailableCount || models.filter((m) => m.status !== "available").length; + data?.unavailableCount || models.filter((m: any) => m.status !== "available").length; const isHealthy = unavailableCount === 0; // Group unhealthy models by provider - const byProvider = {}; - models.forEach((m) => { + const byProvider: Record = {}; + models.forEach((m: any) => { if (m.status === "available") return; const key = m.provider || "unknown"; if (!byProvider[key]) byProvider[key] = []; @@ -105,12 +109,10 @@ export default function ModelAvailabilityBadge() { : "bg-amber-500/10 border-amber-500/20 text-amber-500 hover:bg-amber-500/15" }`} > - + - {isHealthy - ? "All models operational" - : `${unavailableCount} model${unavailableCount !== 1 ? "s" : ""} with issues`} + {isHealthy ? t("allModelsOperational") : t("modelsWithIssues", { count: unavailableCount })} {/* Expanded popover */} @@ -121,25 +123,26 @@ export default function ModelAvailabilityBadge() { - Model Status + {t("modelStatus")}
{isHealthy ? ( -

- All models are responding normally. -

+

{t("allModelsNormal")}

) : (
{Object.entries(byProvider).map(([provider, provModels]) => ( @@ -148,8 +151,10 @@ export default function ModelAvailabilityBadge() { {provider}

- {(provModels as any).map((m) => { - const status = STATUS_CONFIG[m.status] || STATUS_CONFIG.unknown; + {provModels.map((m) => { + const status = + STATUS_CONFIG[m.status as keyof typeof STATUS_CONFIG] || + STATUS_CONFIG.unknown; const isClearing = clearing === `${m.provider}:${m.model}`; return ( diff --git a/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityPanel.tsx b/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityPanel.tsx index b1f603dd50..2ede152ba5 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityPanel.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityPanel.tsx @@ -8,20 +8,24 @@ */ import { useState, useEffect, useCallback } from "react"; -import { Card, Button, EmptyState } from "@/shared/components"; +import { useTranslations } from "next-intl"; +import { Card, Button } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; -const STATUS_CONFIG = { - available: { icon: "check_circle", color: "#22c55e", label: "Available" }, - cooldown: { icon: "schedule", color: "#f59e0b", label: "Cooldown" }, - unavailable: { icon: "error", color: "#ef4444", label: "Unavailable" }, - unknown: { icon: "help", color: "#6b7280", label: "Unknown" }, -}; - export default function ModelAvailabilityPanel() { - const [data, setData] = useState(null); + const t = useTranslations("providers"); + const tc = useTranslations("common"); + + const STATUS_CONFIG = { + available: { icon: "check_circle", color: "#22c55e", label: t("available") }, + cooldown: { icon: "schedule", color: "#f59e0b", label: t("cooldown") }, + unavailable: { icon: "error", color: "#ef4444", label: t("unavailable") }, + unknown: { icon: "help", color: "#6b7280", label: t("unknown") }, + }; + + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); - const [clearing, setClearing] = useState(null); + const [clearing, setClearing] = useState(null); const notify = useNotificationStore(); const fetchStatus = useCallback(async () => { @@ -44,7 +48,7 @@ export default function ModelAvailabilityPanel() { return () => clearInterval(interval); }, [fetchStatus]); - const handleClearCooldown = async (provider, model) => { + const handleClearCooldown = async (provider: string, model: string) => { setClearing(`${provider}:${model}`); try { const res = await fetch("/api/models/availability", { @@ -53,13 +57,13 @@ export default function ModelAvailabilityPanel() { body: JSON.stringify({ action: "clearCooldown", provider, model }), }); if (res.ok) { - notify.success(`Cooldown cleared for ${model}`); + notify.success(t("cooldownCleared", { model })); await fetchStatus(); } else { - notify.error("Failed to clear cooldown"); + notify.error(t("failedClearCooldown")); } } catch { - notify.error("Failed to clear cooldown"); + notify.error(t("failedClearCooldown")); } finally { setClearing(null); } @@ -69,8 +73,10 @@ export default function ModelAvailabilityPanel() { return (
- monitoring - Loading model availability... + + {t("loadingAvailability")}
); @@ -78,18 +84,20 @@ export default function ModelAvailabilityPanel() { const models = data?.models || []; const unavailableCount = - data?.unavailableCount || models.filter((m) => m.status !== "available").length; + data?.unavailableCount || models.filter((m: any) => m.status !== "available").length; if (models.length === 0 || unavailableCount === 0) { return (
- verified +
-

Model Availability

-

All models operational

+

{t("modelAvailability")}

+

{t("allModelsOperational")}

@@ -97,8 +105,8 @@ export default function ModelAvailabilityPanel() { } // Group by provider - const byProvider = {}; - models.forEach((m) => { + const byProvider: Record = {}; + models.forEach((m: any) => { if (m.status === "available") return; const key = m.provider || "unknown"; if (!byProvider[key]) byProvider[key] = []; @@ -110,17 +118,27 @@ export default function ModelAvailabilityPanel() {
- warning +
-

Model Availability

+

{t("modelAvailability")}

- {unavailableCount} model{unavailableCount !== 1 ? "s" : ""} with issues + {t("modelsWithIssues", { count: unavailableCount })}

-
@@ -129,8 +147,9 @@ export default function ModelAvailabilityPanel() {

{provider}

- {(provModels as any).map((m) => { - const status = STATUS_CONFIG[m.status] || STATUS_CONFIG.unknown; + {provModels.map((m) => { + const status = + STATUS_CONFIG[m.status as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.unknown; const isClearing = clearing === `${m.provider}:${m.model}`; return ( @@ -168,7 +188,7 @@ export default function ModelAvailabilityPanel() { disabled={isClearing} className="text-xs" > - {isClearing ? "Clearing..." : "Clear"} + {isClearing ? t("clearing") : t("clearCooldown")} )}
diff --git a/src/app/(dashboard)/dashboard/providers/new/page.tsx b/src/app/(dashboard)/dashboard/providers/new/page.tsx index 8edb03c7b8..605426ff80 100644 --- a/src/app/(dashboard)/dashboard/providers/new/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/new/page.tsx @@ -94,7 +94,7 @@ export default function NewProviderPage() {
{/* Provider Selection */} handleChange("displayName", e.target.value)} - hint="Optional. A friendly name to identify this configuration." + hint={t("displayNameHint")} /> {/* Active Toggle */} diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index c51591df2d..f3dbac6780 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -26,17 +26,19 @@ import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge"; import { useTranslations } from "next-intl"; // Shared helper function to avoid code duplication between ProviderCard and ApiKeyProviderCard -function getStatusDisplay(connected, error, errorCode) { +function getStatusDisplay(connected, error, errorCode, t) { const parts = []; if (connected > 0) { parts.push( - {connected} Connected + {t("connected", { count: connected })} ); } if (error > 0) { - const errText = errorCode ? `${error} Error (${errorCode})` : `${error} Error`; + const errText = errorCode + ? t("errorCount", { count: error, code: errorCode }) + : t("errorCountNoCode", { count: error }); parts.push( {errText} @@ -44,7 +46,7 @@ function getStatusDisplay(connected, error, errorCode) { ); } if (parts.length === 0) { - return No connections; + return {t("noConnections")}; } return parts; } @@ -90,13 +92,13 @@ function getConnectionErrorTag(connection) { } export default function ProvidersPage() { - const [connections, setConnections] = useState([]); - const [providerNodes, setProviderNodes] = useState([]); + const [connections, setConnections] = useState([]); + const [providerNodes, setProviderNodes] = useState([]); const [loading, setLoading] = useState(true); const [showAddCompatibleModal, setShowAddCompatibleModal] = useState(false); const [showAddAnthropicCompatibleModal, setShowAddAnthropicCompatibleModal] = useState(false); - const [testingMode, setTestingMode] = useState(null); - const [testResults, setTestResults] = useState(null); + const [testingMode, setTestingMode] = useState(null); + const [testResults, setTestResults] = useState(null); const notify = useNotificationStore(); const t = useTranslations("providers"); const tc = useTranslations("common"); @@ -212,7 +214,7 @@ export default function ProvidersPage() { .filter((node) => node.type === "openai-compatible") .map((node) => ({ id: node.id, - name: node.name || "OpenAI Compatible", + name: node.name || t("openaiCompatibleName"), color: "#10A37F", textIcon: "OC", apiType: node.apiType, @@ -222,7 +224,7 @@ export default function ProvidersPage() { .filter((node) => node.type === "anthropic-compatible") .map((node) => ({ id: node.id, - name: node.name || "Anthropic Compatible", + name: node.name || t("anthropicCompatibleName"), color: "#D97757", textIcon: "AC", })); @@ -243,7 +245,7 @@ export default function ProvidersPage() {

{t("oauthProviders")}{" "} - +

@@ -284,7 +286,7 @@ export default function ProvidersPage() {

{t("freeProviders")}{" "} - +

@@ -465,6 +467,8 @@ export default function ProvidersPage() { } function ProviderCard({ providerId, provider, stats, authType, onToggle }) { + const t = useTranslations("providers"); + const tc = useTranslations("common"); const { connected, error, errorCode, errorTime, allDisabled } = stats; const [imgError, setImgError] = useState(false); @@ -474,7 +478,12 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) { apikey: "bg-amber-500", compatible: "bg-orange-500", }; - const dotLabels = { free: "Free", oauth: "OAuth", apikey: "API Key", compatible: "Compatible" }; + const dotLabels = { + free: tc("free"), + oauth: t("oauthLabel"), + apikey: t("apiKeyLabel"), + compatible: t("compatibleLabel"), + }; return ( @@ -509,7 +518,7 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) { {provider.name}
@@ -517,12 +526,12 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) { pause_circle - Disabled + {t("disabled")} ) : ( <> - {getStatusDisplay(connected, error, errorCode)} + {getStatusDisplay(connected, error, errorCode, t)} {errorTime && • {errorTime}} )} @@ -543,7 +552,7 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) { size="sm" checked={!allDisabled} onChange={() => {}} - title={allDisabled ? "Enable provider" : "Disable provider"} + title={allDisabled ? t("enableProvider") : t("disableProvider")} />
)} @@ -576,6 +585,8 @@ ProviderCard.propTypes = { // API Key providers - use image with textIcon fallback (same as OAuth providers) function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle }) { + const t = useTranslations("providers"); + const tc = useTranslations("common"); const { connected, error, errorCode, errorTime, allDisabled } = stats; const isCompatible = providerId.startsWith(OPENAI_COMPATIBLE_PREFIX); const isAnthropicCompatible = providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX); @@ -587,7 +598,12 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle }) apikey: "bg-amber-500", compatible: "bg-orange-500", }; - const dotLabels = { free: "Free", oauth: "OAuth", apikey: "API Key", compatible: "Compatible" }; + const dotLabels = { + free: tc("free"), + oauth: t("oauthLabel"), + apikey: t("apiKeyLabel"), + compatible: t("compatibleLabel"), + }; // Determine icon path: OpenAI Compatible providers use specialized icons const getIconPath = () => { @@ -633,7 +649,7 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle }) {provider.name}
@@ -641,20 +657,20 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle }) pause_circle - Disabled + {t("disabled")} ) : ( <> - {getStatusDisplay(connected, error, errorCode)} + {getStatusDisplay(connected, error, errorCode, t)} {isCompatible && ( - {provider.apiType === "responses" ? "Responses" : "Chat"} + {provider.apiType === "responses" ? t("responses") : t("chat")} )} {isAnthropicCompatible && ( - Messages + {t("messages")} )} {errorTime && • {errorTime}} @@ -677,7 +693,7 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle }) size="sm" checked={!allDisabled} onChange={() => {}} - title={allDisabled ? "Enable provider" : "Disable provider"} + title={allDisabled ? t("enableProvider") : t("disableProvider")} />
)} @@ -710,6 +726,7 @@ ApiKeyProviderCard.propTypes = { }; function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { + const t = useTranslations("providers"); const [formData, setFormData] = useState({ name: "", prefix: "", @@ -719,11 +736,11 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { const [submitting, setSubmitting] = useState(false); const [checkKey, setCheckKey] = useState(""); const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); + const [validationResult, setValidationResult] = useState<"success" | "failed" | null>(null); const apiTypeOptions = [ - { value: "chat", label: "Chat Completions" }, - { value: "responses", label: "Responses API" }, + { value: "chat", label: t("chatCompletions") }, + { value: "responses", label: t("responsesApi") }, ]; useEffect(() => { @@ -790,38 +807,38 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { }; return ( - +
setFormData({ ...formData, name: e.target.value })} - placeholder="OpenAI Compatible (Prod)" - hint="Required. A friendly label for this node." + placeholder={t("compatibleProdPlaceholder", { type: t("openai") })} + hint={t("nameHint")} /> setFormData({ ...formData, prefix: e.target.value })} - placeholder="oc-prod" - hint="Required. Used as the provider prefix for model IDs." + placeholder={t("openaiPrefixPlaceholder")} + hint={t("prefixHint")} /> setFormData({ ...formData, baseUrl: e.target.value })} - placeholder="https://api.openai.com/v1" - hint="Use the base URL (ending in /v1) for your OpenAI-compatible API." + placeholder={t("openaiBaseUrlPlaceholder")} + hint={t("compatibleBaseUrlHint", { type: t("openai") })} />
setCheckKey(e.target.value)} @@ -833,13 +850,13 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary" > - {validating ? "Checking..." : "Check"} + {validating ? t("checking") : t("check")}
{validationResult && ( - {validationResult === "success" ? "Valid" : "Invalid"} + {validationResult === "success" ? t("valid") : t("invalid")} )}
@@ -853,10 +870,10 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { submitting } > - {submitting ? "Creating..." : "Create"} + {submitting ? t("creating") : t("add")}
@@ -871,6 +888,7 @@ AddOpenAICompatibleModal.propTypes = { }; function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) { + const t = useTranslations("providers"); const [formData, setFormData] = useState({ name: "", prefix: "", @@ -879,7 +897,7 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) { const [submitting, setSubmitting] = useState(false); const [checkKey, setCheckKey] = useState(""); const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); + const [validationResult, setValidationResult] = useState<"success" | "failed" | null>(null); useEffect(() => { // Reset validation when modal opens @@ -943,32 +961,32 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) { }; return ( - +
setFormData({ ...formData, name: e.target.value })} - placeholder="Anthropic Compatible (Prod)" - hint="Required. A friendly label for this node." + placeholder={t("compatibleProdPlaceholder", { type: t("anthropic") })} + hint={t("nameHint")} /> setFormData({ ...formData, prefix: e.target.value })} - placeholder="ac-prod" - hint="Required. Used as the provider prefix for model IDs." + placeholder={t("anthropicPrefixPlaceholder")} + hint={t("prefixHint")} /> setFormData({ ...formData, baseUrl: e.target.value })} - placeholder="https://api.anthropic.com/v1" - hint="Use the base URL (ending in /v1) for your Anthropic-compatible API. The system will append /messages." + placeholder={t("anthropicBaseUrlPlaceholder")} + hint={t("compatibleBaseUrlHint", { type: t("anthropic") })} />
setCheckKey(e.target.value)} @@ -980,13 +998,13 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) { disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary" > - {validating ? "Checking..." : "Check"} + {validating ? t("checking") : t("check")}
{validationResult && ( - {validationResult === "success" ? "Valid" : "Invalid"} + {validationResult === "success" ? t("valid") : t("invalid")} )}
@@ -1000,10 +1018,10 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) { submitting } > - {submitting ? "Creating..." : "Create"} + {submitting ? t("creating") : t("add")}
@@ -1020,6 +1038,9 @@ AddAnthropicCompatibleModal.propTypes = { // ─── Provider Test Results View (mirrors combo TestResultsView) ────────────── function ProviderTestResultsView({ results }) { + const t = useTranslations("providers"); + const tc = useTranslations("common"); + if (results.error && !results.results) { return (
@@ -1034,11 +1055,12 @@ function ProviderTestResultsView({ results }) { const modeLabel = { - oauth: "OAuth", - free: "Free", - apikey: "API Key", - provider: "Provider", - all: "All", + oauth: t("oauthLabel"), + free: tc("free"), + apikey: t("apiKeyLabel"), + compatible: t("compatibleLabel"), + provider: t("providerLabel"), + all: tc("all"), }[mode] || mode; return ( @@ -1046,16 +1068,18 @@ function ProviderTestResultsView({ results }) { {/* Summary header */} {summary && (
- {modeLabel} Test + {t("modeTest", { mode: modeLabel })} - {summary.passed} passed + {t("passedCount", { count: summary.passed })} {summary.failed > 0 && ( - {summary.failed} failed + {t("failedCount", { count: summary.failed })} )} - {summary.total} tested + + {t("testedCount", { count: summary.total })} +
)} @@ -1077,21 +1101,23 @@ function ProviderTestResultsView({ results }) { ({r.provider})
{r.latencyMs !== undefined && ( - {r.latencyMs}ms + + {t("millisecondsAbbr", { value: r.latencyMs })} + )} - {r.valid ? "OK" : r.diagnosis?.type || "ERROR"} + {r.valid ? t("okShort") : r.diagnosis?.type || t("errorShort")}
))} {items.length === 0 && (
- No active connections found for this group. + {t("noActiveConnectionsInGroup")}
)}
diff --git a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx index 81dc96d56a..79762aa881 100644 --- a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx @@ -11,6 +11,11 @@ export default function AppearanceTab() { const t = useTranslations("settings"); const [settings, setSettings] = useState>({}); const [loading, setLoading] = useState(true); + const themeOptionLabels: Record = { + light: t("themeLight"), + dark: t("themeDark"), + system: t("themeSystem"), + }; useEffect(() => { fetch("/api/settings") @@ -64,7 +69,7 @@ export default function AppearanceTab() {
{["light", "dark", "system"].map((option) => ( @@ -83,7 +88,7 @@ export default function AppearanceTab() { - {option} + {themeOptionLabels[option] || option} ))}
diff --git a/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx index 5c7c288710..f131c74c32 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx @@ -21,6 +21,20 @@ export default function ComboDefaultsTab() { const [saving, setSaving] = useState(false); const t = useTranslations("settings"); const tc = useTranslations("common"); + const strategyOptions = [ + { value: "priority", label: t("priority"), icon: "sort" }, + { value: "weighted", label: t("weighted"), icon: "percent" }, + { value: "round-robin", label: t("roundRobin"), icon: "autorenew" }, + { value: "random", label: t("random"), icon: "shuffle" }, + { value: "least-used", label: t("leastUsed"), icon: "low_priority" }, + { value: "cost-optimized", label: t("costOpt"), icon: "savings" }, + ]; + const numericSettings = [ + { key: "maxRetries", label: t("maxRetriesLabel"), min: 0, max: 5 }, + { key: "retryDelayMs", label: t("retryDelayLabel"), min: 500, max: 10000, step: 500 }, + { key: "timeoutMs", label: t("timeoutLabel"), min: 5000, max: 300000, step: 5000 }, + { key: "maxComboDepth", label: t("maxNestingDepth"), min: 1, max: 10 }, + ]; useEffect(() => { fetch("/api/settings/combo-defaults") @@ -82,17 +96,10 @@ export default function ComboDefaultsTab() {
- {[ - { value: "priority", label: "Priority", icon: "sort" }, - { value: "weighted", label: "Weighted", icon: "percent" }, - { value: "round-robin", label: "Round-Robin", icon: "autorenew" }, - { value: "random", label: "Random", icon: "shuffle" }, - { value: "least-used", label: "Least-Used", icon: "low_priority" }, - { value: "cost-optimized", label: "Cost-Opt", icon: "savings" }, - ].map((s) => ( + {strategyOptions.map((s) => (
setFilters((prev) => ({ ...prev, [key]: val }))} diff --git a/src/app/(dashboard)/dashboard/settings/components/FallbackChainsEditor.tsx b/src/app/(dashboard)/dashboard/settings/components/FallbackChainsEditor.tsx index 18c0a78cd7..f9a2100cf3 100644 --- a/src/app/(dashboard)/dashboard/settings/components/FallbackChainsEditor.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/FallbackChainsEditor.tsx @@ -146,13 +146,13 @@ export default function FallbackChainsEditor() {
setNewModel(e.target.value)} /> setNewProviders(e.target.value)} /> @@ -204,7 +204,7 @@ export default function FallbackChainsEditor() { diff --git a/src/app/(dashboard)/dashboard/settings/components/IPFilterSection.tsx b/src/app/(dashboard)/dashboard/settings/components/IPFilterSection.tsx index 37f4267baa..0de83efdf8 100644 --- a/src/app/(dashboard)/dashboard/settings/components/IPFilterSection.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/IPFilterSection.tsx @@ -1,14 +1,14 @@ "use client"; import { useState, useEffect } from "react"; -import { Card, Button, Input, Toggle } from "@/shared/components"; +import { Card, Button, Input } from "@/shared/components"; import { useTranslations } from "next-intl"; const MODES = [ - { value: "disabled", label: "Disabled", icon: "block" }, - { value: "blacklist", label: "Blacklist", icon: "do_not_disturb" }, - { value: "whitelist", label: "Whitelist", icon: "verified_user" }, - { value: "whitelist-priority", label: "WL Priority", icon: "priority_high" }, + { value: "disabled", labelKey: "ipModeDisabled", icon: "block" }, + { value: "blacklist", labelKey: "ipModeBlacklist", icon: "do_not_disturb" }, + { value: "whitelist", labelKey: "ipModeWhitelist", icon: "verified_user" }, + { value: "whitelist-priority", labelKey: "ipModeWhitelistPriority", icon: "priority_high" }, ]; export default function IPFilterSection() { @@ -49,8 +49,6 @@ export default function IPFilterSection() { } catch {} }; - const toggleEnabled = () => updateConfig({ enabled: !config.enabled }); - const setMode = (mode) => { if (mode === "disabled") { updateConfig({ enabled: false }); @@ -112,7 +110,7 @@ export default function IPFilterSection() { - {m.label} + {t(m.labelKey)} ))} @@ -125,7 +123,7 @@ export default function IPFilterSection() {
setNewIP(e.target.value)} onKeyDown={(e) => e.key === "Enter" && addIP()} @@ -159,7 +157,7 @@ export default function IPFilterSection() { {config.blacklist.length > 0 && (

- Blocked ({config.blacklist.length}) + {t("blocked", { count: config.blacklist.length })}

{config.blacklist.map((ip) => ( @@ -185,7 +183,7 @@ export default function IPFilterSection() { {config.whitelist.length > 0 && (

- Allowed ({config.whitelist.length}) + {t("allowed", { count: config.whitelist.length })}

{config.whitelist.map((ip) => ( @@ -211,7 +209,7 @@ export default function IPFilterSection() { {config.tempBans.length > 0 && (

- Temporary Bans ({config.tempBans.length}) + {t("temporaryBans", { count: config.tempBans.length })}

{config.tempBans.map((ban) => ( @@ -226,7 +224,7 @@ export default function IPFilterSection() {
- {Math.ceil(ban.remainingMs / 60000)}m left + {t("minLeft", { min: Math.ceil(ban.remainingMs / 60000) })}
@@ -340,6 +368,7 @@ function PoliciesCard() { const [loading, setLoading] = useState(true); const [unlocking, setUnlocking] = useState(null); const notify = useNotificationStore(); + const locale = useLocale(); const t = useTranslations("settings"); const fetchPolicies = useCallback(async () => { @@ -371,7 +400,7 @@ function PoliciesCard() { body: JSON.stringify({ action: "unlock", identifier }), }); if (res.ok) { - notify.success(`Unlocked: ${identifier}`); + notify.success(t("unlockedIdentifier", { identifier })); await fetchPolicies(); } else { notify.error(t("failedUnlock")); @@ -448,7 +477,7 @@ function PoliciesCard() { {status.icon} - {cb.name || cb.provider || "Unknown"} + {cb.name || cb.provider || t("unknown")} - {status.label} + {getBreakerStateLabel(cb.state, t)} {cb.failures > 0 && ( - {cb.failures} failures + {t("failures", { count: cb.failures })} )}
@@ -491,7 +520,9 @@ function PoliciesCard() { {identifier} {typeof id === "object" && id.lockedAt && ( - since {new Date(id.lockedAt).toLocaleString()} + {t("sinceDate", { + date: new Date(id.lockedAt).toLocaleString(locale), + })} )}
@@ -529,16 +560,16 @@ export default function ResilienceTab() { try { setLoading(true); const res = await fetch("/api/resilience"); - if (!res.ok) throw new Error(`Failed to load: ${res.status}`); + if (!res.ok) throw new Error(t("failedLoadWithStatus", { status: res.status })); const json = await res.json(); setData(json); setError(null); } catch (err) { - setError(err.message); + setError(getErrorMessage(err, t("failedLoadResilience"))); } finally { setLoading(false); } - }, []); + }, [t]); useEffect(() => { loadData(); @@ -551,10 +582,10 @@ export default function ResilienceTab() { try { setLoading(true); const res = await fetch("/api/resilience/reset", { method: "POST" }); - if (!res.ok) throw new Error("Reset failed"); + if (!res.ok) throw new Error(t("resetFailed")); await loadData(); } catch (err) { - setError(err.message); + setError(getErrorMessage(err, t("resetFailed"))); } finally { setLoading(false); } @@ -568,10 +599,10 @@ export default function ResilienceTab() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ profiles }), }); - if (!res.ok) throw new Error("Save failed"); + if (!res.ok) throw new Error(t("saveFailed")); await loadData(); } catch (err) { - setError(err.message); + setError(getErrorMessage(err, t("saveFailed"))); } finally { setSaving(false); } @@ -585,10 +616,10 @@ export default function ResilienceTab() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ defaults }), }); - if (!res.ok) throw new Error("Save failed"); + if (!res.ok) throw new Error(t("saveFailed")); await loadData(); } catch (err) { - setError(err.message); + setError(getErrorMessage(err, t("saveFailed"))); } finally { setSaving(false); } diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index 4494ab357e..567185d9d4 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -1,30 +1,30 @@ "use client"; import { useState, useEffect } from "react"; -import { Card, Input, Toggle, Button } from "@/shared/components"; +import { Card, Input, Button } from "@/shared/components"; import FallbackChainsEditor from "./FallbackChainsEditor"; import { useTranslations } from "next-intl"; const STRATEGIES = [ { value: "fill-first", - label: "Fill First", - desc: "Use accounts in priority order", + labelKey: "fillFirst", + descKey: "fillFirstDesc", icon: "vertical_align_top", }, - { value: "round-robin", label: "Round Robin", desc: "Cycle through all accounts", icon: "loop" }, - { value: "p2c", label: "P2C", desc: "Pick 2 random, use the healthier one", icon: "balance" }, - { value: "random", label: "Random", desc: "Random account each request", icon: "shuffle" }, + { value: "round-robin", labelKey: "roundRobin", descKey: "roundRobinDesc", icon: "loop" }, + { value: "p2c", labelKey: "p2c", descKey: "p2cDesc", icon: "balance" }, + { value: "random", labelKey: "random", descKey: "randomDesc", icon: "shuffle" }, { value: "least-used", - label: "Least Used", - desc: "Pick least recently used account", + labelKey: "leastUsed", + descKey: "leastUsedDesc", icon: "low_priority", }, { value: "cost-optimized", - label: "Cost Opt", - desc: "Prefer cheapest available account", + labelKey: "costOpt", + descKey: "costOptDesc", icon: "savings", }, ]; @@ -36,6 +36,14 @@ export default function RoutingTab() { const [newPattern, setNewPattern] = useState(""); const [newTarget, setNewTarget] = useState(""); const t = useTranslations("settings"); + const strategyHintKeyByValue: Record = { + "fill-first": "fillFirstDesc", + "round-robin": "roundRobinDesc", + p2c: "p2cDesc", + random: "randomDesc", + "least-used": "leastUsedDesc", + "cost-optimized": "costOptDesc", + }; useEffect(() => { fetch("/api/settings") @@ -114,9 +122,9 @@ export default function RoutingTab() {

- {s.label} + {t(s.labelKey)}

-

{s.desc}

+

{t(s.descKey)}

))} @@ -141,18 +149,7 @@ export default function RoutingTab() { )}

- {settings.fallbackStrategy === "round-robin" && - `Distributing requests across accounts with ${settings.stickyRoundRobinLimit || 3} calls per account.`} - {settings.fallbackStrategy === "fill-first" && - "Using accounts in priority order (Fill First)."} - {settings.fallbackStrategy === "p2c" && - "Power of Two Choices: picks 2 random accounts and routes to the healthier one."} - {settings.fallbackStrategy === "random" && - "Randomly selects an available account for each request."} - {settings.fallbackStrategy === "least-used" && - "Picks the account that was used least recently."} - {settings.fallbackStrategy === "cost-optimized" && - "Prefers accounts with the lowest cost (priority-based, extensible with actual cost data)."} + {t(strategyHintKeyByValue[settings.fallbackStrategy] || "fillFirstDesc")}

@@ -199,7 +196,7 @@ export default function RoutingTab() {
setNewPattern(e.target.value)} /> @@ -207,7 +204,7 @@ export default function RoutingTab() {
setNewTarget(e.target.value)} /> diff --git a/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx index 954dcdee75..5583361467 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx @@ -188,14 +188,7 @@ export default function SecurityTab() {

{t("requireAuthModels")}

-

- When ON, the{" "} - - /v1/models - {" "} - endpoint returns 404 for unauthenticated requests. Prevents model discovery by - unauthorized users. -

+

{t("requireAuthModelsDesc")}

{t("blockedProviders")}

-

- Hide specific providers from the{" "} - - /v1/models - {" "} - response. Blocked providers will not appear in model listings. -

+

{t("blockedProvidersDesc")}

{Object.values(AI_PROVIDERS).map((provider: any) => { @@ -229,7 +216,11 @@ export default function SecurityTab() { ? "bg-red-500/10 border-red-500/30 text-red-600 dark:text-red-400" : "bg-black/[0.02] dark:bg-white/[0.02] border-transparent text-text-muted hover:bg-black/[0.05] dark:hover:bg-white/[0.05]" }`} - title={isBlocked ? `Unblock ${provider.name}` : `Block ${provider.name}`} + title={ + isBlocked + ? t("unblockProviderTitle", { provider: provider.name }) + : t("blockProviderTitle", { provider: provider.name }) + } > 0 && (

warning - {blockedProviders.length} provider{blockedProviders.length !== 1 ? "s" : ""} blocked - from /models + {t("providersBlocked", { count: blockedProviders.length })}

)}
diff --git a/src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx b/src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx index 9a0dfa8e01..b3e6e36fea 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx @@ -32,7 +32,7 @@ export default function SessionInfoCard() { const loginTime = sessionStorage.getItem("omniroute_login_time"); const now = Date.now(); - let sessionAge = "Unknown"; + let sessionAge = t("unknown"); if (loginTime) { const elapsed = now - parseInt(loginTime, 10); const hours = Math.floor(elapsed / 3600000); @@ -61,7 +61,7 @@ export default function SessionInfoCard() { loginTime: loginTime ? new Date(parseInt(loginTime, 10)).toLocaleString() : null, sessionAge, ipAddress: "—", // Server-side only - userAgent: navigator.userAgent.split(" ").slice(-2).join(" ") || "Unknown", + userAgent: navigator.userAgent.split(" ").slice(-2).join(" ") || t("unknown"), }); setLoading(false); } @@ -109,7 +109,7 @@ export default function SessionInfoCard() {

{t("session")}

-
+
{t("status")} diff --git a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx index b732352a25..8df06ecd8f 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { Card, Button, Badge } from "@/shared/components"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; export default function SystemStorageTab() { const [backups, setBackups] = useState([]); @@ -19,6 +19,7 @@ export default function SystemStorageTab() { const [confirmImport, setConfirmImport] = useState(false); const [pendingImportFile, setPendingImportFile] = useState(null); const fileInputRef = useRef(null); + const locale = useLocale(); const t = useTranslations("settings"); const tc = useTranslations("common"); const [storageHealth, setStorageHealth] = useState({ @@ -61,20 +62,23 @@ export default function SystemStorageTab() { const data = await res.json(); if (res.ok) { if (data.filename) { - setManualBackupStatus({ type: "success", message: `Backup created: ${data.filename}` }); + setManualBackupStatus({ + type: "success", + message: t("backupCreated", { file: data.filename }), + }); } else { setManualBackupStatus({ type: "info", - message: data.message || "No changes since last backup", + message: data.message || t("noChangesSinceBackup"), }); } await loadStorageHealth(); if (backupsExpanded) await loadBackups(); } else { - setManualBackupStatus({ type: "error", message: data.error || "Backup failed" }); + setManualBackupStatus({ type: "error", message: data.error || t("backupFailed") }); } } catch { - setManualBackupStatus({ type: "error", message: "An error occurred" }); + setManualBackupStatus({ type: "error", message: t("errorOccurred") }); } finally { setManualBackupLoading(false); } @@ -93,15 +97,20 @@ export default function SystemStorageTab() { if (res.ok) { setRestoreStatus({ type: "success", - message: `Restored! ${data.connectionCount} connections, ${data.nodeCount} nodes, ${data.comboCount} combos, ${data.apiKeyCount} API keys.`, + message: t("restoreSuccess", { + connections: data.connectionCount, + nodes: data.nodeCount, + combos: data.comboCount, + apiKeys: data.apiKeyCount, + }), }); await loadBackups(); await loadStorageHealth(); } else { - setRestoreStatus({ type: "error", message: data.error || "Restore failed" }); + setRestoreStatus({ type: "error", message: data.error || t("restoreFailed") }); } } catch { - setRestoreStatus({ type: "error", message: "An error occurred during restore" }); + setRestoreStatus({ type: "error", message: t("errorDuringRestore") }); } finally { setRestoringId(null); setConfirmRestoreId(null); @@ -118,7 +127,7 @@ export default function SystemStorageTab() { const res = await fetch("/api/db-backups/export"); if (!res.ok) { const data = await res.json(); - throw new Error(data.error || "Export failed"); + throw new Error(data.error || t("exportFailed")); } const blob = await res.blob(); const url = URL.createObjectURL(blob); @@ -136,7 +145,10 @@ export default function SystemStorageTab() { URL.revokeObjectURL(url); } catch (err) { console.error("Export failed:", err); - setImportStatus({ type: "error", message: `Export failed: ${(err as Error).message}` }); + setImportStatus({ + type: "error", + message: t("exportFailedWithError", { error: (err as Error).message }), + }); } finally { setExportLoading(false); } @@ -177,15 +189,20 @@ export default function SystemStorageTab() { if (res.ok) { setImportStatus({ type: "success", - message: `Database imported! ${data.connectionCount} connections, ${data.nodeCount} nodes, ${data.comboCount} combos, ${data.apiKeyCount} API keys.`, + message: t("importSuccess", { + connections: data.connectionCount, + nodes: data.nodeCount, + combos: data.comboCount, + apiKeys: data.apiKeyCount, + }), }); await loadStorageHealth(); if (backupsExpanded) await loadBackups(); } else { - setImportStatus({ type: "error", message: data.error || "Import failed" }); + setImportStatus({ type: "error", message: data.error || t("importFailed") }); } } catch { - setImportStatus({ type: "error", message: "An error occurred during import" }); + setImportStatus({ type: "error", message: t("errorDuringImport") }); } finally { setImportLoading(false); setPendingImportFile(null); @@ -210,12 +227,18 @@ export default function SystemStorageTab() { const then = new Date(isoString); const diffMs = (now as any) - (then as any); const diffMin = Math.floor(diffMs / 60000); - if (diffMin < 1) return "just now"; - if (diffMin < 60) return `${diffMin}m ago`; + if (diffMin < 1) return t("justNow"); + if (diffMin < 60) return t("minutesAgo", { count: diffMin }); const diffHr = Math.floor(diffMin / 60); - if (diffHr < 24) return `${diffHr}h ago`; + if (diffHr < 24) return t("hoursAgo", { count: diffHr }); const diffDays = Math.floor(diffHr / 24); - return `${diffDays}d ago`; + return t("daysAgo", { count: diffDays }); + }; + + const formatBackupReason = (reason) => { + if (reason === "manual") return t("backupReasonManual"); + if (reason === "pre-restore") return t("backupReasonPreRestore"); + return reason; }; return ( @@ -268,7 +291,7 @@ export default function SystemStorageTab() { setExportLoading(true); try { const res = await fetch("/api/db-backups/exportAll"); - if (!res.ok) throw new Error("Export failed"); + if (!res.ok) throw new Error(t("exportFailed")); const blob = await res.blob(); const cd = res.headers.get("Content-Disposition") || ""; const filenameMatch = cd.match(/filename="?([^"]+)"?/); @@ -284,7 +307,7 @@ export default function SystemStorageTab() { } catch (err) { setImportStatus({ type: "error", - message: `Full export failed: ${(err as Error).message}`, + message: t("fullExportFailedWithError", { error: (err as Error).message }), }); } finally { setExportLoading(false); @@ -325,9 +348,7 @@ export default function SystemStorageTab() {

{t("confirmDbImport")}

- This will replace all current data with the content from{" "} - {pendingImportFile.name}. A backup will be - created automatically before the import. + {t("confirmDbImportDesc", { file: pendingImportFile.name })}

@@ -485,7 +506,7 @@ export default function SystemStorageTab() { <>
- {backups.length} backup(s) available + {t("backupsAvailable", { count: backups.length })}
- {backup.connectionCount} connection(s) + {t("connectionsCount", { count: backup.connectionCount })} {formatBytes(backup.size)}
diff --git a/src/app/(dashboard)/dashboard/settings/components/ThinkingBudgetTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ThinkingBudgetTab.tsx index 47003edf74..94beef5048 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ThinkingBudgetTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ThinkingBudgetTab.tsx @@ -1,41 +1,41 @@ "use client"; import { useState, useEffect } from "react"; -import { Card, Button, Select } from "@/shared/components"; +import { Card } from "@/shared/components"; import { useTranslations } from "next-intl"; const MODES = [ { value: "passthrough", - label: "Passthrough", - desc: "No changes — client controls thinking budget", + labelKey: "passthrough", + descKey: "passthroughDesc", icon: "arrow_forward", }, { value: "auto", - label: "Auto", - desc: "Strip all thinking config — let provider decide", + labelKey: "auto", + descKey: "autoDesc", icon: "auto_awesome", }, { value: "custom", - label: "Custom", - desc: "Set a fixed token budget for all requests", + labelKey: "custom", + descKey: "customDesc", icon: "tune", }, { value: "adaptive", - label: "Adaptive", - desc: "Scale budget based on request complexity", + labelKey: "adaptive", + descKey: "adaptiveDesc", icon: "trending_up", }, ]; const EFFORTS = [ - { value: "none", label: "None (0 tokens)" }, - { value: "low", label: "Low (1K tokens)" }, - { value: "medium", label: "Medium (10K tokens)" }, - { value: "high", label: "High (128K tokens)" }, + { value: "none", labelKey: "effortNone" }, + { value: "low", labelKey: "effortLow" }, + { value: "medium", labelKey: "effortMedium" }, + { value: "high", labelKey: "effortHigh" }, ]; export default function ThinkingBudgetTab() { @@ -126,9 +126,9 @@ export default function ThinkingBudgetTab() {

- {m.label} + {t(m.labelKey)}

-

{m.desc}

+

{t(m.descKey)}

))} @@ -179,7 +179,7 @@ export default function ThinkingBudgetTab() { : "border-border/50 text-text-muted hover:border-border" }`} > - {e.value.charAt(0).toUpperCase() + e.value.slice(1)} + {t(e.labelKey)} ))}
diff --git a/src/app/(dashboard)/dashboard/settings/page.tsx b/src/app/(dashboard)/dashboard/settings/page.tsx index 0551c532cd..00b801d8f6 100644 --- a/src/app/(dashboard)/dashboard/settings/page.tsx +++ b/src/app/(dashboard)/dashboard/settings/page.tsx @@ -39,7 +39,7 @@ export default function SettingsPage() { {/* Tab navigation */}
{tabs.map((tab) => ( diff --git a/src/app/(dashboard)/dashboard/settings/pricing/page.tsx b/src/app/(dashboard)/dashboard/settings/pricing/page.tsx index 1159ac5429..b2c3acf263 100644 --- a/src/app/(dashboard)/dashboard/settings/pricing/page.tsx +++ b/src/app/(dashboard)/dashboard/settings/pricing/page.tsx @@ -101,19 +101,19 @@ export default function PricingSettingsPage() {

  • - Input: {t("inputTokenDesc")} + {t("input")}: {t("inputTokenDesc")}
  • - Output: {t("outputTokenDesc")} + {t("output")}: {t("outputTokenDesc")}
  • - Cached: {t("cachedTokenDesc")} + {t("cached")}: {t("cachedTokenDesc")}
  • - Reasoning: {t("reasoningTokenDesc")} + {t("reasoning")}: {t("reasoningTokenDesc")}
  • - Cache Creation: {t("cacheCreationTokenDesc")} + {t("cacheCreation")}: {t("cacheCreationTokenDesc")}

{t("customPricingNote")}

@@ -142,13 +142,13 @@ export default function PricingSettingsPage() {
{provider.toUpperCase()}:{" "} - {Object.keys(currentPricing[provider]).length} models + {Object.keys(currentPricing[provider]).length} {t("models")}
))} {Object.keys(currentPricing).length > 5 && (
- + {Object.keys(currentPricing).length - 5} more providers + + {t("moreProviders", { count: Object.keys(currentPricing).length - 5 })}
)}
diff --git a/src/app/(dashboard)/dashboard/translator/components/LiveMonitorMode.tsx b/src/app/(dashboard)/dashboard/translator/components/LiveMonitorMode.tsx index 93581602e2..4a2f0a630f 100644 --- a/src/app/(dashboard)/dashboard/translator/components/LiveMonitorMode.tsx +++ b/src/app/(dashboard)/dashboard/translator/components/LiveMonitorMode.tsx @@ -18,6 +18,8 @@ export default function LiveMonitorMode() { const [loading, setLoading] = useState(true); const [autoRefresh, setAutoRefresh] = useState(true); const intervalRef = useRef(null); + const notAvailable = t("notAvailableSymbol"); + const formatLatency = (value) => t("millisecondsShort", { value }); const fetchHistory = async () => { try { @@ -55,7 +57,10 @@ export default function LiveMonitorMode() {
{/* Info Banner */}
- +
@@ -79,7 +84,12 @@ export default function LiveMonitorMode() { /> - +
{/* Controls */} @@ -88,6 +98,7 @@ export default function LiveMonitorMode() {
@@ -102,7 +113,9 @@ export default function LiveMonitorMode() { onClick={fetchHistory} className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors" > - refresh + {tc("refresh")}
@@ -115,12 +128,17 @@ export default function LiveMonitorMode() { {loading ? (
- progress_activity + {tc("loading")}
) : events.length === 0 ? (
- +

{t("noTranslations")}

@@ -172,7 +190,9 @@ export default function LiveMonitorMode() { className="border-b border-border/50 hover:bg-bg-subtle/50 transition-colors" > - {event.timestamp ? new Date(event.timestamp).toLocaleTimeString() : "—"} + {event.timestamp + ? new Date(event.timestamp).toLocaleTimeString() + : notAvailable} @@ -180,7 +200,10 @@ export default function LiveMonitorMode() { - + @@ -190,7 +213,7 @@ export default function LiveMonitorMode() { - {event.model || "—"} + {event.model || notAvailable} {event.status === "success" ? ( @@ -204,7 +227,7 @@ export default function LiveMonitorMode() { )} - {event.latency ? `${event.latency}ms` : "—"} + {event.latency ? formatLatency(event.latency) : notAvailable} ); @@ -224,7 +247,12 @@ function StatCard({ icon, label, value, color }) {
- {icon} +

{value}

diff --git a/src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx b/src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx index a527c95a73..33eff28b6a 100644 --- a/src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; /** * BudgetTab — Batch C @@ -14,7 +14,7 @@ import { useState, useEffect, useCallback } from "react"; import { Card, Button, Input, EmptyState } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; -function ProgressBar({ value, max, warningAt = 0.8 }) { +function ProgressBar({ value, max, warningAt = 0.8, formatCurrency }) { const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0; const ratio = max > 0 ? value / max : 0; const color = ratio >= 1 ? "#ef4444" : ratio >= warningAt ? "#f59e0b" : "#22c55e"; @@ -22,8 +22,8 @@ function ProgressBar({ value, max, warningAt = 0.8 }) { return (
- ${value.toFixed(2)} - ${max.toFixed(2)} + {formatCurrency(value)} + {formatCurrency(max)}
+ new Intl.NumberFormat(locale, { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(Number(value || 0)); // Load API keys useEffect(() => { @@ -115,7 +123,9 @@ export default function BudgetTab() { if (loading) { return (
- account_balance_wallet + {t("loadingBudgetData")}
); @@ -143,7 +153,9 @@ export default function BudgetTab() {
- account_balance_wallet +

{t("budgetManagement")}

@@ -167,16 +179,26 @@ export default function BudgetTab() {

{t("todaysSpend")}

-

${dailyCost.toFixed(2)}

+

{formatCurrency(dailyCost)}

{dailyLimit > 0 && ( - + )}

{t("thisMonth")}

-

${monthlyCost.toFixed(2)}

+

{formatCurrency(monthlyCost)}

{monthlyLimit > 0 && ( - + )}
@@ -225,13 +247,14 @@ export default function BudgetTab() {
{budget.budgetCheck.allowed - ? t("budgetOk", { remaining: (budget.budgetCheck.remaining || 0).toFixed(2) }) + ? t("budgetOk", { remaining: formatCurrency(budget.budgetCheck.remaining || 0) }) : t("budgetExceeded")}
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx index b60624117c..f6bde654fe 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import Image from "next/image"; +import { useTranslations } from "next-intl"; import Card from "@/shared/components/Card"; import Badge from "@/shared/components/Badge"; import QuotaProgressBar from "./QuotaProgressBar"; @@ -26,6 +27,7 @@ export default function ProviderLimitCard({ }) { const [refreshing, setRefreshing] = useState(false); const [imgError, setImgError] = useState(false); + const t = useTranslations("usage"); const handleRefresh = async () => { if (!onRefresh || refreshing) return; @@ -70,7 +72,7 @@ export default function ProviderLimitCard({ ) : ( {provider

{name || provider}

{plan && ( - + {plan} )} @@ -95,7 +100,7 @@ export default function ProviderLimitCard({ onClick={handleRefresh} disabled={refreshing || loading} className="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - title="Refresh quota" + title={t("refreshQuota")} > data_usage -

No quota data available

+

{t("noQuotaDataAvailable")}

)} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.tsx index 056adfbb4d..eab32f59ac 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.tsx @@ -1,11 +1,12 @@ "use client"; import { formatResetTime, calculatePercentage } from "./utils"; +import { useLocale, useTranslations } from "next-intl"; /** * Format reset time display (Today, 12:00 PM) */ -function formatResetTimeDisplay(resetTime) { +function formatResetTimeDisplay(resetTime, locale, t) { if (!resetTime) return null; try { @@ -17,20 +18,19 @@ function formatResetTimeDisplay(resetTime) { let dayStr = ""; if (date >= today && date < tomorrow) { - dayStr = "Today"; + dayStr = t("today"); } else if (date >= tomorrow && date < new Date(tomorrow.getTime() + 24 * 60 * 60 * 1000)) { - dayStr = "Tomorrow"; + dayStr = t("tomorrow"); } else { - dayStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); + dayStr = date.toLocaleDateString(locale, { month: "short", day: "numeric" }); } - const timeStr = date.toLocaleTimeString("en-US", { + const timeStr = date.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit", - hour12: true, }); - return `${dayStr}, ${timeStr}`; + return t("dayTimeFormat", { day: dayStr, time: timeStr }); } catch { return null; } @@ -71,6 +71,9 @@ function getColorClasses(remainingPercentage) { * Quota Table Component - Table-based display for quota data */ export default function QuotaTable({ quotas = [] }) { + const t = useTranslations("usage"); + const locale = useLocale(); + if (!quotas || quotas.length === 0) { return null; } @@ -92,7 +95,7 @@ export default function QuotaTable({ quotas = [] }) { const colors = getColorClasses(remaining); const countdown = formatResetTime(quota.resetAt); - const resetDisplay = formatResetTimeDisplay(quota.resetAt); + const resetDisplay = formatResetTimeDisplay(quota.resetAt, locale, t); return ( - {countdown !== "-" || resetDisplay ? ( + {countdown !== t("notAvailableSymbol") || resetDisplay ? (
- {countdown !== "-" && ( -
in {countdown}
+ {countdown !== t("notAvailableSymbol") && ( +
+ {t("inDuration", { duration: countdown })} +
)} {resetDisplay && (
{resetDisplay}
)}
) : ( -
N/A
+
{t("notApplicable")}
)} diff --git a/src/app/(dashboard)/dashboard/usage/components/RateLimitStatus.tsx b/src/app/(dashboard)/dashboard/usage/components/RateLimitStatus.tsx index 5484d5e102..36bcf5fe9b 100644 --- a/src/app/(dashboard)/dashboard/usage/components/RateLimitStatus.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/RateLimitStatus.tsx @@ -56,7 +56,10 @@ export default function RateLimitStatus() { {data.lockouts.length === 0 ? (
- +

{t("noLockouts")}

@@ -70,7 +73,10 @@ export default function RateLimitStatus() { bg-orange-500/5 border border-orange-500/15" >
- +
diff --git a/src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx b/src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx index 24b781dd66..8e8a8e84d8 100644 --- a/src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx @@ -54,7 +54,10 @@ export default function SessionsTab() { {data.sessions.length === 0 ? (
- +

{t("noSessions")}

diff --git a/src/app/forgot-password/page.tsx b/src/app/forgot-password/page.tsx index d8588918f6..1d7984d6ef 100644 --- a/src/app/forgot-password/page.tsx +++ b/src/app/forgot-password/page.tsx @@ -27,7 +27,9 @@ export default function ForgotPasswordPage() {
- terminal +

{t("methodCliTitle")}

@@ -44,7 +46,9 @@ export default function ForgotPasswordPage() {
- database +

{t("methodManualTitle")}

@@ -77,7 +81,9 @@ export default function ForgotPasswordPage() { href="/login" className="text-sm text-primary hover:underline inline-flex items-center gap-1" > - arrow_back + {t("backToLogin")}
diff --git a/src/app/landing/components/Features.tsx b/src/app/landing/components/Features.tsx index d4015c209f..b3d291b8fc 100644 --- a/src/app/landing/components/Features.tsx +++ b/src/app/landing/components/Features.tsx @@ -1,10 +1,11 @@ "use client"; +import { useTranslations } from "next-intl"; const FEATURES = [ { icon: "link", - title: "Unified Endpoint", - desc: "Access all providers via a single standard API URL.", + titleKey: "featureUnifiedEndpointTitle", + descKey: "featureUnifiedEndpointDesc", colors: { border: "hover:border-blue-500/50", bg: "hover:bg-blue-500/5", @@ -15,8 +16,8 @@ const FEATURES = [ }, { icon: "bolt", - title: "Easy Setup", - desc: "Get up and running in minutes with npx command.", + titleKey: "featureEasySetupTitle", + descKey: "featureEasySetupDesc", colors: { border: "hover:border-orange-500/50", bg: "hover:bg-orange-500/5", @@ -27,8 +28,8 @@ const FEATURES = [ }, { icon: "shield_with_heart", - title: "Model Fallback", - desc: "Automatically switch providers on failure or high latency.", + titleKey: "featureModelFallbackTitle", + descKey: "featureModelFallbackDesc", colors: { border: "hover:border-rose-500/50", bg: "hover:bg-rose-500/5", @@ -39,8 +40,8 @@ const FEATURES = [ }, { icon: "monitoring", - title: "Usage Tracking", - desc: "Detailed analytics and cost monitoring across all models.", + titleKey: "featureUsageTrackingTitle", + descKey: "featureUsageTrackingDesc", colors: { border: "hover:border-purple-500/50", bg: "hover:bg-purple-500/5", @@ -51,8 +52,8 @@ const FEATURES = [ }, { icon: "key", - title: "OAuth & API Keys", - desc: "Securely manage credentials in one vault.", + titleKey: "featureOAuthApiKeysTitle", + descKey: "featureOAuthApiKeysDesc", colors: { border: "hover:border-amber-500/50", bg: "hover:bg-amber-500/5", @@ -63,8 +64,8 @@ const FEATURES = [ }, { icon: "cloud_sync", - title: "Cloud Sync", - desc: "Sync your configurations across devices instantly.", + titleKey: "featureCloudSyncTitle", + descKey: "featureCloudSyncDesc", colors: { border: "hover:border-sky-500/50", bg: "hover:bg-sky-500/5", @@ -75,8 +76,8 @@ const FEATURES = [ }, { icon: "terminal", - title: "CLI Support", - desc: "Works with Claude Code, Codex, Cline, Cursor, and more.", + titleKey: "featureCliSupportTitle", + descKey: "featureCliSupportDesc", colors: { border: "hover:border-emerald-500/50", bg: "hover:bg-emerald-500/5", @@ -87,8 +88,8 @@ const FEATURES = [ }, { icon: "dashboard", - title: "Dashboard", - desc: "Visual dashboard for real-time traffic analysis.", + titleKey: "featureDashboardTitle", + descKey: "featureDashboardDesc", colors: { border: "hover:border-fuchsia-500/50", bg: "hover:bg-fuchsia-500/5", @@ -100,33 +101,35 @@ const FEATURES = [ ]; export default function Features() { + const t = useTranslations("landing"); + return (
-

Powerful Features

-

- Everything you need to manage your AI infrastructure in one place, built for scale. -

+

{t("powerfulFeatures")}

+

{t("featuresSubtitle")}

{FEATURES.map((feature) => (
- {feature.icon} +

- {feature.title} + {t(feature.titleKey)}

-

{feature.desc}

+

{t(feature.descKey)}

))}
diff --git a/src/app/landing/components/FlowAnimation.tsx b/src/app/landing/components/FlowAnimation.tsx index a5b501c6b9..193d7f9d93 100644 --- a/src/app/landing/components/FlowAnimation.tsx +++ b/src/app/landing/components/FlowAnimation.tsx @@ -1,149 +1,177 @@ "use client"; import { useEffect, useState } from "react"; import Image from "next/image"; - -const CLI_TOOLS = [ - { id: "claude", name: "Claude Code", image: "/providers/claude.png" }, - { id: "codex", name: "OpenAI Codex", image: "/providers/codex.png" }, - { id: "cline", name: "Cline", image: "/providers/cline.png" }, - { id: "cursor", name: "Cursor", image: "/providers/cursor.png" }, -]; - -const PROVIDERS = [ - { id: "openai", name: "OpenAI", color: "bg-emerald-500", textColor: "text-white" }, - { id: "anthropic", name: "Anthropic", color: "bg-orange-400", textColor: "text-white" }, - { id: "gemini", name: "Gemini", color: "bg-blue-500", textColor: "text-white" }, - { id: "github", name: "GitHub Copilot", color: "bg-gray-700", textColor: "text-white" }, -]; +import { useTranslations } from "next-intl"; export default function FlowAnimation() { + const t = useTranslations("landing"); const [activeFlow, setActiveFlow] = useState(0); + const cliTools = [ + { id: "claude", name: t("flowToolClaudeCode"), image: "/providers/claude.png" }, + { id: "codex", name: t("flowToolOpenAICodex"), image: "/providers/codex.png" }, + { id: "cline", name: t("flowToolCline"), image: "/providers/cline.png" }, + { id: "cursor", name: t("flowToolCursor"), image: "/providers/cursor.png" }, + ]; + + const providers = [ + { + id: "openai", + name: t("flowProviderOpenAI"), + color: "bg-emerald-500", + textColor: "text-white", + }, + { + id: "anthropic", + name: t("flowProviderAnthropic"), + color: "bg-orange-400", + textColor: "text-white", + }, + { + id: "gemini", + name: t("flowProviderGemini"), + color: "bg-blue-500", + textColor: "text-white", + }, + { + id: "github", + name: t("flowProviderGithubCopilot"), + color: "bg-gray-700", + textColor: "text-white", + }, + ]; + useEffect(() => { const interval = setInterval(() => { - setActiveFlow((prev) => (prev + 1) % PROVIDERS.length); + setActiveFlow((prev) => (prev + 1) % providers.length); }, 2000); return () => clearInterval(interval); - }, []); + }, [providers.length]); return ( -
- {/* OmniRoute Hub - Center */} -
- hub - OmniRoute -
-
+
+
+ {/* OmniRoute Hub - Center */} +
+ + + {t("brandName")} + +
+
- {/* CLI Tools - Left side */} -
- {CLI_TOOLS.map((tool) => ( -
-
- {tool.name} + {/* CLI Tools - Left side */} +
+ {cliTools.map((tool) => ( +
+
+ {tool.name} +
-
- ))} -
+ ))} +
- {/* SVG Lines from CLI to OmniRoute */} - - - - - - + {/* SVG Lines from CLI to OmniRoute */} + + + + + + - {/* SVG Lines from OmniRoute to Providers */} - - - - - - + {/* SVG Lines from OmniRoute to Providers */} + + + + + + - {/* AI Providers - Right side */} -
- {PROVIDERS.map((provider, idx) => ( -
- {provider.name} -
- ))} + {/* AI Providers - Right side */} +
+ {providers.map((provider, idx) => ( +
+ {provider.name} +
+ ))} +
{/* Mobile fallback */}
-

Interactive diagram visible on desktop

+

{t("interactiveDiagram")}

); diff --git a/src/app/landing/components/Footer.tsx b/src/app/landing/components/Footer.tsx index cbab87a89a..334eee56d1 100644 --- a/src/app/landing/components/Footer.tsx +++ b/src/app/landing/components/Footer.tsx @@ -1,7 +1,11 @@ "use client"; +import { useTranslations } from "next-intl"; import OmniRouteLogo from "@/shared/components/OmniRouteLogo"; export default function Footer() { + const t = useTranslations("landing"); + const year = new Date().getFullYear(); + return (
@@ -12,12 +16,9 @@ export default function Footer() {
-

OmniRoute

+

{t("brandName")}

-

- The unified endpoint for AI generation. Connect, route, and manage your AI providers - with ease. -

+

{t("footerTagline")}

{/* Product */} {/* Resources */} {/* Legal */}
{/* Bottom */}
-

© 2025 OmniRoute. All rights reserved.

+

{t("copyright", { year })}

diff --git a/src/app/landing/components/GetStarted.tsx b/src/app/landing/components/GetStarted.tsx index 1c48704e5e..20356d0aac 100644 --- a/src/app/landing/components/GetStarted.tsx +++ b/src/app/landing/components/GetStarted.tsx @@ -1,10 +1,16 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; export default function GetStarted() { + const t = useTranslations("landing"); const [copied, setCopied] = useState(false); - const handleCopy = (text) => { + const endpoint = "http://localhost:20128"; + const dashboardUrl = `${endpoint}/dashboard`; + const command = "npx omniroute"; + + const handleCopy = (text: string) => { navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -16,11 +22,8 @@ export default function GetStarted() {
{/* Left: Steps */}
-

Get Started in 30 Seconds

-

- Install OmniRoute, configure your providers via web dashboard, and start routing AI - requests. -

+

{t("getStartedIn30Seconds")}

+

{t("getStartedDescription")}

@@ -28,10 +31,8 @@ export default function GetStarted() { 1
-

Install OmniRoute

-

- Run npx command to start the server instantly -

+

{t("installOmniRoute")}

+

{t("installStepDescription")}

@@ -40,10 +41,8 @@ export default function GetStarted() { 2
-

Open Dashboard

-

- Configure providers and API keys via web interface -

+

{t("openDashboard")}

+

{t("openDashboardStepDescription")}

@@ -52,9 +51,9 @@ export default function GetStarted() { 3
-

Route Requests

+

{t("routeRequests")}

- Point your CLI tools to http://localhost:20128 + {t("routeRequestsStepDescription", { endpoint })}

@@ -69,44 +68,46 @@ export default function GetStarted() {
-
terminal
+
{t("terminal")}
{/* Terminal content */}
handleCopy("npx omniroute")} + onClick={() => handleCopy(command)} > $ - npx omniroute + {command} - {copied ? "✓ Copied" : "Copy"} + {copied ? t("copied") : t("copy")}
- > Starting OmniRoute... + > {t("startingOmniRoute")}
- > Server running on{" "} - http://localhost:20128 + > {t("serverRunningOnLabel")}{" "} + {endpoint}
- > Dashboard:{" "} - http://localhost:20128/dashboard + > {t("dashboardLabel")}:{" "} + {dashboardUrl}
- > Ready to route! ✓ + > {t("readyToRoute")}
- 📝 Configure providers in dashboard or use environment variables + {t("configureProvidersNote")}
- Data Location: + {t("dataLocation")}
- macOS/Linux: ~/.omniroute/db.json + {t("dataLocationMacLinux")}{" "} + ~/.omniroute/db.json
- Windows: %APPDATA%/omniroute/db.json + {t("dataLocationWindows")}{" "} + %APPDATA%/omniroute/db.json
diff --git a/src/app/landing/components/HeroSection.tsx b/src/app/landing/components/HeroSection.tsx index 582999ba0b..6a50d3358e 100644 --- a/src/app/landing/components/HeroSection.tsx +++ b/src/app/landing/components/HeroSection.tsx @@ -1,6 +1,11 @@ "use client"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; export default function HeroSection() { + const t = useTranslations("landing"); + const router = useRouter(); + return (
{/* Glow effect */} @@ -10,26 +15,30 @@ export default function HeroSection() { {/* Version badge */}
- v1.0 is now live + {t("versionLive")}
{/* Main heading */}

- One Endpoint for
- All AI Providers + {t("oneEndpoint")}
+ {t("allProviders")}

{/* Description */}

- AI endpoint proxy with web dashboard - A JavaScript port of CLIProxyAPI. Works seamlessly - with Claude Code, OpenAI Codex, Cline, RooCode, and other CLI tools. + {t("heroDescription")}

{/* CTA Buttons */}
- - code - View on GitHub + + {t("viewOnGithub")}
diff --git a/src/app/landing/components/HowItWorks.tsx b/src/app/landing/components/HowItWorks.tsx index 7f6a6a5bd7..aff42fc238 100644 --- a/src/app/landing/components/HowItWorks.tsx +++ b/src/app/landing/components/HowItWorks.tsx @@ -1,15 +1,15 @@ "use client"; +import { useTranslations } from "next-intl"; export default function HowItWorks() { + const t = useTranslations("landing"); + return (
-

How OmniRoute Works

-

- Data flows seamlessly from your application through our intelligent routing layer to the - best provider for the job. -

+

{t("howItWorks")}

+

{t("howItWorksDescription")}

@@ -19,30 +19,29 @@ export default function HowItWorks() { {/* Step 1: CLI & SDKs */}
- terminal +
-

1. CLI & SDKs

-

- Your requests start from your favorite tools or our unified SDK. Just change the - base URL. -

+

{t("howItWorksStep1Title")}

+

{t("howItWorksStep1Description")}

{/* Step 2: OmniRoute Hub */}
- +
-

2. OmniRoute Hub

-

- Our engine analyzes the prompt, checks provider health, and routes for lowest - latency or cost. -

+

{t("howItWorksStep2Title")}

+

{t("howItWorksStep2Description")}

@@ -57,10 +56,8 @@ export default function HowItWorks() {
-

3. AI Providers

-

- The request is fulfilled by OpenAI, Anthropic, Gemini, or others instantly. -

+

{t("howItWorksStep3Title")}

+

{t("howItWorksStep3Description")}

diff --git a/src/app/landing/components/Navigation.tsx b/src/app/landing/components/Navigation.tsx index 4c463eb291..07da785878 100644 --- a/src/app/landing/components/Navigation.tsx +++ b/src/app/landing/components/Navigation.tsx @@ -1,9 +1,11 @@ "use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import OmniRouteLogo from "@/shared/components/OmniRouteLogo"; export default function Navigation() { + const t = useTranslations("landing"); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const router = useRouter(); @@ -15,12 +17,12 @@ export default function Navigation() { type="button" className="flex items-center gap-3 cursor-pointer bg-transparent border-none p-0" onClick={() => router.push("/")} - aria-label="Navigate to home" + aria-label={t("navigateHome")} >
-

OmniRoute

+

{t("brandName")}

{/* Desktop menu */} @@ -29,19 +31,19 @@ export default function Navigation() { className="text-gray-300 hover:text-white text-sm font-medium transition-colors" href="#features" > - Features + {t("featuresLink")} - How it Works + {t("howItWorks")} - Docs + {t("docsLink")} - GitHub open_in_new + {t("github")} +
@@ -59,13 +64,16 @@ export default function Navigation() { onClick={() => router.push("/dashboard")} className="hidden sm:flex h-9 items-center justify-center rounded-lg px-4 bg-[#E54D5E] hover:bg-[#C93D4E] transition-all text-white text-sm font-bold shadow-[0_0_15px_rgba(229,77,94,0.4)] hover:shadow-[0_0_20px_rgba(229,77,94,0.6)]" > - Get Started + {t("getStarted")}
@@ -79,20 +87,20 @@ export default function Navigation() { href="#features" onClick={() => setMobileMenuOpen(false)} > - Features + {t("featuresLink")} setMobileMenuOpen(false)} > - How it Works + {t("howItWorks")} - Docs + {t("docsLink")} - GitHub + {t("github")}
diff --git a/src/app/landing/page.tsx b/src/app/landing/page.tsx index 8cafb3862d..928ecf52f4 100644 --- a/src/app/landing/page.tsx +++ b/src/app/landing/page.tsx @@ -1,5 +1,6 @@ "use client"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import Navigation from "./components/Navigation"; import HeroSection from "./components/HeroSection"; import FlowAnimation from "./components/FlowAnimation"; @@ -9,6 +10,7 @@ import GetStarted from "./components/GetStarted"; import Footer from "./components/Footer"; export default function LandingPage() { + const t = useTranslations("landing"); const router = useRouter(); return (
@@ -64,25 +66,20 @@ export default function LandingPage() {
-

- Ready to Simplify Your AI Infrastructure? -

-

- Join developers who are streamlining their AI integrations with OmniRoute. Open - source and free to start. -

+

{t("ctaTitle")}

+

{t("ctaDescription")}

diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 6909904b7e..3d46975a0c 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -321,6 +321,9 @@ "concurrencyPerModel": "Concurrency / Model", "queueTimeout": "Queue Timeout (ms)", "advancedHint": "Leave empty to use global defaults. These override per-provider settings.", + "moveUp": "Move up", + "moveDown": "Move down", + "removeModel": "Remove", "saving": "Saving...", "weighted": "Weighted", "leastUsed": "Least-Used", @@ -524,11 +527,18 @@ "newAccount": "New Account", "deleteConfirm": "Are you sure you want to delete this provider?", "testing": "Testing...", + "testConnection": "Test Connection", "testSuccess": "Connection successful", "testFailed": "Connection failed", "available": "Available", + "cooldown": "Cooldown", "unavailable": "Unavailable", "unknown": "Unknown", + "oauthLabel": "OAuth", + "compatibleLabel": "Compatible", + "chat": "Chat", + "responses": "Responses", + "messages": "Messages", "oauthProviders": "OAuth Providers", "freeProviders": "Free Providers", "apiKeyProviders": "API Key Providers", @@ -553,14 +563,17 @@ "addNewProvider": "Add New Provider", "backToProviders": "Back to Providers", "configureNewProvider": "Configure a new AI provider to use with your applications.", + "providerLabel": "Provider", "selectProvider": "Select a provider", "selectedProvider": "Selected provider", "authMethod": "Authentication Method", + "apiKeyLabel": "API Key", "apiKeyRequired": "API Key is required", "selectProviderRequired": "Please select a provider", "enterApiKey": "Enter your API key", "apiKeySecure": "Your API key will be encrypted and stored securely.", "oauth2Connect": "Connect with OAuth2", + "oauth2Label": "OAuth2", "oauth2Desc": "Connect your account using OAuth2 authentication.", "displayName": "Display Name", "displayNamePlaceholder": "e.g., Production API, Dev Environment", @@ -580,7 +593,16 @@ "loadingAvailability": "Loading model availability...", "clearCooldown": "Clear", "clearing": "Clearing...", + "until": "Until {time}", "providerTestFailed": "Provider test failed", + "modeTest": "{mode} Test", + "passedCount": "{count} passed", + "failedCount": "{count} failed", + "testedCount": "{count} tested", + "millisecondsAbbr": "{value}ms", + "okShort": "OK", + "errorShort": "ERROR", + "noActiveConnectionsInGroup": "No active connections found for this group.", "allTestsPassed": "All {total} tests passed", "testSummary": "{passed}/{total} passed, {failed} failed", "nameLabel": "Name", @@ -590,6 +612,10 @@ "prefixHint": "Required. Unique prefix for model names.", "nameHint": "Required. A friendly label for this node.", "baseUrlHint": "Required. \u2003Provider API base URL.", + "anthropicPrefixPlaceholder": "ac-prod", + "openaiPrefixPlaceholder": "oc-prod", + "anthropicBaseUrlPlaceholder": "https://api.anthropic.com/v1", + "openaiBaseUrlPlaceholder": "https://api.openai.com/v1", "validateConnection": "Validate Connection", "validating": "Validating...", "connectionValid": "Connection is valid!", @@ -723,6 +749,7 @@ "check": "Check", "valid": "Valid", "invalid": "Invalid", + "creating": "Creating...", "validationChecksAnthropicCompatible": "Validation checks {provider} by verifying the API key.", "validationChecksOpenAiCompatible": "Validation checks {provider} via /models on your base URL.", "priorityLabel": "Priority", @@ -808,7 +835,12 @@ "ai": "AI", "advanced": "Advanced", "localMode": "Local Mode — All data stored on your machine", + "settingsSectionsAria": "Settings sections", "switchThemes": "Switch between light and dark themes", + "themeSelectionAria": "Theme selection", + "themeLight": "Light", + "themeDark": "Dark", + "themeSystem": "System", "hideHealthLogs": "Hide Health Check Logs", "hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console", "promptCache": "Prompt Cache", @@ -821,6 +853,7 @@ "globalProxy": "Global Proxy", "globalProxyDesc": "Configure a global outbound proxy for all API calls. Individual providers, combos, and keys can override this.", "noGlobalProxy": "No global proxy configured", + "globalLabel": "Global", "configure": "Configure", "globalSystemPrompt": "Global System Prompt", "systemPromptDesc": "Injected into all requests at proxy level", @@ -838,6 +871,10 @@ "customDesc": "Set a fixed token budget for all requests", "adaptive": "Adaptive", "adaptiveDesc": "Scale budget based on request complexity", + "effortNone": "None (0 tokens)", + "effortLow": "Low (1K tokens)", + "effortMedium": "Medium (10K tokens)", + "effortHigh": "High (128K tokens)", "tokenBudget": "Token Budget", "tokens": "tokens", "baseEffortLevel": "Base Effort Level", @@ -862,6 +899,8 @@ "blockedProviders": "Blocked Providers", "blockedProvidersDesc": "Hide specific providers from the /v1/models response. Blocked providers will not appear in model listings.", "providersBlocked": "{count} provider(s) blocked from /models", + "blockProviderTitle": "Block {provider}", + "unblockProviderTitle": "Unblock {provider}", "routingStrategy": "Routing Strategy", "fillFirst": "Fill First", "fillFirstDesc": "Use accounts in priority order", @@ -879,10 +918,13 @@ "stickyLimitDesc": "Calls per account before switching", "modelAliases": "Model Aliases", "modelAliasesDesc": "Wildcard patterns to remap model names • Use * and ?", + "aliasPatternPlaceholder": "claude-sonnet-*", + "aliasTargetPlaceholder": "claude-sonnet-4-20250514", "pattern": "Pattern", "targetModel": "Target Model", "add": "+ Add", "session": "Session", + "sessionDetailsAria": "Session details", "status": "Status", "authenticated": "Authenticated", "guest": "Guest", @@ -892,9 +934,16 @@ "clearLocalData": "Clear Local Data", "logout": "Logout", "clearLocalDataConfirm": "Clear all local data? This will reset your preferences.", + "unknown": "Unknown", + "systemActor": "system", "ipAccessControl": "IP Access Control", "ipAccessControlDesc": "Block or allow specific IP addresses", + "ipModeDisabled": "Disabled", + "ipModeBlacklist": "Blacklist", + "ipModeWhitelist": "Whitelist", + "ipModeWhitelistPriority": "WL Priority", "addIpAddress": "Add IP Address", + "ipAddressPlaceholder": "192.168.1.0/24 or 10.0.*.*", "block": "+ Block", "allow": "+ Allow", "blocked": "Blocked ({count})", @@ -913,7 +962,9 @@ "fallbackChainsDesc": "Define provider fallback order per model", "addChain": "+ Add Chain", "modelName": "Model Name", + "modelNamePlaceholder": "claude-sonnet-4-20250514", "providersCommaSeparated": "Providers (comma-separated, in priority order)", + "providersCommaSeparatedPlaceholder": "anthropic, openai, gemini", "createChain": "Create Chain", "noFallbackChains": "No Fallback Chains", "noFallbackChainsDesc": "Create a chain to define provider fallback order for a model.", @@ -923,20 +974,30 @@ "chainDeleted": "Chain deleted for {model}", "failedCreateChain": "Failed to create chain", "failedDeleteChain": "Failed to delete chain", + "deleteChain": "Delete chain", "fillModelAndProviders": "Please fill model name and providers", "addAtLeastOneProvider": "Add at least one provider", "comboDefaultsTitle": "Combo Defaults", "globalComboConfig": "Global combo configuration", "defaultStrategy": "Default Strategy", "defaultStrategyDesc": "Applied to new combos without explicit strategy", + "comboStrategyAria": "Combo strategy", "priority": "Priority", "weighted": "Weighted", + "maxRetriesLabel": "Max Retries", + "retryDelayLabel": "Retry Delay (ms)", + "timeoutLabel": "Timeout (ms)", "healthCheck": "Health Check", "healthCheckDesc": "Pre-check provider availability", "trackMetrics": "Track Metrics", "trackMetricsDesc": "Record per-combo request metrics", "providerOverrides": "Provider Overrides", "providerOverridesDesc": "Override timeout and retries per provider. Provider settings override global defaults.", + "providerMaxRetriesAria": "{provider} max retries", + "providerTimeoutAria": "{provider} timeout ms", + "removeProviderOverrideAria": "Remove {provider} override", + "newProviderNamePlaceholder": "e.g. google, openai...", + "newProviderNameAria": "New provider name", "retries": "retries", "ms": "ms", "saveComboDefaults": "Save Combo Defaults", @@ -964,6 +1025,9 @@ "running": "Running", "queued": "Queued", "circuitBreakers": "Circuit Breakers", + "breakerStateClosed": "Closed", + "breakerStateOpen": "Open", + "breakerStateHalfOpen": "Half-Open", "tripped": "{count} tripped", "healthy": "{count} healthy", "resetAll": "Reset All", @@ -973,9 +1037,15 @@ "allOperational": "All systems operational — no lockouts or tripped breakers", "loadingPolicies": "Loading policies...", "lockedIdentifiers": "Locked Identifiers", + "unlockedIdentifier": "Unlocked: {identifier}", + "sinceDate": "since {date}", "forceUnlock": "Force Unlock", "unlocking": "Unlocking...", "failedUnlock": "Failed to unlock", + "failedLoadWithStatus": "Failed to load: {status}", + "failedLoadResilience": "Failed to load resilience status", + "saveFailed": "Save failed", + "resetFailed": "Reset failed", "loadingResilience": "Loading resilience status...", "retry": "Retry", "systemStorage": "System & Storage", @@ -1003,6 +1073,19 @@ "no": "No", "restore": "Restore", "invalidFileType": "Invalid file type. Only .sqlite files are accepted.", + "exportFailed": "Export failed", + "exportFailedWithError": "Export failed: {error}", + "fullExportFailedWithError": "Full export failed: {error}", + "backupCreated": "Backup created: {file}", + "restoreSuccess": "Restored! {connections} connections, {nodes} nodes, {combos} combos, {apiKeys} API keys.", + "importSuccess": "Database imported! {connections} connections, {nodes} nodes, {combos} combos, {apiKeys} API keys.", + "justNow": "just now", + "minutesAgo": "{count}m ago", + "hoursAgo": "{count}h ago", + "daysAgo": "{count}d ago", + "backupReasonManual": "manual", + "backupReasonPreRestore": "pre-restore", + "connectionsCount": "{count, plural, one {# connection} other {# connections}}", "noChangesSinceBackup": "No changes since last backup", "backupFailed": "Backup failed", "restoreFailed": "Restore failed", @@ -1025,6 +1108,7 @@ "saving": "Saving...", "model": "Model", "models": "models", + "moreProviders": "{count} more providers", "withPricing": "with pricing configured", "policiesCircuitBreakers": "Policies & Circuit Breakers", "activeIssuesDetected": "Active issues detected", @@ -1080,6 +1164,8 @@ "successful": "Successful", "errors": "Errors", "avgLatency": "Avg Latency", + "millisecondsShort": "{value}ms", + "notAvailableSymbol": "—", "liveAutoRefreshing": "Live — Auto-refreshing", "paused": "Paused", "eventsAppearHint": "Translation events appear here as requests flow through OmniRoute. Use any of these methods to generate events:", @@ -1179,7 +1265,7 @@ "monthlyLimitPlaceholder": "e.g. 50.00", "warningThresholdPlaceholder": "80", "saveLimits": "Save Limits", - "budgetOk": "Budget OK — ${remaining} remaining", + "budgetOk": "Budget OK — {remaining} remaining", "budgetExceeded": "Budget exceeded — requests may be blocked", "totalRequests": "Total requests", "noDataYet": "No data yet", @@ -1279,6 +1365,11 @@ "lastUsed": "Last Used", "actions": "Actions", "refreshQuota": "Refresh quota", + "today": "Today", + "tomorrow": "Tomorrow", + "dayTimeFormat": "{day}, {time}", + "inDuration": "in {duration}", + "notApplicable": "N/A", "rawPlanWithValue": "Raw plan: {plan}", "noPlanFromProvider": "No plan from provider", "noQuotaData": "No quota data", @@ -1390,19 +1481,86 @@ "forgotPassword": "Forgot password?" }, "landing": { + "brandName": "OmniRoute", + "navigateHome": "Navigate to home", + "toggleMenu": "Toggle menu", + "featuresLink": "Features", + "docsLink": "Docs", + "github": "GitHub", + "versionLive": "v1.0 is now live", + "oneEndpoint": "One Endpoint for", "allProviders": "All AI Providers", - "oneEndpoint": "One Endpoint", - "powerfulFeatures": "Powerful Features", - "howItWorks": "How OmniRoute Works", - "installOmniRoute": "Install OmniRoute", - "openDashboard": "Open Dashboard", - "routeRequests": "Route Requests", - "dataLocation": "Data Location:", + "heroDescription": "AI endpoint proxy with web dashboard - A JavaScript port of CLIProxyAPI. Works seamlessly with Claude Code, OpenAI Codex, Cline, RooCode, and other CLI tools.", "getStarted": "Get Started", + "viewOnGithub": "View on GitHub", + "powerfulFeatures": "Powerful Features", + "featuresSubtitle": "Everything you need to manage your AI infrastructure in one place, built for scale.", + "featureUnifiedEndpointTitle": "Unified Endpoint", + "featureUnifiedEndpointDesc": "Access all providers via a single standard API URL.", + "featureEasySetupTitle": "Easy Setup", + "featureEasySetupDesc": "Get up and running in minutes with npx command.", + "featureModelFallbackTitle": "Model Fallback", + "featureModelFallbackDesc": "Automatically switch providers on failure or high latency.", + "featureUsageTrackingTitle": "Usage Tracking", + "featureUsageTrackingDesc": "Detailed analytics and cost monitoring across all models.", + "featureOAuthApiKeysTitle": "OAuth & API Keys", + "featureOAuthApiKeysDesc": "Securely manage credentials in one vault.", + "featureCloudSyncTitle": "Cloud Sync", + "featureCloudSyncDesc": "Sync your configurations across devices instantly.", + "featureCliSupportTitle": "CLI Support", + "featureCliSupportDesc": "Works with Claude Code, Codex, Cline, Cursor, and more.", + "featureDashboardTitle": "Dashboard", + "featureDashboardDesc": "Visual dashboard for real-time traffic analysis.", + "howItWorks": "How OmniRoute Works", + "howItWorksDescription": "Data flows seamlessly from your application through our intelligent routing layer to the best provider for the job.", + "howItWorksStep1Title": "1. CLI & SDKs", + "howItWorksStep1Description": "Your requests start from your favorite tools or our unified SDK. Just change the base URL.", + "howItWorksStep2Title": "2. OmniRoute Hub", + "howItWorksStep2Description": "Our engine analyzes the prompt, checks provider health, and routes for lowest latency or cost.", + "howItWorksStep3Title": "3. AI Providers", + "howItWorksStep3Description": "The request is fulfilled by OpenAI, Anthropic, Gemini, or others instantly.", + "getStartedIn30Seconds": "Get Started in 30 Seconds", + "getStartedDescription": "Install OmniRoute, configure your providers via web dashboard, and start routing AI requests.", + "installOmniRoute": "Install OmniRoute", + "installStepDescription": "Run npx command to start the server instantly", + "openDashboard": "Open Dashboard", + "openDashboardStepDescription": "Configure providers and API keys via web interface", + "routeRequests": "Route Requests", + "routeRequestsStepDescription": "Point your CLI tools to {endpoint}", + "terminal": "terminal", + "copy": "Copy", + "copied": "✓ Copied", + "startingOmniRoute": "Starting OmniRoute...", + "serverRunningOnLabel": "Server running on", + "dashboardLabel": "Dashboard", + "readyToRoute": "Ready to route! ✓", + "configureProvidersNote": "📝 Configure providers in dashboard or use environment variables", + "dataLocation": "Data Location:", + "dataLocationMacLinux": " macOS/Linux:", + "dataLocationWindows": " Windows:", "product": "Product", + "dashboardLink": "Dashboard", + "changelog": "Changelog", "resources": "Resources", + "documentation": "Documentation", + "npm": "NPM", "legal": "Legal", - "interactiveDiagram": "Interactive diagram visible on desktop" + "mitLicense": "MIT License", + "footerTagline": "The unified endpoint for AI generation. Connect, route, and manage your AI providers with ease.", + "copyright": "© {year} OmniRoute. All rights reserved.", + "flowToolClaudeCode": "Claude Code", + "flowToolOpenAICodex": "OpenAI Codex", + "flowToolCline": "Cline", + "flowToolCursor": "Cursor", + "flowProviderOpenAI": "OpenAI", + "flowProviderAnthropic": "Anthropic", + "flowProviderGemini": "Gemini", + "flowProviderGithubCopilot": "GitHub Copilot", + "interactiveDiagram": "Interactive diagram visible on desktop", + "ctaTitle": "Ready to Simplify Your AI Infrastructure?", + "ctaDescription": "Join developers who are streamlining their AI integrations with OmniRoute. Open source and free to start.", + "startFree": "Start Free", + "readDocumentation": "Read Documentation" }, "docs": { "title": "Documentation", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 3b90f00cf7..e7fff896ef 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -321,6 +321,9 @@ "concurrencyPerModel": "Concorrência / Modelo", "queueTimeout": "Timeout da Fila (ms)", "advancedHint": "Deixe vazio para usar padrões globais. Estes substituem configurações por provedor.", + "moveUp": "Mover para cima", + "moveDown": "Mover para baixo", + "removeModel": "Remover", "saving": "Salvando...", "weighted": "Ponderado", "leastUsed": "Menos Usado", @@ -524,11 +527,18 @@ "newAccount": "Nova Conta", "deleteConfirm": "Tem certeza que deseja excluir este provedor?", "testing": "Testando...", + "testConnection": "Testar conexão", "testSuccess": "Conexão bem-sucedida", "testFailed": "Falha na conexão", "available": "Disponível", + "cooldown": "Cooldown", "unavailable": "Indisponível", "unknown": "Desconhecido", + "oauthLabel": "OAuth", + "compatibleLabel": "Compatível", + "chat": "Chat", + "responses": "Responses", + "messages": "Mensagens", "oauthProviders": "Provedores OAuth", "freeProviders": "Provedores Gratuitos", "apiKeyProviders": "Provedores por Chave de API", @@ -553,14 +563,17 @@ "addNewProvider": "Adicionar Novo Provedor", "backToProviders": "Voltar para Provedores", "configureNewProvider": "Configure um novo provedor de IA para usar com suas aplicações.", + "providerLabel": "Provedor", "selectProvider": "Selecione um provedor", "selectedProvider": "Provedor selecionado", "authMethod": "Método de Autenticação", + "apiKeyLabel": "Chave de API", "apiKeyRequired": "Chave de API é obrigatória", "selectProviderRequired": "Selecione um provedor", "enterApiKey": "Digite sua chave de API", "apiKeySecure": "Sua chave de API será criptografada e armazenada com segurança.", "oauth2Connect": "Conectar com OAuth2", + "oauth2Label": "OAuth2", "oauth2Desc": "Conecte sua conta usando autenticação OAuth2.", "displayName": "Nome de Exibição", "displayNamePlaceholder": "ex.: API de Produção, Ambiente Dev", @@ -580,7 +593,16 @@ "loadingAvailability": "Carregando disponibilidade dos modelos...", "clearCooldown": "Limpar", "clearing": "Limpando...", + "until": "Até {time}", "providerTestFailed": "Teste de provedor falhou", + "modeTest": "Teste {mode}", + "passedCount": "{count} passaram", + "failedCount": "{count} falharam", + "testedCount": "{count} testados", + "millisecondsAbbr": "{value}ms", + "okShort": "OK", + "errorShort": "ERRO", + "noActiveConnectionsInGroup": "Nenhuma conexão ativa encontrada para este grupo.", "allTestsPassed": "Todos os {total} testes passaram", "testSummary": "{passed}/{total} passaram, {failed} falharam", "nameLabel": "Nome", @@ -590,6 +612,10 @@ "prefixHint": "Obrigatório. Prefixo único para nomes de modelos.", "nameHint": "Obrigatório. Um rótulo amigável para este nó.", "baseUrlHint": "Obrigatório. URL base da API do provedor.", + "anthropicPrefixPlaceholder": "ac-prod", + "openaiPrefixPlaceholder": "oc-prod", + "anthropicBaseUrlPlaceholder": "https://api.anthropic.com/v1", + "openaiBaseUrlPlaceholder": "https://api.openai.com/v1", "validateConnection": "Validar Conexão", "validating": "Validando...", "connectionValid": "Conexão válida!", @@ -723,6 +749,7 @@ "check": "Verificar", "valid": "Válido", "invalid": "Inválido", + "creating": "Criando...", "validationChecksAnthropicCompatible": "A validação verifica {provider} conferindo a chave de API.", "validationChecksOpenAiCompatible": "A validação verifica {provider} via /models na sua URL base.", "priorityLabel": "Prioridade", @@ -808,7 +835,12 @@ "ai": "IA", "advanced": "Avançado", "localMode": "Modo Local — Todos os dados armazenados na sua máquina", + "settingsSectionsAria": "Seções de configurações", "switchThemes": "Alternar entre temas claro e escuro", + "themeSelectionAria": "Seleção de tema", + "themeLight": "Claro", + "themeDark": "Escuro", + "themeSystem": "Sistema", "hideHealthLogs": "Ocultar Logs de Health Check", "hideHealthLogsDesc": "Quando ATIVADO, suprime mensagens [HealthCheck] no console do servidor", "promptCache": "Cache de Prompt", @@ -821,6 +853,7 @@ "globalProxy": "Proxy Global", "globalProxyDesc": "Configure um proxy de saída global para todas as chamadas de API. Provedores individuais, combos e chaves podem sobrescrever.", "noGlobalProxy": "Nenhum proxy global configurado", + "globalLabel": "Global", "configure": "Configurar", "globalSystemPrompt": "Prompt de Sistema Global", "systemPromptDesc": "Injetado em todas as requisições no nível do proxy", @@ -838,6 +871,10 @@ "customDesc": "Define um orçamento fixo de tokens para todas as requisições", "adaptive": "Adaptativo", "adaptiveDesc": "Escala orçamento baseado na complexidade da requisição", + "effortNone": "Nenhum (0 tokens)", + "effortLow": "Baixo (1K tokens)", + "effortMedium": "Médio (10K tokens)", + "effortHigh": "Alto (128K tokens)", "tokenBudget": "Orçamento de Tokens", "tokens": "tokens", "baseEffortLevel": "Nível de Esforço Base", @@ -862,6 +899,8 @@ "blockedProviders": "Provedores Bloqueados", "blockedProvidersDesc": "Ocultar provedores específicos da resposta /v1/models. Provedores bloqueados não aparecerão nas listagens de modelos.", "providersBlocked": "{count} provedor(es) bloqueado(s) do /models", + "blockProviderTitle": "Bloquear {provider}", + "unblockProviderTitle": "Desbloquear {provider}", "routingStrategy": "Estratégia de Roteamento", "fillFirst": "Preencher Primeiro", "fillFirstDesc": "Usar contas em ordem de prioridade", @@ -879,10 +918,13 @@ "stickyLimitDesc": "Chamadas por conta antes de trocar", "modelAliases": "Aliases de Modelo", "modelAliasesDesc": "Padrões coringa para remapear nomes de modelos • Use * e ?", + "aliasPatternPlaceholder": "claude-sonnet-*", + "aliasTargetPlaceholder": "claude-sonnet-4-20250514", "pattern": "Padrão", "targetModel": "Modelo Alvo", "add": "+ Adicionar", "session": "Sessão", + "sessionDetailsAria": "Detalhes da sessão", "status": "Status", "authenticated": "Autenticado", "guest": "Visitante", @@ -892,9 +934,16 @@ "clearLocalData": "Limpar Dados Locais", "logout": "Sair", "clearLocalDataConfirm": "Limpar todos os dados locais? Isso redefinirá suas preferências.", + "unknown": "Desconhecido", + "systemActor": "sistema", "ipAccessControl": "Controle de Acesso por IP", "ipAccessControlDesc": "Bloquear ou permitir endereços IP específicos", + "ipModeDisabled": "Desativado", + "ipModeBlacklist": "Lista de Bloqueio", + "ipModeWhitelist": "Lista de Permissão", + "ipModeWhitelistPriority": "Permissão Prioritária", "addIpAddress": "Adicionar Endereço IP", + "ipAddressPlaceholder": "192.168.1.0/24 ou 10.0.*.*", "block": "+ Bloquear", "allow": "+ Permitir", "blocked": "Bloqueados ({count})", @@ -913,7 +962,9 @@ "fallbackChainsDesc": "Definir ordem de fallback de provedores por modelo", "addChain": "+ Adicionar Cadeia", "modelName": "Nome do Modelo", + "modelNamePlaceholder": "claude-sonnet-4-20250514", "providersCommaSeparated": "Provedores (separados por vírgula, em ordem de prioridade)", + "providersCommaSeparatedPlaceholder": "anthropic, openai, gemini", "createChain": "Criar Cadeia", "noFallbackChains": "Sem Cadeias de Fallback", "noFallbackChainsDesc": "Crie uma cadeia para definir a ordem de fallback de provedores para um modelo.", @@ -923,20 +974,30 @@ "chainDeleted": "Cadeia excluída para {model}", "failedCreateChain": "Falha ao criar cadeia", "failedDeleteChain": "Falha ao excluir cadeia", + "deleteChain": "Excluir cadeia", "fillModelAndProviders": "Preencha o nome do modelo e os provedores", "addAtLeastOneProvider": "Adicione pelo menos um provedor", "comboDefaultsTitle": "Padrões de Combo", "globalComboConfig": "Configuração global de combos", "defaultStrategy": "Estratégia Padrão", "defaultStrategyDesc": "Aplicada a novos combos sem estratégia explícita", + "comboStrategyAria": "Estratégia de combo", "priority": "Prioridade", "weighted": "Ponderado", + "maxRetriesLabel": "Máx. Tentativas", + "retryDelayLabel": "Atraso entre Tentativas (ms)", + "timeoutLabel": "Timeout (ms)", "healthCheck": "Verificação de Saúde", "healthCheckDesc": "Verificar disponibilidade do provedor antes", "trackMetrics": "Rastrear Métricas", "trackMetricsDesc": "Registrar métricas de requisição por combo", "providerOverrides": "Sobrescritas por Provedor", "providerOverridesDesc": "Substituir timeout e tentativas por provedor. Configurações do provedor substituem os padrões globais.", + "providerMaxRetriesAria": "{provider} tentativas máximas", + "providerTimeoutAria": "timeout de {provider} em ms", + "removeProviderOverrideAria": "Remover sobrescrita de {provider}", + "newProviderNamePlaceholder": "ex.: google, openai...", + "newProviderNameAria": "Nome do novo provedor", "retries": "tentativas", "ms": "ms", "saveComboDefaults": "Salvar Padrões de Combo", @@ -964,6 +1025,9 @@ "running": "Em Execução", "queued": "Na Fila", "circuitBreakers": "Disjuntores", + "breakerStateClosed": "Fechado", + "breakerStateOpen": "Aberto", + "breakerStateHalfOpen": "Semiaberto", "tripped": "{count} aberto(s)", "healthy": "{count} saudável(is)", "resetAll": "Resetar Todos", @@ -973,9 +1037,15 @@ "allOperational": "Todos os sistemas operacionais — sem bloqueios ou disjuntores ativados", "loadingPolicies": "Carregando políticas...", "lockedIdentifiers": "Identificadores Bloqueados", + "unlockedIdentifier": "Desbloqueado: {identifier}", + "sinceDate": "desde {date}", "forceUnlock": "Forçar Desbloqueio", "unlocking": "Desbloqueando...", "failedUnlock": "Falha ao desbloquear", + "failedLoadWithStatus": "Falha ao carregar: {status}", + "failedLoadResilience": "Falha ao carregar status de resiliência", + "saveFailed": "Falha ao salvar", + "resetFailed": "Falha ao resetar", "loadingResilience": "Carregando status de resiliência...", "retry": "Tentar Novamente", "systemStorage": "Sistema e Armazenamento", @@ -1003,6 +1073,19 @@ "no": "Não", "restore": "Restaurar", "invalidFileType": "Tipo de arquivo inválido. Apenas arquivos .sqlite são aceitos.", + "exportFailed": "Falha na exportação", + "exportFailedWithError": "Falha na exportação: {error}", + "fullExportFailedWithError": "Falha na exportação completa: {error}", + "backupCreated": "Backup criado: {file}", + "restoreSuccess": "Restaurado! {connections} conexões, {nodes} nós, {combos} combos, {apiKeys} chaves de API.", + "importSuccess": "Banco importado! {connections} conexões, {nodes} nós, {combos} combos, {apiKeys} chaves de API.", + "justNow": "agora mesmo", + "minutesAgo": "{count}m atrás", + "hoursAgo": "{count}h atrás", + "daysAgo": "{count}d atrás", + "backupReasonManual": "manual", + "backupReasonPreRestore": "pré-restauração", + "connectionsCount": "{count, plural, one {# conexão} other {# conexões}}", "noChangesSinceBackup": "Sem alterações desde o último backup", "backupFailed": "Falha no backup", "restoreFailed": "Falha na restauração", @@ -1025,6 +1108,7 @@ "saving": "Salvando...", "model": "Modelo", "models": "modelos", + "moreProviders": "{count} provedores adicionais", "withPricing": "com preço configurado", "policiesCircuitBreakers": "Políticas e Disjuntores", "activeIssuesDetected": "Problemas ativos detectados", @@ -1080,6 +1164,8 @@ "successful": "Sucesso", "errors": "Erros", "avgLatency": "Latência Média", + "millisecondsShort": "{value}ms", + "notAvailableSymbol": "—", "liveAutoRefreshing": "Ao vivo — atualizando automaticamente", "paused": "Pausado", "eventsAppearHint": "Os eventos de tradução aparecem aqui conforme as requisições passam pelo OmniRoute. Use qualquer um destes métodos para gerar eventos:", @@ -1179,7 +1265,7 @@ "monthlyLimitPlaceholder": "ex.: 50.00", "warningThresholdPlaceholder": "80", "saveLimits": "Salvar limites", - "budgetOk": "Orçamento OK — ${remaining} restantes", + "budgetOk": "Orçamento OK — {remaining} restantes", "budgetExceeded": "Orçamento excedido — requisições podem ser bloqueadas", "totalRequests": "Total de requisições", "noDataYet": "Sem dados ainda", @@ -1279,6 +1365,11 @@ "lastUsed": "Último uso", "actions": "Ações", "refreshQuota": "Atualizar cota", + "today": "Hoje", + "tomorrow": "Amanhã", + "dayTimeFormat": "{day}, {time}", + "inDuration": "em {duration}", + "notApplicable": "N/D", "rawPlanWithValue": "Plano bruto: {plan}", "noPlanFromProvider": "Sem plano do provedor", "noQuotaData": "Sem dados de cota", @@ -1390,19 +1481,86 @@ "forgotPassword": "Esqueceu a senha?" }, "landing": { + "brandName": "OmniRoute", + "navigateHome": "Navegar para a página inicial", + "toggleMenu": "Alternar menu", + "featuresLink": "Recursos", + "docsLink": "Docs", + "github": "GitHub", + "versionLive": "v1.0 já está no ar", + "oneEndpoint": "Um Endpoint para", "allProviders": "Todos os Provedores de IA", - "oneEndpoint": "Um Endpoint", - "powerfulFeatures": "Recursos Poderosos", - "howItWorks": "Como o OmniRoute Funciona", - "installOmniRoute": "Instalar o OmniRoute", - "openDashboard": "Abrir Painel", - "routeRequests": "Rotear Requisições", - "dataLocation": "Local dos Dados:", + "heroDescription": "Proxy de endpoint de IA com painel web - uma versão em JavaScript do CLIProxyAPI. Funciona perfeitamente com Claude Code, OpenAI Codex, Cline, RooCode e outras ferramentas CLI.", "getStarted": "Começar", + "viewOnGithub": "Ver no GitHub", + "powerfulFeatures": "Recursos Poderosos", + "featuresSubtitle": "Tudo que você precisa para gerenciar sua infraestrutura de IA em um só lugar, preparado para escala.", + "featureUnifiedEndpointTitle": "Endpoint Unificado", + "featureUnifiedEndpointDesc": "Acesse todos os provedores por uma única URL de API padrão.", + "featureEasySetupTitle": "Configuração Fácil", + "featureEasySetupDesc": "Fique pronto em minutos com o comando npx.", + "featureModelFallbackTitle": "Fallback de Modelo", + "featureModelFallbackDesc": "Alterne automaticamente entre provedores em caso de falha ou alta latência.", + "featureUsageTrackingTitle": "Rastreamento de Uso", + "featureUsageTrackingDesc": "Análises detalhadas e monitoramento de custos em todos os modelos.", + "featureOAuthApiKeysTitle": "OAuth e Chaves de API", + "featureOAuthApiKeysDesc": "Gerencie credenciais com segurança em um único cofre.", + "featureCloudSyncTitle": "Sincronização em Nuvem", + "featureCloudSyncDesc": "Sincronize suas configurações entre dispositivos instantaneamente.", + "featureCliSupportTitle": "Suporte a CLI", + "featureCliSupportDesc": "Funciona com Claude Code, Codex, Cline, Cursor e mais.", + "featureDashboardTitle": "Painel", + "featureDashboardDesc": "Painel visual para análise de tráfego em tempo real.", + "howItWorks": "Como o OmniRoute Funciona", + "howItWorksDescription": "Os dados fluem de forma contínua da sua aplicação pela nossa camada de roteamento inteligente até o melhor provedor para cada tarefa.", + "howItWorksStep1Title": "1. CLI e SDKs", + "howItWorksStep1Description": "Suas requisições começam nas suas ferramentas favoritas ou no nosso SDK unificado. Basta trocar a URL base.", + "howItWorksStep2Title": "2. Hub OmniRoute", + "howItWorksStep2Description": "Nosso mecanismo analisa o prompt, verifica a saúde dos provedores e roteia para menor latência ou custo.", + "howItWorksStep3Title": "3. Provedores de IA", + "howItWorksStep3Description": "A requisição é atendida por OpenAI, Anthropic, Gemini ou outros provedores instantaneamente.", + "getStartedIn30Seconds": "Comece em 30 segundos", + "getStartedDescription": "Instale o OmniRoute, configure seus provedores pelo painel web e comece a rotear requisições de IA.", + "installOmniRoute": "Instalar o OmniRoute", + "installStepDescription": "Execute o comando npx para iniciar o servidor instantaneamente", + "openDashboard": "Abrir Painel", + "openDashboardStepDescription": "Configure provedores e chaves de API pela interface web", + "routeRequests": "Rotear Requisições", + "routeRequestsStepDescription": "Aponte suas ferramentas CLI para {endpoint}", + "terminal": "terminal", + "copy": "Copiar", + "copied": "✓ Copiado", + "startingOmniRoute": "Iniciando OmniRoute...", + "serverRunningOnLabel": "Servidor em execução em", + "dashboardLabel": "Painel", + "readyToRoute": "Pronto para rotear! ✓", + "configureProvidersNote": "📝 Configure provedores no painel ou use variáveis de ambiente", + "dataLocation": "Local dos Dados:", + "dataLocationMacLinux": " macOS/Linux:", + "dataLocationWindows": " Windows:", "product": "Produto", + "dashboardLink": "Painel", + "changelog": "Changelog", "resources": "Recursos", + "documentation": "Documentação", + "npm": "NPM", "legal": "Legal", - "interactiveDiagram": "Diagrama interativo visível no desktop" + "mitLicense": "Licença MIT", + "footerTagline": "O endpoint unificado para geração de IA. Conecte, roteie e gerencie seus provedores de IA com facilidade.", + "copyright": "© {year} OmniRoute. Todos os direitos reservados.", + "flowToolClaudeCode": "Claude Code", + "flowToolOpenAICodex": "OpenAI Codex", + "flowToolCline": "Cline", + "flowToolCursor": "Cursor", + "flowProviderOpenAI": "OpenAI", + "flowProviderAnthropic": "Anthropic", + "flowProviderGemini": "Gemini", + "flowProviderGithubCopilot": "GitHub Copilot", + "interactiveDiagram": "Diagrama interativo visível no desktop", + "ctaTitle": "Pronto para simplificar sua infraestrutura de IA?", + "ctaDescription": "Junte-se a desenvolvedores que estão simplificando suas integrações de IA com o OmniRoute. Open source e grátis para começar.", + "startFree": "Começar grátis", + "readDocumentation": "Ler documentação" }, "docs": { "title": "Documentação",