mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
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.
131 lines
4.4 KiB
JavaScript
131 lines
4.4 KiB
JavaScript
#!/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`);
|