diff --git a/.gitignore b/.gitignore index 23659f5073..13d4236f33 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/scripts/i18n/extract-keys-from-diff.mjs b/scripts/i18n/extract-keys-from-diff.mjs new file mode 100644 index 0000000000..7d0aece27e --- /dev/null +++ b/scripts/i18n/extract-keys-from-diff.mjs @@ -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]+) { 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`); diff --git a/scripts/i18n/merge-keys.mjs b/scripts/i18n/merge-keys.mjs new file mode 100644 index 0000000000..a2eaf43a7d --- /dev/null +++ b/scripts/i18n/merge-keys.mjs @@ -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)`); diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index 5c6ad0a89f..7afcfe02a9 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -603,7 +603,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { check_circle {updateSteps.find((s) => s.step === "complete")?.message || "Update complete!"}

-

Reloading page automatically...

+

{t("reloadingPageAutomatically")}

)} @@ -788,7 +788,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
-

Provider Topology

+

{t("providerTopology")}

Connected providers routing through OmniRoute in real time

diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index 8e2a146cbb..6227c3ad9a 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -1279,7 +1279,7 @@ const PermissionsModal = memo(function PermissionsModal({ {/* Max Sessions Limit (T08) */}
-

Max Active Sessions

+

{t("maxActiveSessions")}

0 = unlimited. Return 429 when this key exceeds concurrent sticky sessions.

diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 3ca261e16f..bfc3c06f35 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -3440,13 +3440,13 @@ export default function ProviderDetailPage() {

{t("connections")}

{providerId === "codex" && ( -
+
diff --git a/src/app/(dashboard)/dashboard/settings/components/OneproxyTab.tsx b/src/app/(dashboard)/dashboard/settings/components/OneproxyTab.tsx index b9b16520f6..c313af7dbf 100644 --- a/src/app/(dashboard)/dashboard/settings/components/OneproxyTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/OneproxyTab.tsx @@ -140,7 +140,7 @@ export default function OneproxyTab() {
-

1proxy Free Proxy Marketplace

+

{t("oneproxyTitle")}

Fetch and rotate free validated proxies from the 1proxy community platform

@@ -173,7 +173,7 @@ export default function OneproxyTab() {
{stats.total}
-
Total Proxies
+
{t("oneproxyTotalProxies")}
{stats.active}
@@ -183,7 +183,7 @@ export default function OneproxyTab() {
{stats.avgQuality != null ? `${stats.avgQuality}` : "—"}
-
Avg Quality
+
{t("oneproxyAvgQuality")}
diff --git a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx index bcc92aaeed..ab9434e51e 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx @@ -62,16 +62,17 @@ function SectionDescription({ trigger: string; effect: string; }) { + const t = useTranslations("settings"); return (
- Scope: {scope} + {t("resilienceScope")} {scope}
- Trigger: {trigger} + {t("resilienceTrigger")} {trigger}
- Effect: {effect} + {t("resilienceEffect")} {effect}
); @@ -203,6 +204,7 @@ function RequestQueueCard({ onSave: (next: RequestQueueSettings) => Promise; saving: boolean; }) { + const t = useTranslations("settings"); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value); @@ -216,7 +218,7 @@ function RequestQueueCard({
speed -

Request Queue & Rate

+

{t("resilienceRequestQueueTitle")}

@@ -256,13 +258,13 @@ function RequestQueueCard({ } /> setDraft((prev) => ({ ...prev, requestsPerMinute }))} /> @@ -270,7 +272,7 @@ function RequestQueueCard({ } /> @@ -278,7 +280,7 @@ function RequestQueueCard({ } />
-
Auto-enable for API-key providers
+
+ {t("resilienceAutoEnableApiKeyProviders")} +
{value.autoEnableApiKeyProviders ? "Enabled" : "Disabled"}
-
Requests per minute
+
{t("resilienceRequestsPerMinute")}
{value.requestsPerMinute}
-
Minimum time between requests
+
{t("resilienceMinTimeBetweenRequests")}
{formatMs(value.minTimeBetweenRequestsMs)}
-
Concurrent requests
+
{t("resilienceConcurrentRequests")}
{value.concurrentRequests}
-
Maximum queue wait time
+
{t("resilienceMaxQueueWaitTime")}
{formatMs(value.maxWaitMs)}
@@ -333,6 +337,7 @@ function ConnectionCooldownCard({ onSave: (next: ResilienceResponse["connectionCooldown"]) => Promise; saving: boolean; }) { + const t = useTranslations("settings"); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value); @@ -347,7 +352,7 @@ function ConnectionCooldownCard({ {editing ? ( <> @@ -368,7 +373,9 @@ function ConnectionCooldownCard({ />

diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index 06a3fc578e..5f65d8fc1c 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -303,6 +303,7 @@ function StringListEditor({ onChange: (next: string[]) => void; disabled?: boolean; }) { + const t = useTranslations("settings"); return (

{label} @@ -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(
updateField("needles", next)} disabled={disabled} /> updateField("caseSensitive", c)} @@ -396,14 +398,14 @@ function OpEditor({ return wrap(
updateField("prefixes", next)} disabled={disabled} /> updateField("caseSensitive", c)} @@ -416,21 +418,21 @@ function OpEditor({ return wrap(
updateField("match", e.target.value)} /> updateField("replacement", e.target.value)} /> updateField("allOccurrences", c)} @@ -443,21 +445,21 @@ function OpEditor({ return wrap(
updateField("pattern", e.target.value)} /> updateField("flags", e.target.value)} /> updateField("needles", next)} @@ -480,7 +482,7 @@ function OpEditor({ return wrap(
- +