From 481a630273fa70a5ea6a82a325a3acf52fb7e715 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 25 Feb 2026 14:35:10 -0300 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20migrate=20Settings=20batch=204=20?= =?UTF-8?q?=E2=80=94=20final=204=20large=20tabs=20(80+=20strings)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ComboDefaultsTab: strategy labels, toggles, provider overrides - PricingTab: model pricing, stats, save/reset, search - ResilienceTab: provider profiles, rate limiting, circuit breakers, policies - SystemStorageTab: export/import, backup/restore, database info - Expanded settings namespace to ~290 total keys - Completes Settings page i18n migration (17/17 files) --- .../settings/components/ComboDefaultsTab.tsx | 35 ++-- .../settings/components/PricingTab.tsx | 185 +++++++----------- .../settings/components/ResilienceTab.tsx | 69 ++++--- .../settings/components/SystemStorageTab.tsx | 58 +++--- src/i18n/messages/en.json | 105 +++++++++- src/i18n/messages/pt-BR.json | 105 +++++++++- 6 files changed, 357 insertions(+), 200 deletions(-) diff --git a/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx index 6b45895351..5c7c288710 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { Card, Button, Input, Toggle } from "@/shared/components"; import { cn } from "@/shared/utils/cn"; +import { useTranslations } from "next-intl"; export default function ComboDefaultsTab() { const [comboDefaults, setComboDefaults] = useState({ @@ -18,6 +19,8 @@ export default function ComboDefaultsTab() { const [providerOverrides, setProviderOverrides] = useState({}); const [newOverrideProvider, setNewOverrideProvider] = useState(""); const [saving, setSaving] = useState(false); + const t = useTranslations("settings"); + const tc = useTranslations("common"); useEffect(() => { fetch("/api/settings/combo-defaults") @@ -67,17 +70,15 @@ export default function ComboDefaultsTab() { tune -

Combo Defaults

- Global combo configuration +

{t("comboDefaultsTitle")}

+ {t("globalComboConfig")}
{/* Default Strategy */}
-

Default Strategy

-

- Applied to new combos without explicit strategy -

+

{t("defaultStrategy")}

+

{t("defaultStrategyDesc")}

-

Health Check

-

Pre-check provider availability

+

{t("healthCheck")}

+

{t("healthCheckDesc")}

-

Track Metrics

-

Record per-combo request metrics

+

{t("trackMetrics")}

+

{t("trackMetricsDesc")}

-

Provider Overrides

-

- Override timeout and retries per provider. Provider settings override global defaults. -

+

{t("providerOverrides")}

+

{t("providerOverridesDesc")}

{Object.entries(providerOverrides).map(([provider, config]: [string, any]) => (
- retries + {t("retries")} - ms + {t("ms")}
@@ -286,7 +285,7 @@ export default function ComboDefaultsTab() { {/* Save */}
diff --git a/src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx index 7bb8c0fce7..1861a1be22 100644 --- a/src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { Card } from "@/shared/components"; +import { useTranslations } from "next-intl"; const PRICING_FIELDS = ["input", "output", "cached", "reasoning", "cache_creation"]; const FIELD_LABELS = { @@ -22,6 +23,7 @@ export default function PricingTab() { const [expandedProviders, setExpandedProviders] = useState(new Set()); const [searchQuery, setSearchQuery] = useState(""); const [editedProviders, setEditedProviders] = useState(new Set()); + const t = useTranslations("settings"); // Load catalog + pricing useEffect(() => { @@ -50,9 +52,7 @@ export default function PricingTab() { .map(([alias, info]: [string, any]) => ({ alias, ...info, - pricedModels: pricingData[alias] - ? Object.keys(pricingData[alias]).length - : 0, + pricedModels: pricingData[alias] ? Object.keys(pricingData[alias]).length : 0, })) .sort((a, b) => b.modelCount - a.modelCount); return providers; @@ -66,11 +66,7 @@ export default function PricingTab() { (p) => p.alias.toLowerCase().includes(q) || p.id.toLowerCase().includes(q) || - p.models.some( - (m) => - m.id.toLowerCase().includes(q) || - m.name.toLowerCase().includes(q) - ) + p.models.some((m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q)) ); }, [allProviders, searchQuery]); @@ -97,23 +93,20 @@ export default function PricingTab() { }); }, []); - const handlePricingChange = useCallback( - (provider, model, field, value) => { - const numValue = parseFloat(value); - if (isNaN(numValue) || numValue < 0) return; + const handlePricingChange = useCallback((provider, model, field, value) => { + const numValue = parseFloat(value); + if (isNaN(numValue) || numValue < 0) return; - setPricingData((prev) => { - const next = { ...prev }; - if (!next[provider]) next[provider] = {}; - if (!next[provider][model]) - next[provider][model] = { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 }; - next[provider][model] = { ...next[provider][model], [field]: numValue }; - return next; - }); - setEditedProviders((prev) => new Set(prev).add(provider)); - }, - [] - ); + setPricingData((prev) => { + const next = { ...prev }; + if (!next[provider]) next[provider] = {}; + if (!next[provider][model]) + next[provider][model] = { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 }; + next[provider][model] = { ...next[provider][model], [field]: numValue }; + return next; + }); + setEditedProviders((prev) => new Set(prev).add(provider)); + }, []); const saveProvider = useCallback( async (providerAlias) => { @@ -148,36 +141,25 @@ export default function PricingTab() { [pricingData] ); - const resetProvider = useCallback( - async (providerAlias) => { - if ( - !confirm( - `Reset all pricing for ${providerAlias.toUpperCase()} to defaults?` - ) - ) - return; - try { - const response = await fetch( - `/api/pricing?provider=${providerAlias}`, - { method: "DELETE" } - ); - if (response.ok) { - const updated = await response.json(); - setPricingData(updated); - setSaveStatus(`🔄 ${providerAlias.toUpperCase()} reset to defaults`); - setEditedProviders((prev) => { - const next = new Set(prev); - next.delete(providerAlias); - return next; - }); - setTimeout(() => setSaveStatus(""), 3000); - } - } catch (error) { - setSaveStatus(`❌ Reset failed: ${error.message}`); + const resetProvider = useCallback(async (providerAlias) => { + if (!confirm(`Reset all pricing for ${providerAlias.toUpperCase()} to defaults?`)) return; + try { + const response = await fetch(`/api/pricing?provider=${providerAlias}`, { method: "DELETE" }); + if (response.ok) { + const updated = await response.json(); + setPricingData(updated); + setSaveStatus(`🔄 ${providerAlias.toUpperCase()} reset to defaults`); + setEditedProviders((prev) => { + const next = new Set(prev); + next.delete(providerAlias); + return next; + }); + setTimeout(() => setSaveStatus(""), 3000); } - }, - [] - ); + } catch (error) { + setSaveStatus(`❌ Reset failed: ${error.message}`); + } + }, []); const selectProviderFilter = useCallback((alias) => { setSelectedProvider((prev) => (prev === alias ? null : alias)); @@ -194,9 +176,7 @@ export default function PricingTab() { if (loading) { return (
-
- Loading pricing data... -
+
{t("loadingPricing")}
); } @@ -206,30 +186,23 @@ export default function PricingTab() { {/* Header + Stats */}
-

Model Pricing

+

{t("modelPricing")}

- Configure cost rates per model • All rates in{" "} - $/1M tokens + Configure cost rates per model • All rates in $/1M tokens

-
- Providers -
+
{t("providers")}
{stats.providers}
-
- Registry -
+
{t("registry")}
{stats.totalModels}
-
Priced
-
- {stats.pricedCount as number} -
+
{t("priced")}
+
{stats.pricedCount as number}
@@ -249,7 +222,7 @@ export default function PricingTab() { setSearchQuery(e.target.value)} className="w-full pl-10 pr-3 py-2 bg-bg-base border border-border rounded-lg focus:outline-none focus:border-primary text-sm" @@ -261,7 +234,7 @@ export default function PricingTab() { className="px-3 py-2 text-xs bg-primary/10 text-primary border border-primary/20 rounded-lg hover:bg-primary/20 transition-colors flex items-center gap-1" > close - {selectedProvider.toUpperCase()} — Show All + {selectedProvider.toUpperCase()} — {t("showAll")} )}
@@ -276,12 +249,11 @@ export default function PricingTab() { selectedProvider === p.alias ? "bg-primary text-white shadow-sm" : editedProviders.has(p.alias) - ? "bg-yellow-500/15 text-yellow-400 border border-yellow-500/30" - : "bg-bg-subtle text-text-muted hover:bg-bg-hover border border-transparent" + ? "bg-yellow-500/15 text-yellow-400 border border-yellow-500/30" + : "bg-bg-subtle text-text-muted hover:bg-bg-hover border border-transparent" }`} > - {p.alias.toUpperCase()}{" "} - ({p.modelCount}) + {p.alias.toUpperCase()} ({p.modelCount}) ))}
@@ -306,32 +278,26 @@ export default function PricingTab() { ))} {displayProviders.length === 0 && ( -
- No providers match your search. -
+
{t("noProvidersMatch")}
)}
{/* Info Box */}

- - info - - How Pricing Works + info + {t("howPricingWorks")}

- Input: tokens sent to the model •{" "} - Output: tokens generated •{" "} - Cached: reused input (~50% of input rate) •{" "} + Input: tokens sent to the model • Output: tokens + generated • Cached: reused input (~50% of input rate) •{" "} Reasoning: thinking tokens (falls back to Output) •{" "} - Cache Write: creating cache entries (falls back to - Input) + Cache Write: creating cache entries (falls back to Input)

- Cost = (input Ă— input_rate) + (output Ă— output_rate) + (cached Ă— - cached_rate) per million tokens. + Cost = (input Ă— input_rate) + (output Ă— output_rate) + (cached Ă— cached_rate) per + million tokens.

@@ -352,20 +318,19 @@ function ProviderSection({ onReset, saving, }) { + const t = useTranslations("settings"); const pricedCount = Object.keys(pricingData).length; const authBadge = provider.authType === "oauth" ? "OAuth" : provider.authType === "apikey" - ? "API Key" - : provider.authType; + ? "API Key" + : provider.authType; return (
{/* Header (click to expand) */} @@ -385,9 +350,7 @@ function ProviderSection({ {provider.id.charAt(0).toUpperCase() + provider.id.slice(1)} - - ({provider.alias.toUpperCase()}) - + ({provider.alias.toUpperCase()})
{authBadge} @@ -397,11 +360,7 @@ function ProviderSection({
- {isEdited && ( - - unsaved - - )} + {isEdited && {t("unsaved")}} {pricedCount}/{provider.modelCount} priced @@ -426,8 +385,7 @@ function ProviderSection({ {/* Actions bar */}
- {provider.modelCount} models •{" "} - {pricedCount} with pricing configured + {provider.modelCount} models • {pricedCount} with pricing configured
@@ -457,12 +415,9 @@ function ProviderSection({ - + {PRICING_FIELDS.map((field) => ( - ))} @@ -474,9 +429,7 @@ function ProviderSection({ key={model.id} model={model} pricing={pricingData[model.id]} - onPricingChange={(field, value) => - onPricingChange(model.id, field, value) - } + onPricingChange={(field, value) => onPricingChange(model.id, field, value)} /> ))} @@ -498,9 +451,7 @@ function ModelRow({ model, pricing, onPricingChange }) {
Model{t("model")} + {FIELD_LABELS[field]}
{model.name} {model.custom && ( diff --git a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx index 1b37ac2308..f10aa603fb 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback } from "react"; import { Card, Button } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; +import { useTranslations } from "next-intl"; // ─── State colors and labels ────────────────────────────────────────────── const STATE_STYLES = { @@ -46,6 +47,8 @@ function formatMs(ms) { function ProviderProfilesCard({ profiles, onSave, saving }) { const [editMode, setEditMode] = useState(false); const [draft, setDraft] = useState(profiles); + const t = useTranslations("settings"); + const tc = useTranslations("common"); useEffect(() => { setDraft(profiles); @@ -72,12 +75,12 @@ function ProviderProfilesCard({ profiles, onSave, saving }) { -

Provider Profiles

+

{t("providerProfiles")}

{editMode ? (
) : ( )} -

- Separate resilience settings for OAuth (session-based) and API Key (metered) providers. - OAuth providers have stricter thresholds due to lower rate limits. -

+

{t("providerProfilesDesc")}

{["oauth", "apikey"].map((type) => ( @@ -108,7 +108,7 @@ function ProviderProfilesCard({ profiles, onSave, saving }) { - {type === "oauth" ? "OAuth Providers" : "API Key Providers"} + {type === "oauth" ? t("oauthProviders") : t("apiKeyProviders")}
{fields.map(({ key, label, suffix }) => ( @@ -148,6 +148,8 @@ function ProviderProfilesCard({ profiles, onSave, saving }) { function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) { const [editMode, setEditMode] = useState(false); const [draft, setDraft] = useState(defaults || {}); + const t = useTranslations("settings"); + const tc = useTranslations("common"); // Sync draft when defaults change from parent (standard prop-to-state sync) /* eslint-disable react-hooks/set-state-in-effect */ @@ -169,12 +171,12 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) { -

Rate Limiting

+

{t("rateLimiting")}

{editMode ? (
) : ( )}
-

- API Key providers are automatically rate-limited with safe defaults. Limits are learned - from response headers and adapt over time. -

+

{t("rateLimitingDesc")}

- Default Safety Net + {t("defaultSafetyNet")}

{[ @@ -233,7 +232,7 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) { {rateLimitStatus && rateLimitStatus.length > 0 ? (

- Active Limiters + {t("activeLimiters")}

{rateLimitStatus.map((rl, i) => (
) : ( -

No active rate limiters yet.

+

{t("noActiveLimiters")}

)}
@@ -261,6 +260,7 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) { function CircuitBreakerCard({ breakers, onReset, loading }) { const activeBreakers = breakers.filter((b) => b.state !== "CLOSED"); const totalBreakers = breakers.length; + const t = useTranslations("settings"); return ( @@ -270,7 +270,7 @@ function CircuitBreakerCard({ breakers, onReset, loading }) { -

Circuit Breakers

+

{t("circuitBreakers")}

@@ -286,17 +286,14 @@ function CircuitBreakerCard({ breakers, onReset, loading }) { onClick={onReset} disabled={loading} > - Reset All + {t("resetAll")} )}
{breakers.length === 0 ? ( -

- No circuit breakers active yet. They are created automatically when requests flow - through the combo pipeline. -

+

{t("noCircuitBreakers")}

) : (
{breakers.map((b) => { @@ -343,6 +340,7 @@ function PoliciesCard() { const [loading, setLoading] = useState(true); const [unlocking, setUnlocking] = useState(null); const notify = useNotificationStore(); + const t = useTranslations("settings"); const fetchPolicies = useCallback(async () => { try { @@ -394,7 +392,7 @@ function PoliciesCard() {
policy - Loading policies... + {t("loadingPolicies")}
); @@ -408,7 +406,7 @@ function PoliciesCard() { -

Policies & Locked Identifiers

+

{t("policiesLocked")}

{hasIssues && (
-

- All systems operational — no lockouts or tripped breakers -

+

{t("allOperational")}

) : ( @@ -433,7 +429,7 @@ function PoliciesCard() { {/* Circuit Breakers */} {circuitBreakers.filter((cb) => cb.state !== "closed").length > 0 && (
-

Circuit Breakers

+

{t("circuitBreakers")}

{circuitBreakers .filter((cb) => cb.state !== "closed") @@ -479,7 +475,7 @@ function PoliciesCard() { {/* Locked Identifiers */} {lockedIds.length > 0 && (
-

Locked Identifiers

+

{t("lockedIdentifiers")}

{lockedIds.map((id, i) => { const identifier = typeof id === "string" ? id : id.identifier || id.id; @@ -506,7 +502,7 @@ function PoliciesCard() { disabled={unlocking === identifier} className="text-xs" > - {unlocking === identifier ? "Unlocking..." : "Force Unlock"} + {unlocking === identifier ? t("unlocking") : t("forceUnlock")}
); @@ -527,6 +523,7 @@ export default function ResilienceTab() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); + const t = useTranslations("settings"); const loadData = useCallback(async () => { try { @@ -601,7 +598,7 @@ export default function ResilienceTab() { return (
hourglass_empty - Loading resilience status... + {t("loadingResilience")}
); } @@ -614,7 +611,7 @@ export default function ResilienceTab() { {error}
); diff --git a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx index d6c7ac77ef..b732352a25 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { Card, Button, Badge } from "@/shared/components"; +import { useTranslations } from "next-intl"; export default function SystemStorageTab() { const [backups, setBackups] = useState([]); @@ -18,6 +19,8 @@ export default function SystemStorageTab() { const [confirmImport, setConfirmImport] = useState(false); const [pendingImportFile, setPendingImportFile] = useState(null); const fileInputRef = useRef(null); + const t = useTranslations("settings"); + const tc = useTranslations("common"); const [storageHealth, setStorageHealth] = useState({ driver: "sqlite", dbPath: "~/.omniroute/storage.sqlite", @@ -149,7 +152,7 @@ export default function SystemStorageTab() { if (!file.name.endsWith(".sqlite")) { setImportStatus({ type: "error", - message: "Invalid file type. Only .sqlite files are accepted.", + message: t("invalidFileType"), }); return; } @@ -224,8 +227,8 @@ export default function SystemStorageTab() {
-

System & Storage

-

All data stored locally on your machine

+

{t("systemStorage")}

+

{t("allDataLocal")}

{storageHealth.driver || "json"} @@ -235,13 +238,17 @@ export default function SystemStorageTab() { {/* Storage info grid */}
-

Database Path

+

+ {t("databasePath")} +

{storageHealth.dbPath || "~/.omniroute/storage.sqlite"}

-

Database Size

+

+ {t("databaseSize")} +

{formatBytes(storageHealth.sizeBytes)}

@@ -252,7 +259,7 @@ export default function SystemStorageTab() { - Export Database + {t("exportDatabase")}
-

Confirm Database Import

+

{t("confirmDbImport")}

This will replace all current data with the content from{" "} {pendingImportFile.name}. A backup will be @@ -329,10 +336,10 @@ export default function SystemStorageTab() { onClick={handleImportConfirm} className="!bg-amber-500 hover:!bg-amber-600" > - Yes, Import + {t("yesImport")}

@@ -364,11 +371,11 @@ export default function SystemStorageTab() { schedule
-

Last Backup

+

{t("lastBackup")}

{storageHealth.lastBackupAt ? `${new Date(storageHealth.lastBackupAt).toLocaleString("pt-BR")} (${formatRelativeTime(storageHealth.lastBackupAt)})` - : "No backup yet"} + : t("noBackupYet")}

@@ -381,7 +388,7 @@ export default function SystemStorageTab() { - Backup Now + {t("backupNow")} @@ -419,7 +426,7 @@ export default function SystemStorageTab() { > restore -

Backup & Restore

+

{t("backupRestore")}

-

- Database snapshots are created automatically before restore and every 15 minutes when data - changes. Retention: 24 hourly + 30 daily backups with smart rotation. -

+

{t("backupRetentionDesc")}

{restoreStatus.message && (
progress_activity - Loading backups... + {t("loadingBackups")}
) : backups.length === 0 ? (
@@ -475,7 +479,7 @@ export default function SystemStorageTab() { > folder_off - No backups available yet. Backups will be created automatically when data changes. + {t("noBackupsYet")}
) : ( <> @@ -490,7 +494,7 @@ export default function SystemStorageTab() { - Refresh + {t("refresh")} {backups.map((backup) => ( @@ -531,7 +535,7 @@ export default function SystemStorageTab() {
{confirmRestoreId === backup.id ? ( <> - Confirm? + {t("confirm")} ) : ( @@ -561,7 +565,7 @@ export default function SystemStorageTab() { > restore - Restore + {t("restore")} )}
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index cb8c6d3b07..e97d2dc0f6 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -749,7 +749,110 @@ "failedCreateChain": "Failed to create chain", "failedDeleteChain": "Failed to delete chain", "fillModelAndProviders": "Please fill model name and providers", - "addAtLeastOneProvider": "Add at least one provider" + "addAtLeastOneProvider": "Add at least one provider", + "comboDefaultsTitle": "Combo Defaults", + "globalComboConfig": "Global combo configuration", + "defaultStrategy": "Default Strategy", + "defaultStrategyDesc": "Applied to new combos without explicit strategy", + "priority": "Priority", + "weighted": "Weighted", + "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.", + "retries": "retries", + "ms": "ms", + "saveComboDefaults": "Save Combo Defaults", + "maxNestingDepth": "Max Nesting Depth", + "concurrencyPerModel": "Concurrency / Model", + "queueTimeout": "Queue Timeout (ms)", + "providerProfiles": "Provider Profiles", + "providerProfilesDesc": "Separate resilience settings for OAuth (session-based) and API Key (metered) providers. OAuth providers have stricter thresholds due to lower rate limits.", + "oauthProviders": "OAuth Providers", + "apiKeyProviders": "API Key Providers", + "transientCooldown": "Transient Cooldown", + "rateLimitCooldown": "Rate Limit Cooldown", + "maxBackoffLevel": "Max Backoff Level", + "cbThreshold": "CB Threshold", + "cbResetTime": "CB Reset Time", + "rateLimiting": "Rate Limiting", + "rateLimitingDesc": "API Key providers are automatically rate-limited with safe defaults. Limits are learned from response headers and adapt over time.", + "defaultSafetyNet": "Default Safety Net", + "rpm": "RPM", + "minGap": "Min Gap", + "maxConcurrent": "Max Concurrent", + "activeLimiters": "Active Limiters", + "noActiveLimiters": "No active rate limiters yet.", + "reservoir": "Reservoir", + "running": "Running", + "queued": "Queued", + "circuitBreakers": "Circuit Breakers", + "tripped": "{count} tripped", + "healthy": "{count} healthy", + "resetAll": "Reset All", + "noCircuitBreakers": "No circuit breakers active yet. They are created automatically when requests flow through the combo pipeline.", + "failures": "{count} failure(s)", + "policiesLocked": "Policies & Locked Identifiers", + "allOperational": "All systems operational — no lockouts or tripped breakers", + "loadingPolicies": "Loading policies...", + "lockedIdentifiers": "Locked Identifiers", + "forceUnlock": "Force Unlock", + "unlocking": "Unlocking...", + "failedUnlock": "Failed to unlock", + "loadingResilience": "Loading resilience status...", + "retry": "Retry", + "systemStorage": "System & Storage", + "allDataLocal": "All data stored locally on your machine", + "databasePath": "Database Path", + "exportDatabase": "Export Database", + "exportAll": "Export All (.tar.gz)", + "importDatabase": "Import Database", + "confirmDbImport": "Confirm Database Import", + "confirmDbImportDesc": "This will replace all current data with the content from {file}. A backup will be created automatically before the import.", + "yesImport": "Yes, Import", + "lastBackup": "Last Backup", + "noBackupYet": "No backup yet", + "backupNow": "Backup Now", + "backupRestore": "Backup & Restore", + "viewBackups": "View Backups", + "hide": "Hide", + "backupRetentionDesc": "Database snapshots are created automatically before restore and every 15 minutes when data changes. Retention: 24 hourly + 30 daily backups with smart rotation.", + "loadingBackups": "Loading backups...", + "noBackupsYet": "No backups available yet. Backups will be created automatically when data changes.", + "backupsAvailable": "{count} backup(s) available", + "refresh": "Refresh", + "confirm": "Confirm?", + "yes": "Yes", + "no": "No", + "restore": "Restore", + "invalidFileType": "Invalid file type. Only .sqlite files are accepted.", + "noChangesSinceBackup": "No changes since last backup", + "backupFailed": "Backup failed", + "restoreFailed": "Restore failed", + "importFailed": "Import failed", + "errorDuringRestore": "An error occurred during restore", + "errorDuringImport": "An error occurred during import", + "modelPricing": "Model Pricing", + "modelPricingDesc": "Configure cost rates per model • All rates in $/1M tokens", + "providers": "Providers", + "registry": "Registry", + "priced": "Priced", + "searchProvidersModels": "Search providers or models...", + "showAll": "Show All", + "noProvidersMatch": "No providers match your search.", + "howPricingWorks": "How Pricing Works", + "cacheWrite": "Cache Write", + "unsaved": "unsaved", + "resetDefaults": "Reset Defaults", + "saveProvider": "Save Provider", + "saving": "Saving...", + "model": "Model", + "models": "models", + "withPricing": "with pricing configured", + "loadingPricing": "Loading pricing data...", + "databaseSize": "Database Size" }, "translator": { "title": "Translator", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index c8d7cc6838..5b68be35f1 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -749,7 +749,110 @@ "failedCreateChain": "Falha ao criar cadeia", "failedDeleteChain": "Falha ao excluir cadeia", "fillModelAndProviders": "Preencha o nome do modelo e os provedores", - "addAtLeastOneProvider": "Adicione pelo menos um provedor" + "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", + "priority": "Prioridade", + "weighted": "Ponderado", + "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.", + "retries": "tentativas", + "ms": "ms", + "saveComboDefaults": "Salvar Padrões de Combo", + "maxNestingDepth": "Profundidade Máx. de Aninhamento", + "concurrencyPerModel": "Concorrência / Modelo", + "queueTimeout": "Timeout da Fila (ms)", + "providerProfiles": "Perfis de Provedor", + "providerProfilesDesc": "Configurações de resiliência separadas para provedores OAuth (baseados em sessão) e API Key (medidos). Provedores OAuth têm limites mais rigorosos devido a taxas mais baixas.", + "oauthProviders": "Provedores OAuth", + "apiKeyProviders": "Provedores API Key", + "transientCooldown": "Cooldown Transitório", + "rateLimitCooldown": "Cooldown de Rate Limit", + "maxBackoffLevel": "Nível Máx. de Backoff", + "cbThreshold": "Limiar do CB", + "cbResetTime": "Tempo de Reset do CB", + "rateLimiting": "Limitação de Taxa", + "rateLimitingDesc": "Provedores API Key são automaticamente limitados com padrões seguros. Limites são aprendidos dos cabeçalhos de resposta e se adaptam ao longo do tempo.", + "defaultSafetyNet": "Rede de Segurança Padrão", + "rpm": "RPM", + "minGap": "Intervalo Mín.", + "maxConcurrent": "Máx. Concorrentes", + "activeLimiters": "Limitadores Ativos", + "noActiveLimiters": "Nenhum limitador de taxa ativo ainda.", + "reservoir": "Reservatório", + "running": "Em Execução", + "queued": "Na Fila", + "circuitBreakers": "Disjuntores", + "tripped": "{count} aberto(s)", + "healthy": "{count} saudável(is)", + "resetAll": "Resetar Todos", + "noCircuitBreakers": "Nenhum disjuntor ativo ainda. Eles são criados automaticamente quando requisições passam pelo pipeline de combos.", + "failures": "{count} falha(s)", + "policiesLocked": "Políticas e Identificadores Bloqueados", + "allOperational": "Todos os sistemas operacionais — sem bloqueios ou disjuntores ativados", + "loadingPolicies": "Carregando políticas...", + "lockedIdentifiers": "Identificadores Bloqueados", + "forceUnlock": "Forçar Desbloqueio", + "unlocking": "Desbloqueando...", + "failedUnlock": "Falha ao desbloquear", + "loadingResilience": "Carregando status de resiliência...", + "retry": "Tentar Novamente", + "systemStorage": "Sistema e Armazenamento", + "allDataLocal": "Todos os dados armazenados localmente na sua máquina", + "databasePath": "Caminho do Banco de Dados", + "exportDatabase": "Exportar Banco de Dados", + "exportAll": "Exportar Tudo (.tar.gz)", + "importDatabase": "Importar Banco de Dados", + "confirmDbImport": "Confirmar Importação do Banco", + "confirmDbImportDesc": "Isso substituirá todos os dados atuais pelo conteúdo de {file}. Um backup será criado automaticamente antes da importação.", + "yesImport": "Sim, Importar", + "lastBackup": "Último Backup", + "noBackupYet": "Nenhum backup ainda", + "backupNow": "Backup Agora", + "backupRestore": "Backup e Restauração", + "viewBackups": "Ver Backups", + "hide": "Ocultar", + "backupRetentionDesc": "Snapshots do banco são criados automaticamente antes da restauração e a cada 15 minutos quando há alterações. Retenção: 24 horários + 30 diários com rotação inteligente.", + "loadingBackups": "Carregando backups...", + "noBackupsYet": "Nenhum backup disponível ainda. Backups serão criados automaticamente quando houver alterações.", + "backupsAvailable": "{count} backup(s) disponível(is)", + "refresh": "Atualizar", + "confirm": "Confirmar?", + "yes": "Sim", + "no": "Não", + "restore": "Restaurar", + "invalidFileType": "Tipo de arquivo inválido. Apenas arquivos .sqlite são aceitos.", + "noChangesSinceBackup": "Sem alterações desde o último backup", + "backupFailed": "Falha no backup", + "restoreFailed": "Falha na restauração", + "importFailed": "Falha na importação", + "errorDuringRestore": "Ocorreu um erro durante a restauração", + "errorDuringImport": "Ocorreu um erro durante a importação", + "modelPricing": "Preços de Modelos", + "modelPricingDesc": "Configure taxas de custo por modelo • Todas as taxas em $/1M tokens", + "providers": "Provedores", + "registry": "Registro", + "priced": "Com Preço", + "searchProvidersModels": "Buscar provedores ou modelos...", + "showAll": "Mostrar Todos", + "noProvidersMatch": "Nenhum provedor corresponde à sua busca.", + "howPricingWorks": "Como os Preços Funcionam", + "cacheWrite": "Escrita em Cache", + "unsaved": "não salvo", + "resetDefaults": "Restaurar Padrões", + "saveProvider": "Salvar Provedor", + "saving": "Salvando...", + "model": "Modelo", + "models": "modelos", + "withPricing": "com preço configurado", + "loadingPricing": "Carregando dados de preços...", + "databaseSize": "Tamanho do Banco de Dados" }, "translator": { "title": "Tradutor",