From 187aba05140d0ac7b4cf936c430490f6495566e6 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 26 Feb 2026 01:41:52 -0300 Subject: [PATCH] feat(i18n): replace remaining hardcoded strings with translation keys Replace hardcoded English text with t() translation calls across combos, providers, and ModelAvailabilityBadge components. Add corresponding translation keys to locale files for full i18n coverage. --- src/app/(dashboard)/dashboard/combos/page.tsx | 6 +- .../dashboard/providers/[id]/page.tsx | 6 +- .../components/ModelAvailabilityBadge.tsx | 70 +++-- .../components/ModelAvailabilityPanel.tsx | 82 ++++-- .../dashboard/providers/new/page.tsx | 14 +- .../(dashboard)/dashboard/providers/page.tsx | 174 ++++++----- .../settings/components/AppearanceTab.tsx | 9 +- .../settings/components/ComboDefaultsTab.tsx | 46 +-- .../settings/components/ComplianceTab.tsx | 76 ++--- .../components/FallbackChainsEditor.tsx | 6 +- .../settings/components/IPFilterSection.tsx | 24 +- .../settings/components/ProxyTab.tsx | 2 +- .../settings/components/ResilienceTab.tsx | 109 ++++--- .../settings/components/RoutingTab.tsx | 49 ++-- .../settings/components/SecurityTab.tsx | 26 +- .../settings/components/SessionInfoCard.tsx | 6 +- .../settings/components/SystemStorageTab.tsx | 75 +++-- .../settings/components/ThinkingBudgetTab.tsx | 32 +- .../(dashboard)/dashboard/settings/page.tsx | 2 +- .../dashboard/settings/pricing/page.tsx | 14 +- .../translator/components/LiveMonitorMode.tsx | 48 ++- .../dashboard/usage/components/BudgetTab.tsx | 45 ++- .../ProviderLimits/ProviderLimitCard.tsx | 13 +- .../components/ProviderLimits/QuotaTable.tsx | 29 +- .../usage/components/RateLimitStatus.tsx | 10 +- .../usage/components/SessionsTab.tsx | 5 +- src/app/forgot-password/page.tsx | 12 +- src/app/landing/components/Features.tsx | 51 ++-- src/app/landing/components/FlowAnimation.tsx | 276 ++++++++++-------- src/app/landing/components/Footer.tsx | 41 +-- src/app/landing/components/GetStarted.tsx | 61 ++-- src/app/landing/components/HeroSection.tsx | 31 +- src/app/landing/components/HowItWorks.tsx | 39 ++- src/app/landing/components/Navigation.tsx | 34 ++- src/app/landing/page.tsx | 15 +- src/i18n/messages/en.json | 176 ++++++++++- src/i18n/messages/pt-BR.json | 176 ++++++++++- 37 files changed, 1216 insertions(+), 674 deletions(-) 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",