| #!/usr/bin/env node
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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 = [];
|
|
|
| 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");
|
|
|
|
|
|
|
| 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 && 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();
|
| }
|
|
|
|
|
| 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();
|
|
|
| function recordKey(key, value, file) {
|
| const trimmed = value.trim();
|
| if (!trimmed) return;
|
|
|
| if (/^\{|\}$/.test(trimmed)) return;
|
| if (!newKeys.has(key)) {
|
| newKeys.set(key, { value: trimmed, file });
|
| }
|
| }
|
|
|
| for (const { file, removed, added } of allPairs) {
|
|
|
| const tKeys = [...added.matchAll(T_CALL_RE)].map((m) => m[1]);
|
| if (!tKeys.length) continue;
|
|
|
|
|
| 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]);
|
|
|
|
|
|
|
|
|
| let stripped = removed;
|
| let prev;
|
| do {
|
| prev = stripped;
|
| stripped = stripped.replace(/<[^<>]+>/g, "");
|
| } while (stripped !== prev);
|
| stripped = stripped.replace(/\bt\([^)]*\)/g, "").trim();
|
| if (stripped && /[A-Za-z]/.test(stripped)) candidates.push(stripped);
|
|
|
|
|
|
|
| for (let i = 0; i < tKeys.length; i++) {
|
| const candidate = candidates[i] ?? candidates[candidates.length - 1];
|
| if (!candidate) continue;
|
| recordKey(tKeys[i], candidate, 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) {
|
|
|
| console.log(` ${JSON.stringify(key)}: ${JSON.stringify(value)},`);
|
| }
|
| console.log("}");
|
| }
|
| console.log(`\nTotal extracted: ${newKeys.size} keys across ${byNamespace.size} namespaces`);
|
|
|