feat: complete i18n migration — 21 pages/components translated + PT-BR README section

- Full en.json and pt-BR.json with 26 namespaces (~460 strings)
- Migrated: HomePageClient, Analytics, Translator (4 components),
  Usage (5 components), ProviderLimits, Shared (UsageStats,
  TokenHealthBadge, SystemMonitor, ConsoleLogViewer, Footer),
  Login, Callback, ForgotPassword, Forbidden
- README: added PT-BR section with quick start, features, i18n docs
- README: added language switcher badges at top
This commit is contained in:
diegosouzapw
2026-02-25 13:30:57 -03:00
parent 88d5986ac1
commit 0d13f4645c
24 changed files with 1223 additions and 123 deletions

View File

@@ -3,6 +3,8 @@
# 🚀 OmniRoute — The Free AI Gateway
🌐 **[English](#-omniroute--the-free-ai-gateway)** | **[Português (BR)](#-omniroute--gateway-de-ia-gratuito)**
### Never stop coding. Smart routing to **FREE & low-cost AI models** with automatic fallback.
_Your universal API proxy — one endpoint, 36+ providers, zero downtime._
@@ -1145,6 +1147,85 @@ MIT License - see [LICENSE](LICENSE) for details.
---
---
## 🇧🇷 OmniRoute — Gateway de IA Gratuito
<a name="-omniroute--gateway-de-ia-gratuito"></a>
### Nunca pare de codar. Roteamento inteligente para **modelos de IA GRATUITOS e de baixo custo** com fallback automático.
_Seu proxy universal de API — um endpoint, 36+ provedores, zero downtime._
### 🌐 Internacionalização (i18n)
O dashboard do OmniRoute suporta **múltiplos idiomas**. Atualmente disponível em:
| Idioma | Código | Status |
| --------------------- | ------- | ----------- |
| 🇺🇸 English | `en` | ✅ Completo |
| 🇧🇷 Português (Brasil) | `pt-BR` | ✅ Completo |
**Para trocar o idioma:** Clique no seletor de idioma (🇺🇸 EN) no header do dashboard → selecione o idioma desejado.
**Para adicionar um novo idioma:**
1. Crie `src/i18n/messages/{codigo}.json` baseado em `en.json`
2. Adicione o código em `src/i18n/config.ts``LOCALES` e `LANGUAGES`
3. Reinicie o servidor
### ⚡ Início Rápido
```bash
# Instalar via npm
npx omniroute@latest
# Ou rodar do código-fonte
cp .env.example .env
npm install
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
```
### 🐳 Docker
```bash
docker run -d --name omniroute -p 20128:20128 diegosouzapw/omniroute:latest
```
### 🔑 Funcionalidades Principais
- **36+ provedores de IA** — Claude, GPT, Gemini, Llama, Qwen, DeepSeek, e mais
- **Roteamento inteligente** — Fallback automático entre provedores
- **Tradução de formato** — OpenAI ↔ Claude ↔ Gemini automaticamente
- **Multi-conta** — Múltiplas contas por provedor com seleção inteligente
- **Cache semântico** — Reduz custos e latência
- **OAuth automático** — Tokens renovam automaticamente
- **Combos personalizados** — 6 estratégias de roteamento
- **Dashboard completo** — Monitoramento, logs, análises, configurações
- **CLI Tools** — Configure Claude Code, Codex, Cursor, Cline com um clique
- **100% TypeScript** — Código limpo e tipado
### 📖 Documentação
| Documento | Descrição |
| ----------------------------------------------- | -------------------------------------- |
| [Guia do Usuário](docs/USER_GUIDE.md) | Provedores, combos, CLI, deploy |
| [Referência da API](docs/API_REFERENCE.md) | Todos os endpoints com exemplos |
| [Solução de Problemas](docs/TROUBLESHOOTING.md) | Problemas comuns e soluções |
| [Arquitetura](docs/ARCHITECTURE.md) | Arquitetura e internos do sistema |
| [Contribuição](CONTRIBUTING.md) | Setup de desenvolvimento e guidelines |
| [Deploy em VM](docs/VM_DEPLOYMENT_GUIDE.md) | Guia completo: VM + nginx + Cloudflare |
### 📧 Suporte
> 💬 **Entre para a comunidade!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Tire dúvidas, compartilhe dicas e fique atualizado.
- **Website**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
---
<div align="center">
<sub>Built with ❤️ for developers who code 24/7</sub>
<br/>

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useMemo, useCallback } from "react";
import PropTypes from "prop-types";
import Image from "next/image";
@@ -10,6 +12,8 @@ import { AI_PROVIDERS, FREE_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constant
import { useNotificationStore } from "@/store/notificationStore";
export default function HomePageClient({ machineId }) {
const t = useTranslations("home");
const tc = useTranslations("common");
const [providerConnections, setProviderConnections] = useState([]);
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(true);
@@ -103,14 +107,14 @@ export default function HomePageClient({ machineId }) {
}, [selectedProvider, models]);
const quickStartLinks = [
{ label: "Documentation", href: "/docs", icon: "menu_book" },
{ label: "Providers", href: "/dashboard/providers", icon: "dns" },
{ label: t("documentation"), href: "/docs", icon: "menu_book" },
{ label: tc("provider") + "s", href: "/dashboard/providers", icon: "dns" },
{ label: "Combos", href: "/dashboard/combos", icon: "layers" },
{ label: "Analytics", href: "/dashboard/analytics", icon: "analytics" },
{ label: "Health Monitor", href: "/dashboard/health", icon: "health_and_safety" },
{ label: t("healthMonitor"), href: "/dashboard/health", icon: "health_and_safety" },
{ label: "CLI Tools", href: "/dashboard/cli-tools", icon: "terminal" },
{
label: "Report issue",
label: t("reportIssue"),
href: "https://github.com/diegosouzapw/OmniRoute/issues",
external: true,
icon: "bug_report",
@@ -135,17 +139,15 @@ export default function HomePageClient({ machineId }) {
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Quick Start</h2>
<p className="text-sm text-text-muted">
Get up and running in 4 steps. Connect providers, route models, monitor everything.
</p>
<h2 className="text-lg font-semibold">{t("quickStart")}</h2>
<p className="text-sm text-text-muted">{t("quickStartDesc")}</p>
</div>
<Link
href="/docs"
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
>
<span className="material-symbols-outlined text-[14px]">menu_book</span>
Full Docs
{t("fullDocs")}
</Link>
</div>
@@ -155,7 +157,7 @@ export default function HomePageClient({ machineId }) {
<span className="material-symbols-outlined text-[18px]">key</span>
</div>
<div>
<span className="font-semibold">1. Create API key</span>
<span className="font-semibold">{t("step1Title")}</span>
<p className="text-text-muted mt-0.5">
Go to{" "}
<Link href="/dashboard/endpoint" className="text-primary hover:underline">
@@ -170,7 +172,7 @@ export default function HomePageClient({ machineId }) {
<span className="material-symbols-outlined text-[18px]">dns</span>
</div>
<div>
<span className="font-semibold">2. Connect providers</span>
<span className="font-semibold">{t("step2Title")}</span>
<p className="text-text-muted mt-0.5">
Add accounts in{" "}
<Link href="/dashboard/providers" className="text-primary hover:underline">
@@ -185,7 +187,7 @@ export default function HomePageClient({ machineId }) {
<span className="material-symbols-outlined text-[18px]">link</span>
</div>
<div>
<span className="font-semibold">3. Point your client</span>
<span className="font-semibold">{t("step3Title")}</span>
<p className="text-text-muted mt-0.5">
Set base URL to{" "}
<code className="px-1.5 py-0.5 rounded bg-surface text-xs font-mono">
@@ -200,7 +202,7 @@ export default function HomePageClient({ machineId }) {
<span className="material-symbols-outlined text-[18px]">analytics</span>
</div>
<div>
<span className="font-semibold">4. Monitor & optimize</span>
<span className="font-semibold">{t("step4Title")}</span>
<p className="text-text-muted mt-0.5">
Track tokens, cost and errors in{" "}
<Link href="/dashboard/usage" className="text-primary hover:underline">
@@ -239,7 +241,7 @@ export default function HomePageClient({ machineId }) {
<Card>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold">Providers Overview</h2>
<h2 className="text-lg font-semibold">{t("providersOverview")}</h2>
<p className="text-sm text-text-muted">
{providerStats.filter((item) => item.total > 0).length} configured of{" "}
{providerStats.length} available providers
@@ -348,7 +350,7 @@ function ProviderOverviewCard({ item, metrics, onClick }) {
</div>
<p className={`text-xs ${statusVariant}`}>
{item.total === 0
? "Not configured"
? tc("notConfigured")
: `${item.connected} active · ${item.errors} error`}
</p>
{metrics && metrics.totalRequests > 0 && (
@@ -433,7 +435,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
<span className="material-symbols-outlined text-[32px] text-text-muted mb-2">
search_off
</span>
<p className="text-sm text-text-muted">No models available for this provider.</p>
<p className="text-sm text-text-muted">{t("noModelsAvailable")}</p>
<p className="text-xs text-text-muted mt-1">
Configure a connection first in{" "}
<button
@@ -460,7 +462,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
<button
onClick={() => handleCopy(m.fullModel)}
className="shrink-0 ml-2 p-1.5 rounded-lg text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors opacity-0 group-hover:opacity-100"
title="Copy model name"
title={t("copyModelName")}
>
<span className="material-symbols-outlined text-[14px]">
{copiedModel === m.fullModel ? "check" : "content_copy"}
@@ -481,7 +483,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
className="flex-1"
>
<span className="material-symbols-outlined text-[14px] mr-1">settings</span>
Configure Provider
{t("configureProvider")}
</Button>
<Button variant="ghost" size="sm" onClick={onClose}>
Close

View File

@@ -3,15 +3,15 @@
import { useState, Suspense } from "react";
import { UsageAnalytics, CardSkeleton, SegmentedControl } from "@/shared/components";
import EvalsTab from "../usage/components/EvalsTab";
import { useTranslations } from "next-intl";
export default function AnalyticsPage() {
const [activeTab, setActiveTab] = useState("overview");
const t = useTranslations("analytics");
const tabDescriptions = {
overview:
"Monitor your API usage patterns, token consumption, costs, and activity trends across all providers and models.",
evals:
"Run evaluation suites to test and validate your LLM endpoints. Compare model quality, detect regressions, and benchmark latency.",
overview: t("overviewDescription"),
evals: t("evalsDescription"),
};
return (
@@ -20,15 +20,15 @@ export default function AnalyticsPage() {
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<span className="material-symbols-outlined text-primary text-[28px]">analytics</span>
Analytics
{t("title")}
</h1>
<p className="text-sm text-text-muted mt-1">{tabDescriptions[activeTab]}</p>
</div>
<SegmentedControl
options={[
{ value: "overview", label: "Overview" },
{ value: "evals", label: "Evals" },
{ value: "overview", label: t("overview") },
{ value: "evals", label: t("evals") },
]}
value={activeTab}
onChange={setActiveTab}

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useRef } from "react";
import { Card, Button, Select, Badge } from "@/shared/components";
import { FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
@@ -12,7 +14,7 @@ const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
/**
* Chat Tester Mode:
* - Left: Chat interface (send messages as a specific client format)
* - Right: Pipeline visualization showing each translation step
* - Right: {t("pipelineVisualization")} showing each translation step
*
* How it works:
* 1. You type a message and select a "Client Format" (how the request is structured)
@@ -22,6 +24,7 @@ const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
*/
export default function ChatTesterMode() {
const t = useTranslations("translator");
const { provider, setProvider, providerOptions } = useProviderOptions("openai");
const { model, setModel, availableModels, pickModelForFormat } = useAvailableModels();
const [clientFormat, setClientFormat] = useState("openai");
@@ -246,7 +249,7 @@ export default function ChatTesterMode() {
info
</span>
<div>
<p className="font-medium text-text-main mb-0.5">Pipeline Debugger</p>
<p className="font-medium text-text-main mb-0.5">{t("pipelineDebugger")}</p>
<p>
Send messages as a specific client format and see how each step of the translation
pipeline works. The right panel shows the full flow:{" "}
@@ -386,7 +389,7 @@ export default function ChatTesterMode() {
<span className="material-symbols-outlined text-[18px] text-primary">
account_tree
</span>
<h3 className="text-sm font-semibold text-text-main">Translation Pipeline</h3>
<h3 className="text-sm font-semibold text-text-main">{t("translationPipeline")}</h3>
</div>
<p className="text-xs text-text-muted">
Click on any step to inspect the data at that stage

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useRef } from "react";
import { Card, Badge } from "@/shared/components";
import { FORMAT_META } from "../exampleTemplates";
@@ -10,6 +12,7 @@ import { FORMAT_META } from "../exampleTemplates";
* Polls /api/translator/history for translation events.
*/
export default function LiveMonitorMode() {
const t = useTranslations("translator");
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [autoRefresh, setAutoRefresh] = useState(true);
@@ -55,12 +58,12 @@ export default function LiveMonitorMode() {
info
</span>
<div>
<p className="font-medium text-text-main mb-0.5">Real-Time Translation Activity</p>
<p className="font-medium text-text-main mb-0.5">{t("realtime")}</p>
<p>
Shows translation events as API calls flow through OmniRoute. Events come from the
in-memory buffer (resets on restart). Use{" "}
<strong className="text-text-main">Chat Tester</strong>,{" "}
<strong className="text-text-main">Test Bench</strong>, or external API calls to
<strong className="text-text-main">{t("chatTester")}</strong>,{" "}
<strong className="text-text-main">{t("testBench")}</strong>, or external API calls to
generate events.
</p>
</div>
@@ -103,7 +106,7 @@ export default function LiveMonitorMode() {
{/* Events Table */}
<Card>
<div className="p-4">
<h3 className="text-sm font-semibold text-text-main mb-3">Recent Translations</h3>
<h3 className="text-sm font-semibold text-text-main mb-3">{t("recentTranslations")}</h3>
{loading ? (
<div className="flex items-center justify-center py-12 text-text-muted">
@@ -115,7 +118,7 @@ export default function LiveMonitorMode() {
<span className="material-symbols-outlined text-[48px] mb-3 opacity-30">
monitoring
</span>
<p className="text-sm font-medium mb-1">No translations yet</p>
<p className="text-sm font-medium mb-1">{t("noTranslations")}</p>
<p className="text-xs text-center max-w-sm">
Translation events appear here as requests flow through OmniRoute. Use any of these
methods to generate events:
@@ -143,13 +146,13 @@ export default function LiveMonitorMode() {
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-text-muted border-b border-border">
<th className="pb-2 pr-4">Time</th>
<th className="pb-2 pr-4">Source</th>
<th className="pb-2 pr-4">{t(">time</")}</th>
<th className="pb-2 pr-4">{t(">source</")}</th>
<th className="pb-2 pr-4"></th>
<th className="pb-2 pr-4">Target</th>
<th className="pb-2 pr-4">Model</th>
<th className="pb-2 pr-4">Status</th>
<th className="pb-2 text-right">Latency</th>
<th className="pb-2 pr-4">{t(">target</")}</th>
<th className="pb-2 pr-4">{t(">model</")}</th>
<th className="pb-2 pr-4">{t(">status</")}</th>
<th className="pb-2 text-right">{t(">latency</")}</th>
</tr>
</thead>
<tbody>

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useCallback, useEffect } from "react";
import { Card, Button, Select, Badge } from "@/shared/components";
import { EXAMPLE_TEMPLATES, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
@@ -8,6 +10,7 @@ import dynamic from "next/dynamic";
const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
export default function PlaygroundMode() {
const t = useTranslations("translator");
const [sourceFormat, setSourceFormat] = useState("claude");
const [targetFormat, setTargetFormat] = useState("openai");
const [inputContent, setInputContent] = useState("");
@@ -114,7 +117,7 @@ export default function PlaygroundMode() {
info
</span>
<div>
<p className="font-medium text-text-main mb-0.5">Format Converter</p>
<p className="font-medium text-text-main mb-0.5">{t("formatConverter")}</p>
<p>
Paste or type a JSON request body. The translator will auto-detect the source format and
convert it to the target format. Use this to debug how OmniRoute translates requests
@@ -201,7 +204,7 @@ export default function PlaygroundMode() {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-text-muted">input</span>
<h3 className="text-sm font-semibold text-text-main">Input</h3>
<h3 className="text-sm font-semibold text-text-main">{t(">input</")}</h3>
{detectedFormat && (
<Badge variant="info" size="sm" dot>
{FORMAT_META[detectedFormat]?.label || detectedFormat}
@@ -265,7 +268,7 @@ export default function PlaygroundMode() {
<span className="material-symbols-outlined text-[18px] text-text-muted">
output
</span>
<h3 className="text-sm font-semibold text-text-main">Output</h3>
<h3 className="text-sm font-semibold text-text-main">{t(">output</")}</h3>
{outputContent && (
<Badge variant="success" size="sm" dot>
{FORMAT_META[targetFormat]?.label || targetFormat}
@@ -303,7 +306,7 @@ export default function PlaygroundMode() {
</Card>
</div>
{/* Example Templates */}
{/* {t("exampleTemplates")} */}
<Card>
<div className="p-4 space-y-3">
<div className="flex items-center gap-2">

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect } from "react";
import { Card, Button, Select, Badge } from "@/shared/components";
import { EXAMPLE_TEMPLATES, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
@@ -26,6 +28,7 @@ const SCENARIOS = [
];
export default function TestBenchMode() {
const t = useTranslations("translator");
const [sourceFormat, setSourceFormat] = useState("claude");
const { provider, setProvider, providerOptions } = useProviderOptions("openai");
const { model, setModel, availableModels, pickModelForFormat } = useAvailableModels();
@@ -147,7 +150,7 @@ export default function TestBenchMode() {
info
</span>
<div>
<p className="font-medium text-text-main mb-0.5">Compatibility Tester</p>
<p className="font-medium text-text-main mb-0.5">{t("compatibilityTester")}</p>
<p>
Run predefined scenarios (Simple Chat, Tool Calling, etc.) to verify translation and
provider compatibility. Select a source format and target provider, then run all tests
@@ -232,7 +235,7 @@ export default function TestBenchMode() {
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<h3 className="text-sm font-semibold text-text-main">Compatibility Report</h3>
<h3 className="text-sm font-semibold text-text-main">{t("compatibilityReport")}</h3>
<Badge
variant={
compatibility >= 80 ? "success" : compatibility >= 50 ? "warning" : "error"

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
/**
* BudgetTab — Batch C
*
@@ -34,6 +36,7 @@ function ProgressBar({ value, max, warningAt = 0.8 }) {
}
export default function BudgetTab() {
const t = useTranslations("usage");
const [keys, setKeys] = useState([]);
const [selectedKey, setSelectedKey] = useState(null);
const [budget, setBudget] = useState(null);
@@ -142,11 +145,11 @@ export default function BudgetTab() {
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-500">
<span className="material-symbols-outlined text-[20px]">account_balance_wallet</span>
</div>
<h3 className="text-lg font-semibold">Budget Management</h3>
<h3 className="text-lg font-semibold">{t("budgetManagement")}</h3>
</div>
<div className="mb-4">
<label className="text-sm text-text-muted mb-1 block">API Key</label>
<label className="text-sm text-text-muted mb-1 block">{t(">apiKey</")}</label>
<select
value={selectedKey || ""}
onChange={(e) => setSelectedKey(e.target.value)}
@@ -170,7 +173,7 @@ export default function BudgetTab() {
)}
</div>
<div className="p-4 rounded-lg border border-border/30 bg-surface/20">
<p className="text-sm text-text-muted mb-2">This Month</p>
<p className="text-sm text-text-muted mb-2">{t("thisMonth")}</p>
<p className="text-2xl font-bold text-text-main">${monthlyCost.toFixed(2)}</p>
{monthlyLimit > 0 && (
<ProgressBar value={monthlyCost} max={monthlyLimit} warningAt={warnPct} />
@@ -180,7 +183,7 @@ export default function BudgetTab() {
{/* Budget Form */}
<div className="border-t border-border/30 pt-4">
<p className="text-sm font-medium mb-3">Set Limits</p>
<p className="text-sm font-medium mb-3">{t("setLimits")}</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<Input
label="Daily Limit (USD)"

View File

@@ -1,9 +1,12 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect } from "react";
import { Card } from "@/shared/components";
export default function BudgetTelemetryCards() {
const t = useTranslations("usage");
const [telemetry, setTelemetry] = useState(null);
const [cache, setCache] = useState(null);
const [policies, setPolicies] = useState(null);
@@ -45,12 +48,12 @@ export default function BudgetTelemetryCards() {
<span className="font-mono">{fmt(telemetry.p99)}</span>
</div>
<div className="flex justify-between border-t border-border pt-2 mt-2">
<span className="text-text-muted">Total requests</span>
<span className="text-text-muted">{t("totalRequests")}</span>
<span className="font-mono">{telemetry.totalRequests ?? 0}</span>
</div>
</div>
) : (
<p className="text-sm text-text-muted">No data yet</p>
<p className="text-sm text-text-muted">{t("noDataYet")}</p>
)}
</Card>
@@ -63,17 +66,17 @@ export default function BudgetTelemetryCards() {
{cache ? (
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-text-muted">Entries</span>
<span className="text-text-muted">{t(">entries</")}</span>
<span className="font-mono">
{cache.size}/{cache.maxSize}
</span>
</div>
<div className="flex justify-between">
<span className="text-text-muted">Hit Rate</span>
<span className="text-text-muted">{t("hitRate")}</span>
<span className="font-mono">{cache.hitRate?.toFixed(1) ?? 0}%</span>
</div>
<div className="flex justify-between">
<span className="text-text-muted">Hits / Misses</span>
<span className="text-text-muted">{t("hitsMisses")}</span>
<span className="font-mono">
{cache.hits ?? 0} / {cache.misses ?? 0}
</span>
@@ -93,11 +96,11 @@ export default function BudgetTelemetryCards() {
{policies ? (
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-text-muted">Circuit Breakers</span>
<span className="text-text-muted">{t("circuitBreakers")}</span>
<span className="font-mono">{policies.circuitBreakers?.length ?? 0} active</span>
</div>
<div className="flex justify-between">
<span className="text-text-muted">Locked IPs</span>
<span className="text-text-muted">{t("lockedIPs")}</span>
<span className="font-mono">{policies.lockedIdentifiers?.length ?? 0}</span>
</div>
{policies.circuitBreakers?.some((cb) => cb.state === "OPEN") && (

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
/**
* EvalsTab — Batch F
*
@@ -49,6 +51,7 @@ const STRATEGIES = [
];
export default function EvalsTab() {
const t = useTranslations("usage");
const [suites, setSuites] = useState([]);
const [apiKey, setApiKey] = useState(null);
const [loading, setLoading] = useState(true);
@@ -255,7 +258,7 @@ export default function EvalsTab() {
</Card>
</div>
{/* How It Works — Collapsible */}
{/* {t("howItWorks")} — Collapsible */}
<Card className="p-0 overflow-hidden">
<button
onClick={() => setShowHowItWorks(!showHowItWorks)}
@@ -289,7 +292,7 @@ export default function EvalsTab() {
<div className="w-10 h-10 rounded-full bg-violet-500/20 flex items-center justify-center mb-3">
<span className="text-lg font-bold text-violet-400">1</span>
</div>
<h4 className="text-sm font-semibold text-text-main mb-1">Define</h4>
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">define</")}</h4>
<p className="text-xs text-text-muted">
Create test cases with input prompts and expected output criteria using strategies
like contains, regex, or exact match.
@@ -299,7 +302,7 @@ export default function EvalsTab() {
<div className="w-10 h-10 rounded-full bg-sky-500/20 flex items-center justify-center mb-3">
<span className="text-lg font-bold text-sky-400">2</span>
</div>
<h4 className="text-sm font-semibold text-text-main mb-1">Run</h4>
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">run</")}</h4>
<p className="text-xs text-text-muted">
Execute test cases against your LLM endpoints through OmniRoute. Each case is sent
as a real API request.
@@ -309,7 +312,7 @@ export default function EvalsTab() {
<div className="w-10 h-10 rounded-full bg-emerald-500/20 flex items-center justify-center mb-3">
<span className="text-lg font-bold text-emerald-400">3</span>
</div>
<h4 className="text-sm font-semibold text-text-main mb-1">Evaluate</h4>
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">evaluate</")}</h4>
<p className="text-xs text-text-muted">
Responses are compared against expected criteria. See pass/fail for each case with
latency metrics and detailed feedback.
@@ -369,7 +372,7 @@ export default function EvalsTab() {
<span className="material-symbols-outlined text-[20px]">science</span>
</div>
<div>
<h3 className="text-lg font-semibold">Evaluation Suites</h3>
<h3 className="text-lg font-semibold">{t("evalSuites")}</h3>
<p className="text-xs text-text-muted">
Click a suite to view test cases, then run to evaluate your LLM endpoints
</p>
@@ -663,7 +666,7 @@ function HeroSection() {
<span className="material-symbols-outlined text-[28px]">science</span>
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-text-main mb-1">Model Evaluations</h2>
<h2 className="text-xl font-bold text-text-main mb-1">{t("modelEvals")}</h2>
<p className="text-sm text-text-muted leading-relaxed max-w-2xl">
Test and validate your LLM endpoints by running predefined evaluation suites. Each
suite contains test cases that send real prompts through OmniRoute and compare

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import Image from "next/image";
import { parseQuotaData, calculatePercentage, normalizePlanTier } from "./utils";
@@ -81,6 +83,7 @@ function formatCountdown(resetAt) {
}
export default function ProviderLimits() {
const t = useTranslations("usage");
const [connections, setConnections] = useState([]);
const [quotaData, setQuotaData] = useState({});
const [loading, setLoading] = useState({});
@@ -286,7 +289,7 @@ export default function ProviderLimits() {
<Card padding="lg">
<div className="text-center py-12">
<span className="material-symbols-outlined text-[64px] opacity-15">cloud_off</span>
<h3 className="mt-4 text-lg font-semibold text-text-main">No Providers Connected</h3>
<h3 className="mt-4 text-lg font-semibold text-text-main">{t("noProviders")}</h3>
<p className="mt-2 text-sm text-text-muted max-w-[400px] mx-auto">
Connect to providers with OAuth to track your API quota limits and usage.
</p>
@@ -300,7 +303,7 @@ export default function ProviderLimits() {
{/* Header */}
<div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-3">
<h2 className="text-lg font-semibold text-text-main m-0">Provider Limits</h2>
<h2 className="text-lg font-semibold text-text-main m-0">{t("providerLimits")}</h2>
<span className="text-[13px] text-text-muted">
{visibleConnections.length} account{visibleConnections.length !== 1 ? "s" : ""}
{visibleConnections.length !== sortedConnections.length
@@ -373,10 +376,10 @@ export default function ProviderLimits() {
className="items-center px-4 py-2.5 border-b border-white/[0.06] text-[11px] font-semibold uppercase tracking-wider text-text-muted"
style={{ display: "grid", gridTemplateColumns: "280px 1fr 100px 48px" }}
>
<div>Account</div>
<div>Model Quotas</div>
<div className="text-center">Last Used</div>
<div className="text-center">Actions</div>
<div>{t(">account</")}</div>
<div>{t("modelQuotas")}</div>
<div className="text-center">{t(">lastUsed</")}</div>
<div className="text-center">{t(">actions</")}</div>
</div>
{visibleConnections.map((conn, idx) => {
@@ -491,7 +494,7 @@ export default function ProviderLimits() {
);
})
) : (
<div className="text-xs text-text-muted italic">No quota data</div>
<div className="text-xs text-text-muted italic">{t("noQuotaData")}</div>
)}
</div>

View File

@@ -1,9 +1,12 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useCallback } from "react";
import { Card } from "@/shared/components";
export default function RateLimitStatus() {
const t = useTranslations("usage");
const [data, setData] = useState({ lockouts: [], cacheStats: null });
const [loading, setLoading] = useState(true);
@@ -31,7 +34,7 @@ export default function RateLimitStatus() {
return (
<div className="flex flex-col gap-4">
{/* Model Lockouts */}
{/* {t("modelLockouts")} */}
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-orange-500/10 text-orange-500">
@@ -55,7 +58,7 @@ export default function RateLimitStatus() {
<span className="material-symbols-outlined text-[32px] mb-2 block opacity-40">
lock_open
</span>
<p className="text-sm">No models currently locked</p>
<p className="text-sm">{t("noLockouts")}</p>
</div>
) : (
<div className="flex flex-col gap-2">

View File

@@ -1,9 +1,12 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useCallback } from "react";
import { Card } from "@/shared/components";
export default function SessionsTab() {
const t = useTranslations("usage");
const [data, setData] = useState({ count: 0, sessions: [] });
const [loading, setLoading] = useState(true);
@@ -11,7 +14,8 @@ export default function SessionsTab() {
try {
const res = await fetch("/api/sessions");
if (res.ok) setData(await res.json());
} catch {} finally {
} catch {
} finally {
setLoading(false);
}
}, []);
@@ -37,15 +41,15 @@ export default function SessionsTab() {
</span>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold">Active Sessions</h3>
<p className="text-sm text-text-muted">Tracked via request fingerprinting Auto-refresh 5s</p>
<h3 className="text-lg font-semibold">{t("activeSessions")}</h3>
<p className="text-sm text-text-muted">
Tracked via request fingerprinting Auto-refresh 5s
</p>
</div>
<div className="flex items-center gap-2">
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/20">
<span className="w-2 h-2 rounded-full bg-cyan-500 animate-pulse" />
<span className="text-sm font-semibold tabular-nums text-cyan-400">
{data.count}
</span>
<span className="text-sm font-semibold tabular-nums text-cyan-400">{data.count}</span>
</span>
</div>
</div>
@@ -55,23 +59,34 @@ export default function SessionsTab() {
<span className="material-symbols-outlined text-[40px] mb-2 block opacity-40">
fingerprint
</span>
<p className="text-sm">No active sessions</p>
<p className="text-xs mt-1">Sessions appear as requests flow through the proxy</p>
<p className="text-sm">{t("noSessions")}</p>
<p className="text-xs mt-1">{t("sessionsHint")}</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border/30">
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">Session</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">Age</th>
<th className="text-right py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">Requests</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">Connection</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
{t(">session</")}
</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
{t(">age</")}
</th>
<th className="text-right py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
{t(">requests</")}
</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
{t(">connection</")}
</th>
</tr>
</thead>
<tbody>
{data.sessions.map((s) => (
<tr key={s.sessionId} className="border-b border-border/10 hover:bg-surface/20 transition-colors">
<tr
key={s.sessionId}
className="border-b border-border/10 hover:bg-surface/20 transition-colors"
>
<td className="py-2.5 px-3">
<span className="font-mono text-xs px-2 py-1 rounded bg-surface/40 text-text-muted">
{s.sessionId.slice(0, 12)}
@@ -83,7 +98,9 @@ export default function SessionsTab() {
</td>
<td className="py-2.5 px-3">
{s.connectionId ? (
<span className="text-xs font-mono text-cyan-400">{s.connectionId.slice(0, 10)}</span>
<span className="text-xs font-mono text-cyan-400">
{s.connectionId.slice(0, 10)}
</span>
) : (
<span className="text-text-muted/40"></span>
)}

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import { Suspense, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
@@ -94,8 +96,8 @@ function CallbackContent() {
progress_activity
</span>
</div>
<h1 className="text-xl font-semibold mb-2">Processing...</h1>
<p className="text-text-muted">Please wait while we complete the authorization.</p>
<h1 className="text-xl font-semibold mb-2">{t("processing")}</h1>
<p className="text-text-muted">{t("pleaseWait")}</p>
</>
)}
@@ -106,7 +108,7 @@ function CallbackContent() {
check_circle
</span>
</div>
<h1 className="text-xl font-semibold mb-2">Authorization Successful!</h1>
<h1 className="text-xl font-semibold mb-2">{t("authSuccess")}</h1>
<p className="text-text-muted">
{status === "success"
? "This window will close automatically..."
@@ -120,7 +122,7 @@ function CallbackContent() {
<div className="size-16 mx-auto mb-4 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
<span className="material-symbols-outlined text-3xl text-yellow-600">info</span>
</div>
<h1 className="text-xl font-semibold mb-2">Copy This URL</h1>
<h1 className="text-xl font-semibold mb-2">{t("copyUrl")}</h1>
<p className="text-text-muted mb-4">
Please copy the URL from the address bar and paste it in the application.
</p>
@@ -141,6 +143,7 @@ function CallbackContent() {
* Receives callback from OAuth providers and sends data back via multiple methods
*/
export default function CallbackPage() {
const t = useTranslations("auth");
return (
<Suspense
fallback={

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
/**
* 403 Forbidden Page — Phase 8.1
*
@@ -12,6 +14,7 @@
import Link from "next/link";
export default function ForbiddenPage() {
const t = useTranslations("auth");
return (
<div className="flex flex-col items-center justify-center min-h-screen p-6 bg-[var(--bg-primary,#0a0a0f)] text-[var(--text-primary,#e0e0e0)] text-center">
<div
@@ -24,7 +27,7 @@ export default function ForbiddenPage() {
>
403
</div>
<h1 className="text-2xl font-semibold mb-2">Access Denied</h1>
<h1 className="text-2xl font-semibold mb-2">{t("accessDenied")}</h1>
<p className="text-[15px] text-[var(--text-secondary,#888)] max-w-[400px] leading-relaxed mb-8">
You don&apos;t have permission to access this resource. Check your API key or contact the
administrator.

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
/**
* Forgot Password Page — Phase 8.2
*
@@ -12,12 +14,13 @@ import Link from "next/link";
import { Card } from "@/shared/components";
export default function ForgotPasswordPage() {
const t = useTranslations("auth");
return (
<div className="min-h-screen flex items-center justify-center bg-bg p-4">
<div className="w-full max-w-lg">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-primary mb-2">Reset Password</h1>
<p className="text-text-muted">Choose a method to recover access to your dashboard</p>
<h1 className="text-3xl font-bold text-primary mb-2">{t(">resetPassword</")}</h1>
<p className="text-text-muted">{t("resetDescription")}</p>
</div>
{/* Method 1: CLI Reset */}
@@ -53,7 +56,7 @@ export default function ForgotPasswordPage() {
Delete the password from the database and set a new one on startup:
</p>
<ol className="text-sm text-text-muted space-y-2 list-decimal list-inside mb-3">
<li>Stop the OmniRoute server</li>
<li>{t("stopServer")}</li>
<li>
Set a new password in your{" "}
<code className="bg-black/30 px-1 rounded text-text-main">.env</code> file:

View File

@@ -1,10 +1,13 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect } from "react";
import { Button, Input } from "@/shared/components";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const t = useTranslations("auth");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
@@ -101,7 +104,7 @@ export default function LoginPage() {
rocket_launch
</span>
</div>
<h1 className="text-3xl font-bold text-text-main tracking-tight">Welcome</h1>
<h1 className="text-3xl font-bold text-text-main tracking-tight">{t(">welcome</")}</h1>
<p className="text-text-muted mt-2">
Let&apos;s get your OmniRoute instance configured
</p>
@@ -124,7 +127,7 @@ export default function LoginPage() {
</div>
<p className="text-center text-xs text-text-muted/60 mt-8">
OmniRoute Unified AI API Proxy
OmniRoute {t("unifiedProxy")}
</p>
</div>
</div>
@@ -146,7 +149,7 @@ export default function LoginPage() {
<h1 className="text-3xl font-bold text-text-main tracking-tight">
Secure Your Instance
</h1>
<p className="text-text-muted mt-2">Password protection is not enabled</p>
<p className="text-text-muted mt-2">{t("passwordNotEnabled")}</p>
</div>
<div className="bg-surface border border-border rounded-2xl p-8 shadow-soft">
@@ -186,13 +189,13 @@ export default function LoginPage() {
</div>
<span className="text-xl font-semibold text-text-main tracking-tight">OmniRoute</span>
</div>
<h1 className="text-2xl font-bold text-text-main tracking-tight">Sign in</h1>
<p className="text-text-muted mt-1.5">Enter your password to continue</p>
<h1 className="text-2xl font-bold text-text-main tracking-tight">{t(">signIn</")}</h1>
<p className="text-text-muted mt-1.5">{t("enterPassword")}</p>
</div>
<form onSubmit={handleLogin} className="space-y-5">
<div className="space-y-2">
<label className="text-sm font-medium text-text-main">Password</label>
<label className="text-sm font-medium text-text-main">{t(">password</")}</label>
<Input
type="password"
placeholder="Enter your password"

View File

@@ -22,7 +22,41 @@
"disabled": "Disabled",
"active": "Active",
"inactive": "Inactive",
"noData": "No data available"
"noData": "No data available",
"configure": "Configure",
"manage": "Manage",
"name": "Name",
"actions": "Actions",
"status": "Status",
"type": "Type",
"model": "Model",
"models": "models",
"provider": "Provider",
"account": "Account",
"time": "Time",
"details": "Details",
"created": "Created",
"lastUsed": "Last Used",
"loadMore": "Load More",
"noResults": "No results found",
"reloadPage": "Reload Page",
"connected": "Connected",
"disconnected": "Disconnected",
"notConfigured": "Not configured",
"testConnection": "Test Connection",
"enable": "Enable",
"disable": "Disable",
"columns": "Columns",
"newest": "Newest",
"oldest": "Oldest",
"all": "All",
"none": "None",
"yes": "Yes",
"no": "No",
"warning": "Warning",
"note": "Note",
"free": "Free",
"skipToContent": "Skip to content"
},
"sidebar": {
"home": "Home",
@@ -47,7 +81,14 @@
"restart": "Restart",
"shutdownConfirm": "Shut down OmniRoute?",
"restartConfirm": "Restart OmniRoute?",
"version": "v{version}"
"version": "v{version}",
"debug": "Debug",
"system": "System",
"help": "Help",
"serverDisconnected": "Server Disconnected",
"serverDisconnectedMsg": "The proxy server has been stopped or is restarting.",
"expandSidebar": "Expand sidebar",
"collapseSidebar": "Collapse sidebar"
},
"header": {
"logout": "Logout",
@@ -70,5 +111,428 @@
"settingsDescription": "Manage your preferences",
"openaiCompatible": "OpenAI Compatible",
"anthropicCompatible": "Anthropic Compatible"
},
"home": {
"quickStart": "Quick Start",
"quickStartDesc": "Get up and running in 4 steps. Connect providers, route models, monitor everything.",
"fullDocs": "Full Docs",
"step1Title": "1. Create API key",
"step1Desc": "Go to {endpoint} → Registered Keys. Generate one key per environment.",
"step2Title": "2. Connect providers",
"step2Desc": "Add accounts in {providers}. Supports OAuth, API Key, and free tiers.",
"step3Title": "3. Point your client",
"step3Desc": "Set base URL to {url} in your IDE or API client.",
"step4Title": "4. Monitor & optimize",
"step4Desc": "Track tokens, cost and errors in {logs} and {analytics}.",
"providersOverview": "Providers Overview",
"configuredOf": "{configured} configured of {total} available providers",
"noModelsAvailable": "No models available for this provider.",
"configureFirst": "Configure a connection first in {providers}",
"configureProvider": "Configure Provider",
"modelAvailable": "{count} model available",
"modelsAvailable": "{count} models available",
"connectionsActive": "{count} connection active",
"connectionsActivePlural": "{count} connections active",
"copyModelName": "Copy model name",
"documentation": "Documentation",
"healthMonitor": "Health Monitor",
"reportIssue": "Report issue",
"activeError": "{active} active · {errors} error"
},
"analytics": {
"title": "Analytics",
"overviewDescription": "Monitor your API usage patterns, token consumption, costs, and activity trends across all providers and models.",
"evalsDescription": "Run evaluation suites to test and validate your LLM endpoints. Compare model quality, detect regressions, and benchmark latency.",
"overview": "Overview",
"evals": "Evals"
},
"apiManager": {
"title": "API Keys",
"createKey": "Create API Key",
"key": "Key",
"revokeKey": "Revoke Key",
"revokeConfirm": "Are you sure you want to revoke this API key?",
"noKeys": "No API keys created yet",
"noKeysDesc": "Create an API key to authenticate with OmniRoute",
"keyLabel": "Key Label",
"permissions": "Permissions",
"expiresAt": "Expires",
"never": "Never",
"revoke": "Revoke",
"showKey": "Show Key",
"hideKey": "Hide Key",
"copyKey": "Copy API Key",
"allModels": "All Models",
"selectedModels": "Selected Models",
"readOnly": "Read Only",
"fullAccess": "Full Access"
},
"auditLog": {
"title": "Audit Log",
"searchPlaceholder": "Search actions...",
"action": "Action",
"actor": "Actor",
"target": "Target",
"ipAddress": "IP Address",
"timestamp": "Timestamp",
"noEntries": "No audit entries found",
"filterByAction": "Filter by action"
},
"cliTools": {
"title": "CLI Tools",
"mapModels": "Map Models",
"testConnection": "Test Connection",
"connectionStatus": "Connection Status",
"configureEndpoint": "Configure Endpoint",
"instructions": "Instructions",
"modelMapping": "Model Mapping",
"baseUrl": "Base URL",
"apiKey": "API Key"
},
"combos": {
"title": "Combos",
"createCombo": "Create Combo",
"editCombo": "Edit Combo",
"deleteCombo": "Delete Combo",
"noModels": "No models",
"noModelsYet": "No models added yet",
"addModel": "Add Model",
"routingStrategy": "Routing Strategy",
"maxRetries": "Max Retries",
"timeout": "Timeout (ms)",
"healthcheck": "Healthcheck",
"priority": "Priority",
"fallback": "Fallback",
"roundRobin": "Round Robin",
"random": "Random",
"leastLatency": "Least Latency",
"comboName": "Combo Name",
"comboNamePlaceholder": "e.g. my-smart-combo",
"deleteConfirm": "Are you sure you want to delete this combo?"
},
"costs": {
"title": "Costs",
"totalCost": "Total Cost",
"breakdown": "Cost Breakdown",
"noData": "No cost data",
"byModel": "By Model",
"byProvider": "By Provider"
},
"endpoint": {
"title": "API Endpoint",
"available": "Available Endpoints",
"cloudProxy": "Cloud Proxy",
"disableConfirm": "Are you sure you want to disable cloud proxy?",
"baseUrl": "Base URL",
"apiKeyLabel": "API Key",
"registeredKeys": "Registered Keys",
"chatCompletions": "Chat Completions",
"responses": "Responses",
"listModels": "List Models"
},
"health": {
"title": "System Health",
"healthy": "Healthy",
"degraded": "Degraded",
"down": "Down",
"uptime": "Uptime",
"memory": "Memory",
"cpu": "CPU",
"database": "Database",
"lastCheck": "Last Check",
"providerHealth": "Provider Health",
"systemMetrics": "System Metrics",
"tokenHealth": "Token Health",
"refreshAll": "Refresh All",
"checkNow": "Check Now"
},
"limits": {
"title": "Limits & Quotas",
"rateLimit": "Rate Limit",
"remaining": "Remaining",
"requestsPerMinute": "Requests/min",
"tokensPerMinute": "Tokens/min",
"dailyLimit": "Daily Limit"
},
"logs": {
"title": "Logs",
"requestLogs": "Request Logs",
"proxyLogs": "Proxy Logs",
"auditLog": "Audit Log",
"console": "Console"
},
"onboarding": {
"welcome": "Welcome to OmniRoute",
"setPassword": "Set Password",
"addProvider": "Add your first provider",
"getStarted": "Get Started",
"skip": "Skip",
"passwordLabel": "Password",
"confirmPassword": "Confirm Password",
"setupComplete": "Setup Complete!",
"goToDashboard": "Go to Dashboard"
},
"providers": {
"title": "Providers",
"addProvider": "Add Provider",
"editProvider": "Edit Provider",
"deleteProvider": "Delete Provider",
"noProviders": "No providers configured",
"modelAvailability": "Model Availability",
"accounts": "Accounts",
"newAccount": "New Account",
"deleteConfirm": "Are you sure you want to delete this provider?",
"testing": "Testing...",
"testSuccess": "Connection successful",
"testFailed": "Connection failed",
"available": "Available",
"unavailable": "Unavailable",
"unknown": "Unknown"
},
"settings": {
"title": "Settings",
"general": "General",
"security": "Security",
"appearance": "Appearance",
"routing": "Routing",
"cache": "Cache",
"resilience": "Resilience",
"systemPrompt": "System Prompt",
"thinkingBudget": "Thinking Budget",
"proxy": "Proxy",
"pricing": "Pricing",
"storage": "Storage",
"policies": "Policies",
"ipFilter": "IP Filter",
"comboDefaults": "Combo Defaults",
"fallbackChains": "Fallback Chains",
"changePassword": "Change Password",
"enablePassword": "Enable Password",
"darkMode": "Dark Mode",
"lightMode": "Light Mode",
"systemTheme": "System Theme",
"enableCache": "Enable Cache",
"cacheTTL": "Cache TTL",
"maxCacheSize": "Max Cache Size",
"clearCache": "Clear Cache",
"cacheHits": "Cache Hits",
"cacheMisses": "Cache Misses",
"hitRate": "Hit Rate",
"cacheEntries": "Cache Entries",
"circuitBreaker": "Circuit Breaker",
"retryPolicy": "Retry Policy",
"maxRetries": "Max Retries",
"retryDelay": "Retry Delay",
"timeoutMs": "Timeout (ms)",
"enableSystemPrompt": "Enable System Prompt",
"systemPromptText": "System Prompt Text",
"enableThinking": "Enable Thinking",
"maxThinkingTokens": "Max Thinking Tokens",
"enableProxy": "Enable Proxy",
"proxyUrl": "Proxy URL",
"pricingRates": "Pricing Rates Format",
"currentPricing": "Current Pricing Overview",
"loadingPricing": "Loading pricing data...",
"noPricing": "No pricing data available",
"input": "Input",
"output": "Output",
"cached": "Cached",
"reasoning": "Reasoning",
"cacheCreation": "Cache Creation",
"customPricing": "Custom Pricing",
"databaseSize": "Database Size",
"backupDb": "Backup Database",
"restoreDb": "Restore Database",
"exportData": "Export Data",
"importData": "Import Data",
"clearData": "Clear All Data",
"clearDataConfirm": "This will permanently delete all data. Are you sure?",
"enableRequestLogs": "Enable Request Logs",
"logRetention": "Log Retention",
"ipWhitelist": "IP Whitelist",
"ipBlacklist": "IP Blacklist",
"addIP": "Add IP",
"savedSuccessfully": "Settings saved successfully"
},
"translator": {
"title": "Translator",
"realtime": "Real-Time Translation Activity",
"chatTester": "Chat Tester",
"testBench": "Test Bench",
"recentTranslations": "Recent Translations",
"noTranslations": "No translations yet",
"source": "Source",
"target": "Target",
"latency": "Latency",
"formatConverter": "Format Converter",
"input": "Input",
"output": "Output",
"exampleTemplates": "Example Templates",
"compatibilityTester": "Compatibility Tester",
"compatibilityReport": "Compatibility Report",
"pipelineDebugger": "Pipeline Debugger",
"translationPipeline": "Translation Pipeline",
"pipelineVisualization": "Pipeline visualization"
},
"usage": {
"title": "Usage",
"budgetManagement": "Budget Management",
"apiKey": "API Key",
"thisMonth": "This Month",
"setLimits": "Set Limits",
"totalRequests": "Total requests",
"noDataYet": "No data yet",
"entries": "Entries",
"hitRate": "Hit Rate",
"hitsMisses": "Hits / Misses",
"circuitBreakers": "Circuit Breakers",
"lockedIPs": "Locked IPs",
"howItWorks": "How It Works",
"define": "Define",
"run": "Run",
"evaluate": "Evaluate",
"evalSuites": "Evaluation Suites",
"modelEvals": "Model Evaluations",
"modelLockouts": "Model Lockouts",
"noLockouts": "No models currently locked",
"activeSessions": "Active Sessions",
"noSessions": "No active sessions",
"sessionsHint": "Sessions appear as requests flow through the proxy",
"session": "Session",
"age": "Age",
"requests": "Requests",
"connection": "Connection",
"providerLimits": "Provider Limits",
"noProviders": "No Providers Connected",
"modelQuotas": "Model Quotas",
"noQuotaData": "No quota data",
"noQuotaDataAvailable": "No quota data available"
},
"modals": {
"waitingAuth": "Waiting for Authorization",
"verificationUrl": "Verification URL",
"yourCode": "Your Code",
"remoteAccess": "Remote access:",
"connectedSuccess": "Connected Successfully!",
"connectionFailed": "Connection Failed",
"chooseAuthMethod": "Choose your authentication method:",
"awsBuilderId": "AWS Builder ID",
"awsIamIdentity": "AWS IAM Identity Center",
"googleAccount": "Google Account",
"githubAccount": "GitHub Account",
"importToken": "Import Token",
"pasteToken": "Paste refresh token from Kiro IDE.",
"awsRegion": "AWS Region",
"autoDetecting": "Auto-detecting tokens...",
"readingFromCache": "Reading from AWS SSO cache",
"readingFromCursor": "Reading from Cursor IDE database",
"initializing": "Initializing...",
"pricingConfig": "Pricing Configuration",
"loadingPricing": "Loading pricing data...",
"pricingRatesFormat": "Pricing Rates Format",
"noPricingData": "No pricing data available",
"noModelsFound": "No models found"
},
"loggers": {
"allProviders": "All Providers",
"allModels": "All Models",
"allAccounts": "All Accounts",
"allApiKeys": "All API Keys",
"allTypes": "All Types",
"allLevels": "All Levels",
"modelAZ": "Model A-Z",
"modelZA": "Model Z-A",
"loadingLogs": "Loading logs...",
"loadingProxyLogs": "Loading proxy logs...",
"noLogEntries": "No log entries found",
"noPayloadData": "No payload data available for this log entry.",
"proxyEvent": "Proxy Event",
"proxy": "Proxy",
"level": "Level",
"directNative": "Direct (native)",
"combo": "Combo",
"inputTokens": "I:",
"outputTokens": "O:"
},
"stats": {
"usageOverview": "Usage Overview",
"outputTokens": "Output Tokens",
"totalCost": "Total Cost",
"usageByModel": "Usage by Model",
"usageByAccount": "Usage by Account",
"failedToLoad": "Failed to load usage statistics.",
"tokenHealth": "Token Health",
"totalOAuth": "Total OAuth",
"healthy": "Healthy",
"errored": "Errored",
"lastCheck": "Last check",
"noData": "No data",
"share": "Share",
"unableToLoad": "Unable to load system metrics",
"product": "Product",
"resources": "Resources",
"company": "Company"
},
"auth": {
"welcome": "Welcome",
"signIn": "Sign in",
"enterPassword": "Enter your password to continue",
"password": "Password",
"unifiedProxy": "Unified AI API Proxy",
"passwordNotEnabled": "Password protection is not enabled",
"resetPassword": "Reset Password",
"resetDescription": "Choose a method to recover access to your dashboard",
"stopServer": "Stop the OmniRoute server",
"processing": "Processing...",
"pleaseWait": "Please wait while we complete the authorization.",
"authSuccess": "Authorization Successful!",
"copyUrl": "Copy This URL",
"accessDenied": "Access Denied",
"forgotPassword": "Forgot password?"
},
"landing": {
"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:",
"getStarted": "Get Started",
"product": "Product",
"resources": "Resources",
"legal": "Legal",
"interactiveDiagram": "Interactive diagram visible on desktop"
},
"docs": {
"title": "Documentation",
"quickStart": "Quick Start",
"features": "Features",
"supportedProviders": "Supported Providers",
"commonUseCases": "Common Use Cases",
"clientCompatibility": "Client Compatibility",
"apiReference": "API Reference",
"method": "Method",
"path": "Path",
"notes": "Notes",
"modelPrefixes": "Model Prefixes",
"prefix": "Prefix",
"troubleshooting": "Troubleshooting",
"supportsChat": "Supports both chat and responses endpoints.",
"oauthAutoRefresh": "OAuth connection with automatic token refresh.",
"fullStreaming": "Full streaming support for all models."
},
"legal": {
"privacyPolicy": "Privacy Policy",
"termsOfService": "Terms of Service",
"providerConfigurations": "Provider configurations",
"apiKeys": "API keys",
"usageLogs": "Usage logs",
"applicationSettings": "Application settings",
"viewExportAnalytics": "View and export usage analytics",
"clearHistory": "Clear usage history at any time",
"configureRetention": "Configure log retention policies",
"backupRestore": "Back up and restore your database"
}
}

View File

@@ -22,7 +22,41 @@
"disabled": "Desativado",
"active": "Ativo",
"inactive": "Inativo",
"noData": "Nenhum dado disponível"
"noData": "Nenhum dado disponível",
"configure": "Configurar",
"manage": "Gerenciar",
"name": "Nome",
"actions": "Ações",
"status": "Status",
"type": "Tipo",
"model": "Modelo",
"models": "modelos",
"provider": "Provedor",
"account": "Conta",
"time": "Tempo",
"details": "Detalhes",
"created": "Criado",
"lastUsed": "Último Uso",
"loadMore": "Carregar Mais",
"noResults": "Nenhum resultado encontrado",
"reloadPage": "Recarregar Página",
"connected": "Conectado",
"disconnected": "Desconectado",
"notConfigured": "Não configurado",
"testConnection": "Testar Conexão",
"enable": "Ativar",
"disable": "Desativar",
"columns": "Colunas",
"newest": "Mais Recente",
"oldest": "Mais Antigo",
"all": "Todos",
"none": "Nenhum",
"yes": "Sim",
"no": "Não",
"warning": "Aviso",
"note": "Nota",
"free": "Gratuito",
"skipToContent": "Pular para conteúdo"
},
"sidebar": {
"home": "Início",
@@ -47,7 +81,14 @@
"restart": "Reiniciar",
"shutdownConfirm": "Desligar o OmniRoute?",
"restartConfirm": "Reiniciar o OmniRoute?",
"version": "v{version}"
"version": "v{version}",
"debug": "Depuração",
"system": "Sistema",
"help": "Ajuda",
"serverDisconnected": "Servidor Desconectado",
"serverDisconnectedMsg": "O servidor proxy foi parado ou está reiniciando.",
"expandSidebar": "Expandir barra lateral",
"collapseSidebar": "Recolher barra lateral"
},
"header": {
"logout": "Sair",
@@ -70,5 +111,428 @@
"settingsDescription": "Gerencie suas preferências",
"openaiCompatible": "Compatível com OpenAI",
"anthropicCompatible": "Compatível com Anthropic"
},
"home": {
"quickStart": "Início Rápido",
"quickStartDesc": "Comece em 4 passos. Conecte provedores, roteie modelos, monitore tudo.",
"fullDocs": "Docs Completa",
"step1Title": "1. Criar chave de API",
"step1Desc": "Vá em {endpoint} → Chaves Registradas. Gere uma chave por ambiente.",
"step2Title": "2. Conectar provedores",
"step2Desc": "Adicione contas em {providers}. Suporta OAuth, API Key e planos gratuitos.",
"step3Title": "3. Apontar seu cliente",
"step3Desc": "Defina a URL base como {url} no seu IDE ou cliente de API.",
"step4Title": "4. Monitorar e otimizar",
"step4Desc": "Acompanhe tokens, custos e erros em {logs} e {analytics}.",
"providersOverview": "Visão Geral dos Provedores",
"configuredOf": "{configured} configurados de {total} provedores disponíveis",
"noModelsAvailable": "Nenhum modelo disponível para este provedor.",
"configureFirst": "Configure uma conexão primeiro em {providers}",
"configureProvider": "Configurar Provedor",
"modelAvailable": "{count} modelo disponível",
"modelsAvailable": "{count} modelos disponíveis",
"connectionsActive": "{count} conexão ativa",
"connectionsActivePlural": "{count} conexões ativas",
"copyModelName": "Copiar nome do modelo",
"documentation": "Documentação",
"healthMonitor": "Monitor de Saúde",
"reportIssue": "Reportar problema",
"activeError": "{active} ativo · {errors} erro"
},
"analytics": {
"title": "Análises",
"overviewDescription": "Monitore padrões de uso da API, consumo de tokens, custos e tendências de atividade em todos os provedores e modelos.",
"evalsDescription": "Execute suítes de avaliação para testar e validar seus endpoints LLM. Compare qualidade de modelos, detecte regressões e faça benchmarks de latência.",
"overview": "Visão Geral",
"evals": "Avaliações"
},
"apiManager": {
"title": "Chaves de API",
"createKey": "Criar Chave de API",
"key": "Chave",
"revokeKey": "Revogar Chave",
"revokeConfirm": "Tem certeza que deseja revogar esta chave de API?",
"noKeys": "Nenhuma chave de API criada",
"noKeysDesc": "Crie uma chave de API para autenticar com o OmniRoute",
"keyLabel": "Rótulo da Chave",
"permissions": "Permissões",
"expiresAt": "Expira",
"never": "Nunca",
"revoke": "Revogar",
"showKey": "Mostrar Chave",
"hideKey": "Ocultar Chave",
"copyKey": "Copiar Chave de API",
"allModels": "Todos os Modelos",
"selectedModels": "Modelos Selecionados",
"readOnly": "Somente Leitura",
"fullAccess": "Acesso Total"
},
"auditLog": {
"title": "Log de Auditoria",
"searchPlaceholder": "Buscar ações...",
"action": "Ação",
"actor": "Autor",
"target": "Alvo",
"ipAddress": "Endereço IP",
"timestamp": "Data/Hora",
"noEntries": "Nenhum registro de auditoria",
"filterByAction": "Filtrar por ação"
},
"cliTools": {
"title": "Ferramentas CLI",
"mapModels": "Mapear Modelos",
"testConnection": "Testar Conexão",
"connectionStatus": "Status da Conexão",
"configureEndpoint": "Configurar Endpoint",
"instructions": "Instruções",
"modelMapping": "Mapeamento de Modelos",
"baseUrl": "URL Base",
"apiKey": "Chave de API"
},
"combos": {
"title": "Combos",
"createCombo": "Criar Combo",
"editCombo": "Editar Combo",
"deleteCombo": "Excluir Combo",
"noModels": "Sem modelos",
"noModelsYet": "Nenhum modelo adicionado",
"addModel": "Adicionar Modelo",
"routingStrategy": "Estratégia de Roteamento",
"maxRetries": "Máximo de Tentativas",
"timeout": "Timeout (ms)",
"healthcheck": "Verificação de Saúde",
"priority": "Prioridade",
"fallback": "Fallback",
"roundRobin": "Round Robin",
"random": "Aleatório",
"leastLatency": "Menor Latência",
"comboName": "Nome do Combo",
"comboNamePlaceholder": "ex: meu-combo-inteligente",
"deleteConfirm": "Tem certeza que deseja excluir este combo?"
},
"costs": {
"title": "Custos",
"totalCost": "Custo Total",
"breakdown": "Detalhamento de Custos",
"noData": "Sem dados de custo",
"byModel": "Por Modelo",
"byProvider": "Por Provedor"
},
"endpoint": {
"title": "Endpoint da API",
"available": "Endpoints Disponíveis",
"cloudProxy": "Proxy na Nuvem",
"disableConfirm": "Tem certeza que deseja desativar o proxy na nuvem?",
"baseUrl": "URL Base",
"apiKeyLabel": "Chave de API",
"registeredKeys": "Chaves Registradas",
"chatCompletions": "Chat Completions",
"responses": "Respostas",
"listModels": "Listar Modelos"
},
"health": {
"title": "Saúde do Sistema",
"healthy": "Saudável",
"degraded": "Degradado",
"down": "Offline",
"uptime": "Tempo Ativo",
"memory": "Memória",
"cpu": "CPU",
"database": "Banco de Dados",
"lastCheck": "Última Verificação",
"providerHealth": "Saúde dos Provedores",
"systemMetrics": "Métricas do Sistema",
"tokenHealth": "Saúde dos Tokens",
"refreshAll": "Atualizar Tudo",
"checkNow": "Verificar Agora"
},
"limits": {
"title": "Limites e Cotas",
"rateLimit": "Limite de Taxa",
"remaining": "Restante",
"requestsPerMinute": "Requisições/min",
"tokensPerMinute": "Tokens/min",
"dailyLimit": "Limite Diário"
},
"logs": {
"title": "Logs",
"requestLogs": "Logs de Requisições",
"proxyLogs": "Logs do Proxy",
"auditLog": "Log de Auditoria",
"console": "Console"
},
"onboarding": {
"welcome": "Bem-vindo ao OmniRoute",
"setPassword": "Definir Senha",
"addProvider": "Adicione seu primeiro provedor",
"getStarted": "Começar",
"skip": "Pular",
"passwordLabel": "Senha",
"confirmPassword": "Confirmar Senha",
"setupComplete": "Configuração Concluída!",
"goToDashboard": "Ir para o Painel"
},
"providers": {
"title": "Provedores",
"addProvider": "Adicionar Provedor",
"editProvider": "Editar Provedor",
"deleteProvider": "Excluir Provedor",
"noProviders": "Nenhum provedor configurado",
"modelAvailability": "Disponibilidade de Modelos",
"accounts": "Contas",
"newAccount": "Nova Conta",
"deleteConfirm": "Tem certeza que deseja excluir este provedor?",
"testing": "Testando...",
"testSuccess": "Conexão bem-sucedida",
"testFailed": "Falha na conexão",
"available": "Disponível",
"unavailable": "Indisponível",
"unknown": "Desconhecido"
},
"settings": {
"title": "Configurações",
"general": "Geral",
"security": "Segurança",
"appearance": "Aparência",
"routing": "Roteamento",
"cache": "Cache",
"resilience": "Resiliência",
"systemPrompt": "Prompt do Sistema",
"thinkingBudget": "Orçamento de Raciocínio",
"proxy": "Proxy",
"pricing": "Preços",
"storage": "Armazenamento",
"policies": "Políticas",
"ipFilter": "Filtro de IP",
"comboDefaults": "Padrões de Combo",
"fallbackChains": "Cadeias de Fallback",
"changePassword": "Alterar Senha",
"enablePassword": "Ativar Senha",
"darkMode": "Modo Escuro",
"lightMode": "Modo Claro",
"systemTheme": "Tema do Sistema",
"enableCache": "Ativar Cache",
"cacheTTL": "TTL do Cache",
"maxCacheSize": "Tamanho Máximo do Cache",
"clearCache": "Limpar Cache",
"cacheHits": "Acertos de Cache",
"cacheMisses": "Erros de Cache",
"hitRate": "Taxa de Acerto",
"cacheEntries": "Entradas no Cache",
"circuitBreaker": "Disjuntor",
"retryPolicy": "Política de Retentativa",
"maxRetries": "Máximo de Tentativas",
"retryDelay": "Intervalo de Retentativa",
"timeoutMs": "Timeout (ms)",
"enableSystemPrompt": "Ativar Prompt do Sistema",
"systemPromptText": "Texto do Prompt do Sistema",
"enableThinking": "Ativar Raciocínio",
"maxThinkingTokens": "Máximo de Tokens de Raciocínio",
"enableProxy": "Ativar Proxy",
"proxyUrl": "URL do Proxy",
"pricingRates": "Formato de Taxas de Preço",
"currentPricing": "Visão Geral de Preços Atual",
"loadingPricing": "Carregando dados de preços...",
"noPricing": "Nenhum dado de preço disponível",
"input": "Entrada",
"output": "Saída",
"cached": "Em Cache",
"reasoning": "Raciocínio",
"cacheCreation": "Criação de Cache",
"customPricing": "Preços Personalizados",
"databaseSize": "Tamanho do Banco de Dados",
"backupDb": "Backup do Banco de Dados",
"restoreDb": "Restaurar Banco de Dados",
"exportData": "Exportar Dados",
"importData": "Importar Dados",
"clearData": "Limpar Todos os Dados",
"clearDataConfirm": "Isso excluirá permanentemente todos os dados. Tem certeza?",
"enableRequestLogs": "Ativar Logs de Requisição",
"logRetention": "Retenção de Logs",
"ipWhitelist": "Lista de IPs Permitidos",
"ipBlacklist": "Lista de IPs Bloqueados",
"addIP": "Adicionar IP",
"savedSuccessfully": "Configurações salvas com sucesso"
},
"translator": {
"title": "Tradutor",
"realtime": "Atividade de Tradução em Tempo Real",
"chatTester": "Testador de Chat",
"testBench": "Bancada de Testes",
"recentTranslations": "Traduções Recentes",
"noTranslations": "Nenhuma tradução ainda",
"source": "Origem",
"target": "Destino",
"latency": "Latência",
"formatConverter": "Conversor de Formato",
"input": "Entrada",
"output": "Saída",
"exampleTemplates": "Modelos de Exemplo",
"compatibilityTester": "Testador de Compatibilidade",
"compatibilityReport": "Relatório de Compatibilidade",
"pipelineDebugger": "Depurador de Pipeline",
"translationPipeline": "Pipeline de Tradução",
"pipelineVisualization": "Visualização do pipeline"
},
"usage": {
"title": "Uso",
"budgetManagement": "Gerenciamento de Orçamento",
"apiKey": "Chave de API",
"thisMonth": "Este Mês",
"setLimits": "Definir Limites",
"totalRequests": "Total de requisições",
"noDataYet": "Sem dados ainda",
"entries": "Entradas",
"hitRate": "Taxa de Acerto",
"hitsMisses": "Acertos / Erros",
"circuitBreakers": "Disjuntores",
"lockedIPs": "IPs Bloqueados",
"howItWorks": "Como Funciona",
"define": "Definir",
"run": "Executar",
"evaluate": "Avaliar",
"evalSuites": "Suítes de Avaliação",
"modelEvals": "Avaliações de Modelos",
"modelLockouts": "Bloqueios de Modelo",
"noLockouts": "Nenhum modelo bloqueado",
"activeSessions": "Sessões Ativas",
"noSessions": "Sem sessões ativas",
"sessionsHint": "Sessões aparecem conforme requisições passam pelo proxy",
"session": "Sessão",
"age": "Idade",
"requests": "Requisições",
"connection": "Conexão",
"providerLimits": "Limites do Provedor",
"noProviders": "Nenhum Provedor Conectado",
"modelQuotas": "Cotas de Modelo",
"noQuotaData": "Sem dados de cota",
"noQuotaDataAvailable": "Nenhum dado de cota disponível"
},
"modals": {
"waitingAuth": "Aguardando Autorização",
"verificationUrl": "URL de Verificação",
"yourCode": "Seu Código",
"remoteAccess": "Acesso remoto:",
"connectedSuccess": "Conectado com Sucesso!",
"connectionFailed": "Falha na Conexão",
"chooseAuthMethod": "Escolha seu método de autenticação:",
"awsBuilderId": "AWS Builder ID",
"awsIamIdentity": "AWS IAM Identity Center",
"googleAccount": "Conta Google",
"githubAccount": "Conta GitHub",
"importToken": "Importar Token",
"pasteToken": "Cole o refresh token do Kiro IDE.",
"awsRegion": "Região AWS",
"autoDetecting": "Detectando tokens automaticamente...",
"readingFromCache": "Lendo do cache AWS SSO",
"readingFromCursor": "Lendo do banco de dados do Cursor IDE",
"initializing": "Inicializando...",
"pricingConfig": "Configuração de Preços",
"loadingPricing": "Carregando dados de preços...",
"pricingRatesFormat": "Formato de Taxas de Preço",
"noPricingData": "Nenhum dado de preço disponível",
"noModelsFound": "Nenhum modelo encontrado"
},
"loggers": {
"allProviders": "Todos os Provedores",
"allModels": "Todos os Modelos",
"allAccounts": "Todas as Contas",
"allApiKeys": "Todas as Chaves de API",
"allTypes": "Todos os Tipos",
"allLevels": "Todos os Níveis",
"modelAZ": "Modelo A-Z",
"modelZA": "Modelo Z-A",
"loadingLogs": "Carregando logs...",
"loadingProxyLogs": "Carregando logs do proxy...",
"noLogEntries": "Nenhuma entrada de log encontrada",
"noPayloadData": "Nenhum dado de payload disponível para esta entrada.",
"proxyEvent": "Evento do Proxy",
"proxy": "Proxy",
"level": "Nível",
"directNative": "Direto (nativo)",
"combo": "Combo",
"inputTokens": "E:",
"outputTokens": "S:"
},
"stats": {
"usageOverview": "Visão Geral de Uso",
"outputTokens": "Tokens de Saída",
"totalCost": "Custo Total",
"usageByModel": "Uso por Modelo",
"usageByAccount": "Uso por Conta",
"failedToLoad": "Falha ao carregar estatísticas de uso.",
"tokenHealth": "Saúde dos Tokens",
"totalOAuth": "Total OAuth",
"healthy": "Saudável",
"errored": "Com Erro",
"lastCheck": "Última verificação",
"noData": "Sem dados",
"share": "Compartilhar",
"unableToLoad": "Não foi possível carregar métricas do sistema",
"product": "Produto",
"resources": "Recursos",
"company": "Empresa"
},
"auth": {
"welcome": "Bem-vindo",
"signIn": "Entrar",
"enterPassword": "Digite sua senha para continuar",
"password": "Senha",
"unifiedProxy": "Proxy Unificado de API de IA",
"passwordNotEnabled": "Proteção por senha não está ativada",
"resetPassword": "Redefinir Senha",
"resetDescription": "Escolha um método para recuperar acesso ao painel",
"stopServer": "Pare o servidor OmniRoute",
"processing": "Processando...",
"pleaseWait": "Aguarde enquanto completamos a autorização.",
"authSuccess": "Autorização bem-sucedida!",
"copyUrl": "Copiar esta URL",
"accessDenied": "Acesso Negado",
"forgotPassword": "Esqueceu a senha?"
},
"landing": {
"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:",
"getStarted": "Começar",
"product": "Produto",
"resources": "Recursos",
"legal": "Legal",
"interactiveDiagram": "Diagrama interativo visível no desktop"
},
"docs": {
"title": "Documentação",
"quickStart": "Início Rápido",
"features": "Recursos",
"supportedProviders": "Provedores Suportados",
"commonUseCases": "Casos de Uso Comuns",
"clientCompatibility": "Compatibilidade de Clientes",
"apiReference": "Referência da API",
"method": "Método",
"path": "Caminho",
"notes": "Notas",
"modelPrefixes": "Prefixos de Modelo",
"prefix": "Prefixo",
"troubleshooting": "Solução de Problemas",
"supportsChat": "Suporta endpoints de chat e responses.",
"oauthAutoRefresh": "Conexão OAuth com atualização automática de token.",
"fullStreaming": "Suporte completo a streaming para todos os modelos."
},
"legal": {
"privacyPolicy": "Política de Privacidade",
"termsOfService": "Termos de Serviço",
"providerConfigurations": "Configurações de provedores",
"apiKeys": "Chaves de API",
"usageLogs": "Logs de uso",
"applicationSettings": "Configurações do aplicativo",
"viewExportAnalytics": "Visualizar e exportar análises de uso",
"clearHistory": "Limpar histórico de uso a qualquer momento",
"configureRetention": "Configurar políticas de retenção de logs",
"backupRestore": "Fazer backup e restaurar seu banco de dados"
}
}

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
/**
* Console Log Viewer — Real-time application log viewer.
*
@@ -42,6 +44,7 @@ const LEVEL_BG: Record<string, string> = {
const POLL_INTERVAL = 5000; // 5 seconds
export default function ConsoleLogViewer() {
const t = useTranslations("loggers");
const [logs, setLogs] = useState<LogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -131,7 +134,7 @@ export default function ConsoleLogViewer() {
aria-label="Filter by log level"
className="px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] focus:outline-2 focus:outline-[var(--color-accent)]"
>
<option value="all">All Levels</option>
<option value="all">{t("allLevels")}</option>
<option value="debug">Debug+</option>
<option value="info">Info+</option>
<option value="warn">Warn+</option>
@@ -226,7 +229,7 @@ export default function ConsoleLogViewer() {
<span className="material-symbols-outlined text-[40px] block mb-2 opacity-30">
terminal
</span>
<p>No log entries found</p>
<p>{t("noLogEntries")}</p>
<p className="text-[10px] mt-1 opacity-60">
Ensure LOG_TO_FILE=true is set in your .env file
</p>

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { APP_CONFIG } from "@/shared/constants/config";
@@ -36,6 +38,7 @@ const footerLinks = {
};
export default function Footer() {
const t = useTranslations("stats");
const renderFooterLink = (link) => {
if (link.external) {
return (
@@ -106,7 +109,7 @@ export default function Footer() {
{/* Product */}
<div>
<h4 className="font-semibold text-text-main mb-4">Product</h4>
<h4 className="font-semibold text-text-main mb-4">{t(">product</")}</h4>
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
{footerLinks.product.map((link) => (
<li key={link.label}>{renderFooterLink(link)}</li>
@@ -116,7 +119,7 @@ export default function Footer() {
{/* Resources */}
<div>
<h4 className="font-semibold text-text-main mb-4">Resources</h4>
<h4 className="font-semibold text-text-main mb-4">{t(">resources</")}</h4>
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
{footerLinks.resources.map((link) => (
<li key={link.label}>{renderFooterLink(link)}</li>
@@ -126,7 +129,7 @@ export default function Footer() {
{/* Company */}
<div>
<h4 className="font-semibold text-text-main mb-4">Company</h4>
<h4 className="font-semibold text-text-main mb-4">{t(">company</")}</h4>
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
{footerLinks.company.map((link) => (
<li key={link.label}>{renderFooterLink(link)}</li>

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
/**
* SystemMonitor — Real-time system metrics widget
*
@@ -51,6 +53,7 @@ function MetricRow({ icon, label, value, color = "text-text-main" }) {
}
export default function SystemMonitor({ compact = false }) {
const t = useTranslations("stats");
const [metrics, setMetrics] = useState(null);
const [error, setError] = useState(false);
const mountedRef = useRef(true);
@@ -85,7 +88,7 @@ export default function SystemMonitor({ compact = false }) {
<Card className="p-4">
<div className="flex items-center gap-2 text-text-muted text-sm">
<span className="material-symbols-outlined text-[18px] text-red-400">error</span>
<span>Unable to load system metrics</span>
<span>{t("unableToLoad")}</span>
</div>
</Card>
);

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
/**
* TokenHealthBadge — Batch G
*
@@ -17,6 +19,7 @@ const STATUS_MAP = {
};
export default function TokenHealthBadge() {
const t = useTranslations("stats");
const [health, setHealth] = useState(null);
const [showTooltip, setShowTooltip] = useState(false);
@@ -71,31 +74,31 @@ export default function TokenHealthBadge() {
backdropFilter: "blur(12px)",
}}
>
<p className="text-xs font-medium text-text-main mb-2">Token Health</p>
<p className="text-xs font-medium text-text-main mb-2">{t("tokenHealth")}</p>
<div className="flex flex-col gap-1 text-xs">
<div className="flex justify-between">
<span className="text-text-muted">Total OAuth</span>
<span className="text-text-muted">{t("totalOAuth")}</span>
<span className="text-text-main">{health.total}</span>
</div>
<div className="flex justify-between">
<span className="text-emerald-400">Healthy</span>
<span className="text-emerald-400">{t(">healthy</")}</span>
<span className="text-text-main">{health.healthy}</span>
</div>
{health.errored > 0 && (
<div className="flex justify-between">
<span className="text-red-400">Errored</span>
<span className="text-red-400">{t(">errored</")}</span>
<span className="text-text-main">{health.errored}</span>
</div>
)}
{health.warning > 0 && (
<div className="flex justify-between">
<span className="text-amber-400">Warning</span>
<span className="text-amber-400">{t(">warning</")}</span>
<span className="text-text-main">{health.warning}</span>
</div>
)}
{health.lastCheckAt && (
<div className="flex justify-between mt-1 pt-1 border-t border-white/5">
<span className="text-text-muted">Last check</span>
<span className="text-text-muted">{t("lastCheck")}</span>
<span className="text-text-muted">
{new Date(health.lastCheckAt).toLocaleTimeString()}
</span>

View File

@@ -1,5 +1,7 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
import PropTypes from "prop-types";
import { useSearchParams, useRouter } from "next/navigation";
@@ -8,7 +10,15 @@ import Badge from "./Badge";
import { CardSkeleton } from "./Loading";
import { fmtFull, fmtCost } from "@/shared/utils/formatting";
function SortIcon({ field, currentSort, currentOrder }: { field: string; currentSort: string; currentOrder: string }) {
function SortIcon({
field,
currentSort,
currentOrder,
}: {
field: string;
currentSort: string;
currentOrder: string;
}) {
if (currentSort !== field) return <span className="ml-1 opacity-20"></span>;
return <span className="ml-1">{currentOrder === "asc" ? "↑" : "↓"}</span>;
}
@@ -19,7 +29,13 @@ SortIcon.propTypes = {
currentOrder: PropTypes.string.isRequired,
};
function MiniBarGraph({ data, colorClass = "bg-primary" }: { data: number[]; colorClass?: string }) {
function MiniBarGraph({
data,
colorClass = "bg-primary",
}: {
data: number[];
colorClass?: string;
}) {
const max = Math.max(...data, 1);
return (
<div className="flex items-end gap-1 h-8 w-24">
@@ -41,6 +57,7 @@ MiniBarGraph.propTypes = {
};
export default function UsageStats() {
const t = useTranslations("stats");
const router = useRouter();
const searchParams = useSearchParams();
@@ -188,7 +205,7 @@ export default function UsageStats() {
if (loading) return <CardSkeleton />;
if (!stats) return <div className="text-text-muted">Failed to load usage statistics.</div>;
if (!stats) return <div className="text-text-muted">{t("failedToLoad")}</div>;
// Format number with commas — delegated to shared module
const fmt = (n: number) => fmtFull(n);
@@ -213,7 +230,7 @@ export default function UsageStats() {
<div className="flex flex-col gap-6">
{/* Header with Auto Refresh Toggle and View Toggle */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Usage Overview</h2>
<h2 className="text-xl font-semibold">{t("usageOverview")}</h2>
<div className="flex items-center gap-2">
{/* View Toggle */}
<div className="flex items-center gap-1 bg-bg-subtle rounded-lg p-1 border border-border">
@@ -331,21 +348,25 @@ export default function UsageStats() {
<Card className="px-4 py-2 flex flex-col gap-1">
<div className="flex justify-between items-start gap-4">
<div className="flex flex-col gap-1 flex-1">
<span className="text-text-muted text-sm uppercase font-semibold">Output Tokens</span>
<span className="text-text-muted text-sm uppercase font-semibold">
{t("outputTokens")}
</span>
<span className="text-2xl font-bold text-success">
{fmt(stats.totalCompletionTokens)}
</span>
</div>
<div className="w-px bg-border self-stretch mx-2" />
<div className="flex flex-col gap-1 flex-1">
<span className="text-text-muted text-sm uppercase font-semibold">Total Cost</span>
<span className="text-text-muted text-sm uppercase font-semibold">
{t("totalCost")}
</span>
<span className="text-2xl font-bold text-warning">{fmtCost(stats.totalCost)}</span>
</div>
</div>
</Card>
</div>
{/* Usage by Model Table */}
{/* {t("usageByModel")} Table */}
<Card className="overflow-hidden">
<div className="p-4 border-b border-border bg-bg-subtle/50">
<h3 className="font-semibold">Usage by Model</h3>
@@ -504,7 +525,7 @@ export default function UsageStats() {
</div>
</Card>
{/* Usage by Account Table */}
{/* {t("usageByAccount")} Table */}
<Card className="overflow-hidden">
<div className="p-4 border-b border-border bg-bg-subtle/50">
<h3 className="font-semibold">Usage by Account</h3>