File size: 4,723 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/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)
  // Apply tag removal repeatedly until stable to prevent incomplete sanitization
  // (e.g. "<scr<script>ipt>" collapsing into "<script>" after a single pass)
  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 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`);