chore(i18n): replace hardcoded UI text with t() calls across dashboard (round 1)

Subagents refactored 8 high-impact dashboard pages, replacing 81 of the
407 hardcoded English/PT strings flagged by the audit with proper
useTranslations() lookups. Added 73 corresponding keys to en.json across
the home, apiManager, providers, settings, and usage namespaces.

Pages affected:
- BudgetTab (27 → 0)
- HomePageClient (2 → 0)
- RoutingTab (25 → 7)
- ResilienceTab (38 → 18)
- SystemStorageTab (42 → 21)
- providers/[id] (17 → 15)
- ApiManagerPageClient (14 → 13)
- OneproxyTab (13 → 10)

Also adds two helper scripts:
- scripts/i18n/extract-keys-from-diff.mjs — extracts new keys from git diff
- scripts/i18n/merge-keys.mjs — merges a pending-keys JSON into en.json

Remaining hardcoded strings will be addressed in follow-up rounds.
This commit is contained in:
diegosouzapw 2026-05-19 21:21:46 -03:00
parent 5f37f0f804
commit 8d34f4c650
12 changed files with 340 additions and 86 deletions

1
.gitignore vendored
View file

@ -156,3 +156,4 @@ _ideia/_triage.json
# i18n audit artifact (generated by scripts/i18n/audit-dashboard-pages.mjs)
scripts/i18n/_audit.json
scripts/i18n/_pending-keys.json

View file

@ -0,0 +1,130 @@
#!/usr/bin/env node
/**
* Extract NEW i18n keys created by subagents from git diff.
*
* For each modified .tsx file, walk the unified diff and pair "-" lines
* (containing literal English text) with their following "+" lines
* (now using `t("key")`). When we find a stable pairing, record the key
* and its original English value.
*
* The output is a per-namespace map ready to merge into en.json.
*/
import { execSync } from "node:child_process";
const diff = execSync('git diff --unified=0 "src/app/(dashboard)"', {
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
});
const blocks = diff.split(/^diff --git /m).slice(1);
const allPairs = []; // { file, removed, added }
for (const block of blocks) {
const headLine = block.split("\n")[0];
const fileMatch = headLine.match(/b\/(.+?)$/);
const file = fileMatch ? fileMatch[1] : "?";
const lines = block.split("\n");
// Walk in groups: consecutive "-" lines, then consecutive "+" lines.
// Pair them positionally (removed[i] ↔ added[i]).
let removed = [];
let added = [];
function flush() {
const n = Math.min(removed.length, added.length);
for (let i = 0; i < n; i++) allPairs.push({ file, removed: removed[i], added: added[i] });
// If removed.length > added.length, pair remaining removed with last added (multi-string in one new line)
if (removed.length > added.length && added.length > 0) {
for (let i = added.length; i < removed.length; i++) {
allPairs.push({ file, removed: removed[i], added: added[added.length - 1] });
}
}
removed = [];
added = [];
}
for (const line of lines) {
if (line.startsWith("---") || line.startsWith("+++")) continue;
if (line.startsWith("-")) {
if (added.length) flush();
removed.push(line.slice(1));
} else if (line.startsWith("+")) {
added.push(line.slice(1));
} else {
flush();
}
}
flush();
}
/** Patterns inside JSX or attribute strings */
const T_CALL_RE = /\bt\(\s*["']([^"']+)["']\s*\)/g;
const JSX_TEXT_RE = />([^<>{}\n]+)</g;
const ATTR_RE = /\b(?:title|placeholder|aria-label|alt|label)\s*=\s*["']([^"']+)["']/g;
const newKeys = new Map(); // key -> { value, file }
function recordKey(key, value, file) {
const trimmed = value.trim();
if (!trimmed) return;
// Ignore if value contains JSX expression syntax — those were dynamic
if (/^\{|\}$/.test(trimmed)) return;
if (!newKeys.has(key)) {
newKeys.set(key, { value: trimmed, file });
}
}
for (const { file, removed, added } of allPairs) {
// Extract every t("xxx") key from the "added" line
const tKeys = [...added.matchAll(T_CALL_RE)].map((m) => m[1]);
if (!tKeys.length) continue;
// Extract candidate strings from the "removed" line: JSX text + attr values
const candidates = [];
for (const m of removed.matchAll(JSX_TEXT_RE)) candidates.push(m[1]);
for (const m of removed.matchAll(ATTR_RE)) candidates.push(m[1]);
// Direct strings without surrounding markup (rare)
const stripped = removed
.replace(/<[^<>]+>/g, "")
.replace(/\bt\([^)]*\)/g, "")
.trim();
if (stripped && /[A-Za-z]/.test(stripped)) candidates.push(stripped);
// For each tKey in added, try to align with the candidate at the same index.
// The agents typically replaced strings in the same left-to-right order.
for (let i = 0; i < tKeys.length; i++) {
const candidate = candidates[i] ?? candidates[candidates.length - 1];
if (!candidate) continue;
recordKey(tKeys[i], candidate, file);
}
}
// Group by file's primary namespace via useTranslations() call in file
import { readFileSync } from "node:fs";
function inferNamespaceForFile(file) {
try {
const src = readFileSync(file, "utf8");
const m = src.match(/useTranslations\s*\(\s*["']([^"']+)["']\s*\)/);
return m?.[1] ?? "common";
} catch {
return "common";
}
}
const byNamespace = new Map();
for (const [key, { value, file }] of newKeys) {
const ns = inferNamespaceForFile(file);
if (!byNamespace.has(ns)) byNamespace.set(ns, []);
byNamespace.get(ns).push({ key, value });
}
for (const [ns, items] of byNamespace) {
console.log(`\nNEW_KEYS_FOR_NAMESPACE: ${ns}`);
console.log("{");
for (const { key, value } of items) {
// Escape value for JSON
console.log(` ${JSON.stringify(key)}: ${JSON.stringify(value)},`);
}
console.log("}");
}
console.log(`\nTotal extracted: ${newKeys.size} keys across ${byNamespace.size} namespaces`);

View file

@ -0,0 +1,38 @@
#!/usr/bin/env node
/**
* Merge keys from scripts/i18n/_pending-keys.json into src/i18n/messages/en.json
*
* Format of _pending-keys.json:
* { "namespace": { "key": "value", ... }, ... }
*
* Keys are appended to the END of each namespace block. Existing keys with
* the same name are preserved (we never overwrite).
*/
import { readFileSync, writeFileSync } from "node:fs";
const SRC = "src/i18n/messages/en.json";
const PENDING = "scripts/i18n/_pending-keys.json";
const enJson = JSON.parse(readFileSync(SRC, "utf8"));
const pending = JSON.parse(readFileSync(PENDING, "utf8"));
let added = 0;
let skipped = 0;
for (const [ns, keys] of Object.entries(pending)) {
if (!Object.prototype.hasOwnProperty.call(enJson, ns)) {
console.warn(`! namespace missing in en.json: ${ns} — skipping`);
continue;
}
const target = enJson[ns];
for (const [k, v] of Object.entries(keys)) {
if (Object.prototype.hasOwnProperty.call(target, k)) {
skipped++;
continue;
}
target[k] = v;
added++;
}
}
writeFileSync(SRC, JSON.stringify(enJson, null, 2) + "\n");
console.log(`✓ merged ${added} new keys (${skipped} skipped — already present)`);

View file

@ -603,7 +603,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
<span className="material-symbols-outlined text-[18px]">check_circle</span>
{updateSteps.find((s) => s.step === "complete")?.message || "Update complete!"}
</p>
<p className="text-xs text-text-muted mt-1">Reloading page automatically...</p>
<p className="text-xs text-text-muted mt-1">{t("reloadingPageAutomatically")}</p>
</div>
)}
</div>
@ -788,7 +788,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
<Card>
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="text-base font-semibold">Provider Topology</h2>
<h2 className="text-base font-semibold">{t("providerTopology")}</h2>
<p className="text-xs text-text-muted">
Connected providers routing through OmniRoute in real time
</p>

View file

@ -1279,7 +1279,7 @@ const PermissionsModal = memo(function PermissionsModal({
{/* Max Sessions Limit (T08) */}
<div className="flex items-start justify-between gap-3 p-3 rounded-lg border border-border bg-surface/40">
<div className="flex flex-col gap-1">
<p className="text-sm font-medium text-text-main">Max Active Sessions</p>
<p className="text-sm font-medium text-text-main">{t("maxActiveSessions")}</p>
<p className="text-xs text-text-muted">
0 = unlimited. Return 429 when this key exceeds concurrent sticky sessions.
</p>

View file

@ -3440,13 +3440,13 @@ export default function ProviderDetailPage() {
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h2 className="text-lg font-semibold">{t("connections")}</h2>
{providerId === "codex" && (
<div title="Apply Codex Fast tier to all Codex connections by default">
<div title={t("providerDetailFastTierTooltip")}>
<Toggle
size="sm"
checked={codexGlobalFastServiceTier}
onChange={handleToggleCodexGlobalFastServiceTier}
disabled={savingCodexGlobalFastServiceTier}
label="Fast default"
label={t("providerDetailFastDefaultLabel")}
ariaLabel="Toggle Codex Fast default"
className="rounded-lg border border-sky-500/20 bg-sky-500/5 px-2 py-1"
/>

View file

@ -140,7 +140,7 @@ export default function OneproxyTab() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-text-main">1proxy Free Proxy Marketplace</h2>
<h2 className="text-lg font-semibold text-text-main">{t("oneproxyTitle")}</h2>
<p className="text-sm text-text-muted mt-1">
Fetch and rotate free validated proxies from the 1proxy community platform
</p>
@ -173,7 +173,7 @@ export default function OneproxyTab() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-4">
<div className="text-2xl font-bold text-text-main">{stats.total}</div>
<div className="text-sm text-text-muted">Total Proxies</div>
<div className="text-sm text-text-muted">{t("oneproxyTotalProxies")}</div>
</Card>
<Card className="p-4">
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
@ -183,7 +183,7 @@ export default function OneproxyTab() {
<div className="text-2xl font-bold text-text-main">
{stats.avgQuality != null ? `${stats.avgQuality}` : "—"}
</div>
<div className="text-sm text-text-muted">Avg Quality</div>
<div className="text-sm text-text-muted">{t("oneproxyAvgQuality")}</div>
</Card>
<Card className="p-4">
<div className="text-2xl font-bold text-text-main">

View file

@ -62,16 +62,17 @@ function SectionDescription({
trigger: string;
effect: string;
}) {
const t = useTranslations("settings");
return (
<div className="grid grid-cols-1 gap-2 text-xs text-text-muted sm:grid-cols-3">
<div>
<span className="font-semibold text-text-main">Scope:</span> {scope}
<span className="font-semibold text-text-main">{t("resilienceScope")}</span> {scope}
</div>
<div>
<span className="font-semibold text-text-main">Trigger:</span> {trigger}
<span className="font-semibold text-text-main">{t("resilienceTrigger")}</span> {trigger}
</div>
<div>
<span className="font-semibold text-text-main">Effect:</span> {effect}
<span className="font-semibold text-text-main">{t("resilienceEffect")}</span> {effect}
</div>
</div>
);
@ -203,6 +204,7 @@ function RequestQueueCard({
onSave: (next: RequestQueueSettings) => Promise<void>;
saving: boolean;
}) {
const t = useTranslations("settings");
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(value);
@ -216,7 +218,7 @@ function RequestQueueCard({
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-xl text-primary">speed</span>
<h2 className="text-lg font-bold">Request Queue & Rate</h2>
<h2 className="text-lg font-bold">{t("resilienceRequestQueueTitle")}</h2>
</div>
<SectionDescription
scope="Per request queue"
@ -248,7 +250,7 @@ function RequestQueueCard({
{editing ? (
<>
<BooleanField
label="Auto-enable for API-key providers"
label={t("resilienceAutoEnableApiKeyProviders")}
description="Enable queue protection by default for active API-key connections."
checked={draft.autoEnableApiKeyProviders}
onChange={(autoEnableApiKeyProviders) =>
@ -256,13 +258,13 @@ function RequestQueueCard({
}
/>
<NumberField
label="Requests per minute"
label={t("resilienceRequestsPerMinute")}
value={draft.requestsPerMinute}
min={1}
onChange={(requestsPerMinute) => setDraft((prev) => ({ ...prev, requestsPerMinute }))}
/>
<NumberField
label="Minimum time between requests"
label={t("resilienceMinTimeBetweenRequests")}
value={draft.minTimeBetweenRequestsMs}
suffix="ms"
onChange={(minTimeBetweenRequestsMs) =>
@ -270,7 +272,7 @@ function RequestQueueCard({
}
/>
<NumberField
label="Concurrent requests"
label={t("resilienceConcurrentRequests")}
value={draft.concurrentRequests}
min={1}
onChange={(concurrentRequests) =>
@ -278,7 +280,7 @@ function RequestQueueCard({
}
/>
<NumberField
label="Maximum queue wait time"
label={t("resilienceMaxQueueWaitTime")}
value={draft.maxWaitMs}
min={1}
suffix="ms"
@ -288,31 +290,33 @@ function RequestQueueCard({
) : (
<>
<div className="rounded-xl border border-border bg-bg-subtle p-4">
<div className="text-xs text-text-muted">Auto-enable for API-key providers</div>
<div className="text-xs text-text-muted">
{t("resilienceAutoEnableApiKeyProviders")}
</div>
<div className="mt-1 text-sm font-semibold text-text-main">
{value.autoEnableApiKeyProviders ? "Enabled" : "Disabled"}
</div>
</div>
<div className="rounded-xl border border-border bg-bg-subtle p-4">
<div className="text-xs text-text-muted">Requests per minute</div>
<div className="text-xs text-text-muted">{t("resilienceRequestsPerMinute")}</div>
<div className="mt-1 text-sm font-semibold text-text-main">
{value.requestsPerMinute}
</div>
</div>
<div className="rounded-xl border border-border bg-bg-subtle p-4">
<div className="text-xs text-text-muted">Minimum time between requests</div>
<div className="text-xs text-text-muted">{t("resilienceMinTimeBetweenRequests")}</div>
<div className="mt-1 text-sm font-semibold text-text-main">
{formatMs(value.minTimeBetweenRequestsMs)}
</div>
</div>
<div className="rounded-xl border border-border bg-bg-subtle p-4">
<div className="text-xs text-text-muted">Concurrent requests</div>
<div className="text-xs text-text-muted">{t("resilienceConcurrentRequests")}</div>
<div className="mt-1 text-sm font-semibold text-text-main">
{value.concurrentRequests}
</div>
</div>
<div className="rounded-xl border border-border bg-bg-subtle p-4">
<div className="text-xs text-text-muted">Maximum queue wait time</div>
<div className="text-xs text-text-muted">{t("resilienceMaxQueueWaitTime")}</div>
<div className="mt-1 text-sm font-semibold text-text-main">
{formatMs(value.maxWaitMs)}
</div>
@ -333,6 +337,7 @@ function ConnectionCooldownCard({
onSave: (next: ResilienceResponse["connectionCooldown"]) => Promise<void>;
saving: boolean;
}) {
const t = useTranslations("settings");
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(value);
@ -347,7 +352,7 @@ function ConnectionCooldownCard({
{editing ? (
<>
<NumberField
label="Base cooldown"
label={t("resilienceBaseCooldown")}
value={current.baseCooldownMs}
min={0}
suffix="ms"
@ -356,7 +361,7 @@ function ConnectionCooldownCard({
}
/>
<BooleanField
label="Use upstream retry hints"
label={t("resilienceUseUpstreamRetryHints")}
description="Use upstream retry-after/reset values when available."
checked={current.useUpstreamRetryHints}
onChange={(useUpstreamRetryHints) =>
@ -368,7 +373,9 @@ function ConnectionCooldownCard({
/>
<div className="flex flex-col gap-1">
<label className="flex items-center justify-between gap-2 text-sm">
<span className="text-text-muted">Use upstream 429 hints for breaker cooldown</span>
<span className="text-text-muted">
{t("resilienceUseUpstream429HintsForBreaker")}
</span>
<select
className="rounded border border-border-default bg-surface-1 px-2 py-1 text-sm font-mono"
value={
@ -396,9 +403,9 @@ function ConnectionCooldownCard({
});
}}
>
<option value="default">Default (per provider)</option>
<option value="on">Always on</option>
<option value="off">Always off</option>
<option value="default">{t("resilienceDefaultPerProvider")}</option>
<option value="on">{t("resilienceAlwaysOn")}</option>
<option value="off">{t("resilienceAlwaysOff")}</option>
</select>
</label>
<p className="text-xs text-text-muted">

View file

@ -303,6 +303,7 @@ function StringListEditor({
onChange: (next: string[]) => void;
disabled?: boolean;
}) {
const t = useTranslations("settings");
return (
<div className="flex flex-col gap-1.5">
<span className="text-xs font-medium text-text-main">{label}</span>
@ -324,7 +325,7 @@ function StringListEditor({
size="sm"
icon="close"
disabled={disabled}
aria-label="Remove entry"
aria-label={t("routingRemoveEntry")}
onClick={() => {
const next = [...items];
next.splice(idx, 1);
@ -356,6 +357,7 @@ function OpEditor({
onChange: (next: any) => void;
disabled?: boolean;
}) {
const t = useTranslations("settings");
const updateField = (field: string, value: any) => onChange({ ...op, [field]: value });
const kind = op?.kind as TransformOpKind | undefined;
const opDescription = kind ? OP_KIND_DESCRIPTIONS[kind] : null;
@ -376,14 +378,14 @@ function OpEditor({
return wrap(
<div className="flex flex-col gap-2">
<StringListEditor
label="Needles (substrings to match)"
label={t("routingNeedlesSubstrings")}
hint={FIELD_HINTS.needles}
items={op.needles || []}
onChange={(next) => updateField("needles", next)}
disabled={disabled}
/>
<Toggle
label="Case sensitive"
label={t("routingCaseSensitive")}
description={FIELD_HINTS.caseSensitive}
checked={op.caseSensitive !== false}
onChange={(c) => updateField("caseSensitive", c)}
@ -396,14 +398,14 @@ function OpEditor({
return wrap(
<div className="flex flex-col gap-2">
<StringListEditor
label="Prefixes"
label={t("routingPrefixes")}
hint={FIELD_HINTS.prefixes}
items={op.prefixes || []}
onChange={(next) => updateField("prefixes", next)}
disabled={disabled}
/>
<Toggle
label="Case sensitive"
label={t("routingCaseSensitive")}
description={FIELD_HINTS.caseSensitive}
checked={op.caseSensitive !== false}
onChange={(c) => updateField("caseSensitive", c)}
@ -416,21 +418,21 @@ function OpEditor({
return wrap(
<div className="flex flex-col gap-2">
<Input
label="Match"
label={t("routingMatch")}
hint={FIELD_HINTS.matchLiteral}
value={op.match || ""}
disabled={disabled}
onChange={(e) => updateField("match", e.target.value)}
/>
<Input
label="Replacement"
label={t("routingReplacement")}
hint={FIELD_HINTS.replacementText}
value={op.replacement || ""}
disabled={disabled}
onChange={(e) => updateField("replacement", e.target.value)}
/>
<Toggle
label="Replace all occurrences"
label={t("routingReplaceAllOccurrences")}
description={FIELD_HINTS.allOccurrences}
checked={op.allOccurrences !== false}
onChange={(c) => updateField("allOccurrences", c)}
@ -443,21 +445,21 @@ function OpEditor({
return wrap(
<div className="flex flex-col gap-2">
<Input
label="Pattern (regex)"
label={t("routingPatternRegex")}
hint={FIELD_HINTS.pattern}
value={op.pattern || ""}
disabled={disabled}
onChange={(e) => updateField("pattern", e.target.value)}
/>
<Input
label="Flags"
label={t("routingFlags")}
hint={FIELD_HINTS.regexFlags}
value={op.flags || "g"}
disabled={disabled}
onChange={(e) => updateField("flags", e.target.value)}
/>
<Input
label="Replacement"
label={t("routingReplacement")}
hint={FIELD_HINTS.replacementText}
value={op.replacement || ""}
disabled={disabled}
@ -468,7 +470,7 @@ function OpEditor({
case "drop_block_if_contains":
return wrap(
<StringListEditor
label="Needles"
label={t("routingNeedles")}
hint={FIELD_HINTS.needles}
items={op.needles || []}
onChange={(next) => updateField("needles", next)}
@ -480,7 +482,7 @@ function OpEditor({
return wrap(
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-text-main">Block text</label>
<label className="text-sm font-medium text-text-main">{t("routingBlockText")}</label>
<textarea
rows={3}
value={op.text || ""}
@ -491,7 +493,7 @@ function OpEditor({
<p className="text-xs text-text-muted">{FIELD_HINTS.blockText}</p>
</div>
<Input
label="Idempotency key"
label={t("routingIdempotencyKey")}
hint={FIELD_HINTS.idempotencyKey}
value={op.idempotencyKey || ""}
disabled={disabled}
@ -503,14 +505,14 @@ function OpEditor({
return wrap(
<div className="flex flex-col gap-2">
<Input
label="Entrypoint"
label={t("routingEntrypoint")}
hint={FIELD_HINTS.billingEntrypoint}
value={op.entrypoint || "sdk-cli"}
disabled={disabled}
onChange={(e) => updateField("entrypoint", e.target.value)}
/>
<Select
label="Version format"
label={t("routingVersionFormat")}
hint={FIELD_HINTS.billingVersionFormat}
value={op.versionFormat || "ex-machina"}
disabled={disabled}
@ -521,7 +523,7 @@ function OpEditor({
]}
/>
<Select
label="CCH algorithm"
label={t("routingCchAlgorithm")}
hint={FIELD_HINTS.billingCchAlgo}
value={op.cchAlgo || "sha256-first-user"}
disabled={disabled}
@ -538,7 +540,7 @@ function OpEditor({
return wrap(
<div className="flex flex-col gap-2">
<StringListEditor
label="Words to obfuscate (ZWJ inserted after first char)"
label={t("routingWordsToObfuscate")}
hint={FIELD_HINTS.obfuscateWords}
items={op.words || []}
onChange={(next) => updateField("words", next)}

View file

@ -551,7 +551,7 @@ export default function SystemStorageTab() {
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div>
<p className="text-sm font-medium text-text-main">Logs Settings</p>
<p className="text-sm font-medium text-text-main">{t("logsSettingsTitle")}</p>
<p className="text-xs text-text-muted">
Configure detailed logging and call log pipeline settings
</p>
@ -560,26 +560,26 @@ export default function SystemStorageTab() {
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Detailed Logs Enabled</span>
<p className="text-xs text-text-muted">Enable detailed request/response logging</p>
<span className="font-medium">{t("detailedLogsLabel")}</span>
<p className="text-xs text-text-muted">{t("detailedLogsDesc")}</p>
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Call Log Pipeline</span>
<p className="text-xs text-text-muted">Enable call log processing pipeline</p>
<span className="font-medium">{t("callLogPipelineLabel")}</span>
<p className="text-xs text-text-muted">{t("callLogPipelineDesc")}</p>
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Max Detail Size (KB)</span>
<p className="text-xs text-text-muted">Maximum size for detailed log entries</p>
<span className="font-medium">{t("maxDetailSizeLabel")}</span>
<p className="text-xs text-text-muted">{t("maxDetailSizeDesc")}</p>
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Ring Buffer Size</span>
<p className="text-xs text-text-muted">Size of the ring buffer for logs</p>
<span className="font-medium">{t("ringBufferSizeLabel")}</span>
<p className="text-xs text-text-muted">{t("ringBufferSizeDesc")}</p>
</label>
</div>
</div>
@ -589,7 +589,7 @@ export default function SystemStorageTab() {
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div>
<p className="text-sm font-medium text-text-main">Cache Settings</p>
<p className="text-sm font-medium text-text-main">{t("cacheSettings")}</p>
<p className="text-xs text-text-muted">
Configure semantic and prompt caching behavior
</p>
@ -598,7 +598,7 @@ export default function SystemStorageTab() {
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Semantic Cache Enabled</span>
<span className="font-medium">{t("semanticCacheEnabledLabel")}</span>
<p className="text-xs text-text-muted">
Enable semantic caching for similar requests
</p>
@ -606,13 +606,13 @@ export default function SystemStorageTab() {
</div>
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Semantic Cache Max Size</span>
<p className="text-xs text-text-muted">Maximum number of semantic cache entries</p>
<span className="font-medium">{t("semanticCacheMaxSizeLabel")}</span>
<p className="text-xs text-text-muted">{t("semanticCacheMaxSizeDesc")}</p>
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Semantic Cache TTL</span>
<span className="font-medium">{t("semanticCacheTTLLabel")}</span>
<p className="text-xs text-text-muted">
Time-to-live for semantic cache entries (ms)
</p>
@ -620,20 +620,20 @@ export default function SystemStorageTab() {
</div>
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Prompt Cache Enabled</span>
<p className="text-xs text-text-muted">Enable prompt caching</p>
<span className="font-medium">{t("promptCacheEnabledLabel")}</span>
<p className="text-xs text-text-muted">{t("promptCacheEnabledDesc")}</p>
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Prompt Cache Strategy</span>
<p className="text-xs text-text-muted">Strategy for prompt caching</p>
<span className="font-medium">{t("promptCacheStrategyLabel")}</span>
<p className="text-xs text-text-muted">{t("promptCacheStrategyDesc")}</p>
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-sm">
<span className="font-medium">Always Preserve Client Cache</span>
<p className="text-xs text-text-muted">Client cache preservation policy</p>
<span className="font-medium">{t("alwaysPreserveClientCacheLabel")}</span>
<p className="text-xs text-text-muted">{t("alwaysPreserveClientCacheDesc")}</p>
</label>
</div>
</div>
@ -642,7 +642,7 @@ export default function SystemStorageTab() {
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div>
<p className="text-sm font-medium text-text-main">Log retention policy</p>
<p className="text-sm font-medium text-text-main">{t("logRetentionPolicyTitle")}</p>
<p className="text-xs text-text-muted">
Request logs retain up to <code>CALL_LOGS_TABLE_MAX_ROWS</code> rows (default:
100,000). Proxy logs retain up to <code>PROXY_LOGS_TABLE_MAX_ROWS</code> rows. Older

View file

@ -461,26 +461,29 @@ export default function BudgetTab() {
</div>
<div className="grid grid-cols-2 lg:grid-cols-6 gap-2">
<KpiBlock label="Today" value={formatCurrency(stats.today)} />
<KpiBlock label="This month" value={formatCurrency(stats.month)} />
<KpiBlock label={t("budgetKpiToday")} value={formatCurrency(stats.today)} />
<KpiBlock label={t("budgetKpiThisMonth")} value={formatCurrency(stats.month)} />
<KpiBlock
label="Proj EOM"
label={t("budgetKpiProjEom")}
value={formatCurrency(stats.projectionEom)}
tone={projectionOverBudget ? "amber" : undefined}
hint={projectionOverBudget ? "above limit ⚠" : "on track"}
/>
<KpiBlock
label="Blocked"
label={t("budgetKpiBlocked")}
value={String(stats.counts.blocked)}
tone={stats.counts.blocked > 0 ? "red" : undefined}
/>
<KpiBlock
label="At risk"
label={t("budgetKpiAtRisk")}
value={String(stats.counts.alerting)}
tone={stats.counts.alerting > 0 ? "amber" : undefined}
hint="≥ warning"
/>
<KpiBlock label="Active keys" value={`${stats.counts.active} / ${rows.length}`} />
<KpiBlock
label={t("budgetKpiActiveKeys")}
value={`${stats.counts.active} / ${rows.length}`}
/>
</div>
</Card>
@ -493,7 +496,7 @@ export default function BudgetTab() {
</span>
<input
type="text"
placeholder="Search keys..."
placeholder={t("budgetSearchKeysPlaceholder")}
value={searchQuery}
onChange={(e) => 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"
@ -504,10 +507,10 @@ export default function BudgetTab() {
onChange={(e) => setSortKey(e.target.value as typeof sortKey)}
className="bg-bg-base border border-border rounded-md px-2 py-1.5 text-xs text-text-main cursor-pointer"
>
<option value="usedDesc">Sort: % Used </option>
<option value="todayDesc">Sort: Today $ </option>
<option value="monthDesc">Sort: Month $ </option>
<option value="name">Sort: Name (AZ)</option>
<option value="usedDesc">{t("budgetSortPctUsed")}</option>
<option value="todayDesc">{t("budgetSortTodayDollar")}</option>
<option value="monthDesc">{t("budgetSortMonthDollar")}</option>
<option value="name">{t("budgetSortNameAZ")}</option>
</select>
</div>
@ -601,9 +604,9 @@ export default function BudgetTab() {
<div>Key</div>
<div className="text-right">Today</div>
<div className="text-right">Month</div>
<div className="text-right">Daily lim</div>
<div className="text-right">Monthly lim</div>
<div className="text-right">Used %</div>
<div className="text-right">{t("budgetColDailyLim")}</div>
<div className="text-right">{t("budgetColMonthlyLim")}</div>
<div className="text-right">{t("budgetColUsedPct")}</div>
<div className="text-center">Status</div>
</div>

View file

@ -1103,7 +1103,9 @@
"updateNow": "Update Now",
"updating": "Updating...",
"updateAvailableDesc": "A new version is available. Click to update.",
"updateStarted": "Update started..."
"updateStarted": "Update started...",
"reloadingPageAutomatically": "Reloading page automatically...",
"providerTopology": "Provider Topology"
},
"analytics": {
"title": "Analytics",
@ -1231,7 +1233,8 @@
"permissionsTitle": "Permissions: {name}",
"allowAllDesc": "This key can access all available models.",
"restrictDesc": "This key can access {selectedCount} of {totalModels} models.",
"selectedCount": "{count} selected"
"selectedCount": "{count} selected",
"maxActiveSessions": "Max Active Sessions"
},
"auditLog": {
"title": "Audit Log",
@ -3522,7 +3525,9 @@
"providerSummaryAll": "Total",
"ideProviders": "IDE Providers",
"ideProvidersDesc": "Editors with built-in AI subscription. Use the provider page to import credentials directly from the IDE keychain.",
"noIdeProviders": "No IDE providers match the current filters."
"noIdeProviders": "No IDE providers match the current filters.",
"providerDetailFastTierTooltip": "Apply Codex Fast tier to all Codex connections by default",
"providerDetailFastDefaultLabel": "Fast default"
},
"settings": {
"title": "Settings",
@ -4216,7 +4221,61 @@
"optional": "Optional",
"current": "Current",
"remove": "Remove",
"search": "Search"
"search": "Search",
"oneproxyTitle": "1proxy Free Proxy Marketplace",
"oneproxyTotalProxies": "Total Proxies",
"oneproxyAvgQuality": "Avg Quality",
"resilienceScope": "Scope:",
"resilienceTrigger": "Trigger:",
"resilienceEffect": "Effect:",
"resilienceRequestQueueTitle": "Request Queue & Rate",
"resilienceAutoEnableApiKeyProviders": "Auto-enable for API-key providers",
"resilienceRequestsPerMinute": "Requests per minute",
"resilienceMinTimeBetweenRequests": "Minimum time between requests",
"resilienceConcurrentRequests": "Concurrent requests",
"resilienceMaxQueueWaitTime": "Maximum queue wait time",
"resilienceBaseCooldown": "Base cooldown",
"resilienceUseUpstreamRetryHints": "Use upstream retry hints",
"resilienceDefaultPerProvider": "Default (per provider)",
"resilienceAlwaysOn": "Always on",
"resilienceAlwaysOff": "Always off",
"routingRemoveEntry": "Remove entry",
"routingNeedlesSubstrings": "Needles (substrings to match)",
"routingCaseSensitive": "Case sensitive",
"routingPrefixes": "Prefixes",
"routingMatch": "Match",
"routingReplacement": "Replacement",
"routingReplaceAllOccurrences": "Replace all occurrences",
"routingPatternRegex": "Pattern (regex)",
"routingFlags": "Flags",
"routingNeedles": "Needles",
"routingBlockText": "Block text",
"routingIdempotencyKey": "Idempotency key",
"routingEntrypoint": "Entrypoint",
"routingVersionFormat": "Version format",
"routingCchAlgorithm": "CCH algorithm",
"routingWordsToObfuscate": "Words to obfuscate (ZWJ inserted after first char)",
"logsSettingsTitle": "Logs Settings",
"detailedLogsLabel": "Detailed Logs Enabled",
"detailedLogsDesc": "Enable detailed request/response logging",
"callLogPipelineLabel": "Call Log Pipeline",
"callLogPipelineDesc": "Enable call log processing pipeline",
"maxDetailSizeLabel": "Max Detail Size (KB)",
"maxDetailSizeDesc": "Maximum size for detailed log entries",
"ringBufferSizeLabel": "Ring Buffer Size",
"ringBufferSizeDesc": "Size of the ring buffer for logs",
"semanticCacheEnabledLabel": "Semantic Cache Enabled",
"semanticCacheMaxSizeLabel": "Semantic Cache Max Size",
"semanticCacheMaxSizeDesc": "Maximum number of semantic cache entries",
"semanticCacheTTLLabel": "Semantic Cache TTL",
"promptCacheEnabledLabel": "Prompt Cache Enabled",
"promptCacheEnabledDesc": "Enable prompt caching",
"promptCacheStrategyLabel": "Prompt Cache Strategy",
"promptCacheStrategyDesc": "Strategy for prompt caching",
"alwaysPreserveClientCacheLabel": "Always Preserve Client Cache",
"alwaysPreserveClientCacheDesc": "Client cache preservation policy",
"logRetentionPolicyTitle": "Log retention policy",
"resilienceUseUpstream429HintsForBreaker": "Use upstream 429 hints (breaker)"
},
"contextRtk": {
"title": "RTK Engine",
@ -4830,7 +4889,21 @@
"quotaCutoffsDefaultHint": "Default min remaining: {default}%",
"quotaCutoffsResetAll": "Reset all",
"quotaCutoffsNoWindows": "No quota windows are available for this account yet.",
"quotaThresholdInvalid": "Enter a whole number from 0 to 100."
"quotaThresholdInvalid": "Enter a whole number from 0 to 100.",
"budgetKpiToday": "Today",
"budgetKpiThisMonth": "This month",
"budgetKpiProjEom": "Proj EOM",
"budgetKpiBlocked": "Blocked",
"budgetKpiAtRisk": "At risk",
"budgetKpiActiveKeys": "Active keys",
"budgetSearchKeysPlaceholder": "Search keys...",
"budgetSortPctUsed": "Sort: % Used ↓",
"budgetSortTodayDollar": "Sort: Today $ ↓",
"budgetSortMonthDollar": "Sort: Month $ ↓",
"budgetSortNameAZ": "Sort: Name (AZ)",
"budgetColDailyLim": "Daily lim",
"budgetColMonthlyLim": "Monthly lim",
"budgetColUsedPct": "Used %"
},
"modals": {
"waitingAuth": "Waiting for Authorization",