@i18nprune/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/dist/adapters-gp1lXp0T.d.ts +12 -0
  4. package/dist/capabilities-x74cD2Hu.d.ts +48 -0
  5. package/dist/cleanup.d.ts +64 -0
  6. package/dist/cleanup.js +3999 -0
  7. package/dist/config.d.ts +201 -0
  8. package/dist/config.js +2865 -0
  9. package/dist/coreContext-DMaWLvmB.d.ts +388 -0
  10. package/dist/fs-BUYD8ZhA.d.ts +20 -0
  11. package/dist/generate.d.ts +487 -0
  12. package/dist/generate.js +9389 -0
  13. package/dist/humanEmit-ygNlYX-S.d.ts +79 -0
  14. package/dist/index-BQuLEQ9b.d.ts +7 -0
  15. package/dist/index-B_ow_Xvr.d.ts +97 -0
  16. package/dist/index-BgG01AKL.d.ts +287 -0
  17. package/dist/index-CIzZl4W8.d.ts +124 -0
  18. package/dist/index-Csm1w7XD.d.ts +58 -0
  19. package/dist/index-DLwTogCo.d.ts +43 -0
  20. package/dist/index-DVT26v11.d.ts +61 -0
  21. package/dist/index-DdjljwMj.d.ts +39 -0
  22. package/dist/index-DeIw-cZd.d.ts +52 -0
  23. package/dist/index-X50E1FIX.d.ts +50 -0
  24. package/dist/index.d.ts +9180 -0
  25. package/dist/index.js +21888 -0
  26. package/dist/init.d.ts +86 -0
  27. package/dist/init.js +848 -0
  28. package/dist/listWindow-XEFxQZi1.d.ts +30 -0
  29. package/dist/localeTargetCodes-BBIQjauw.d.ts +11 -0
  30. package/dist/locales.d.ts +39 -0
  31. package/dist/locales.js +2288 -0
  32. package/dist/missing-BVCvgUC8.d.ts +10 -0
  33. package/dist/missing.d.ts +85 -0
  34. package/dist/missing.js +5892 -0
  35. package/dist/modeResolve-cGVaY5Hh.d.ts +25 -0
  36. package/dist/path-Bfn3SAts.d.ts +11 -0
  37. package/dist/profile-BwOP9WKh.d.ts +9 -0
  38. package/dist/providers-0uMEfT6q.d.ts +82 -0
  39. package/dist/prune-c6hKZCv_.d.ts +33 -0
  40. package/dist/quality.d.ts +36 -0
  41. package/dist/quality.js +3868 -0
  42. package/dist/report-D5-6bVFj.d.ts +8 -0
  43. package/dist/report-schema.d.ts +102 -0
  44. package/dist/report-schema.js +42 -0
  45. package/dist/resumeCandidates-xR13eEwt.d.ts +200 -0
  46. package/dist/root-2-kCaBvQ.d.ts +1110 -0
  47. package/dist/runtime/edge.d.ts +21 -0
  48. package/dist/runtime/edge.js +87 -0
  49. package/dist/runtime/helpers/sync.d.ts +16 -0
  50. package/dist/runtime/helpers/sync.js +117 -0
  51. package/dist/runtime/node.d.ts +24 -0
  52. package/dist/runtime/node.js +204 -0
  53. package/dist/runtime/web.d.ts +21 -0
  54. package/dist/runtime/web.js +84 -0
  55. package/dist/shared.d.ts +1177 -0
  56. package/dist/shared.js +4897 -0
  57. package/dist/sourceContext-1LQg3HiQ.d.ts +36 -0
  58. package/dist/sourceSurface-mDtwGo1E.d.ts +122 -0
  59. package/dist/sync.d.ts +86 -0
  60. package/dist/sync.js +4971 -0
  61. package/dist/syncSegment-Bx6He2Mu.d.ts +149 -0
  62. package/dist/targets-EmtKyr6F.d.ts +23 -0
  63. package/dist/template-CGM-_WLT.d.ts +139 -0
  64. package/dist/translate-CIHYp7wi.d.ts +77 -0
  65. package/dist/types/shared.d.ts +21 -0
  66. package/dist/types/shared.js +1 -0
  67. package/dist/types.d.ts +1345 -0
  68. package/dist/types.js +1 -0
  69. package/dist/validate.d.ts +126 -0
  70. package/dist/validate.js +3717 -0
  71. package/package.json +128 -0
@@ -0,0 +1,3999 @@
1
+ // src/policies/preserve.ts
2
+ function pathMatchesPreserveKey(path, prefix) {
3
+ return path === prefix || path.startsWith(`${prefix}.`) || path.startsWith(`${prefix}[`);
4
+ }
5
+ function isPreservePath(path, policy) {
6
+ if (!policy) return false;
7
+ if (policy.copyKeys?.some((k) => path === k || pathMatchesPreserveKey(path, k))) return true;
8
+ if (policy.copyPrefixes?.some((p) => path === p || path.startsWith(p))) return true;
9
+ return false;
10
+ }
11
+
12
+ // src/shared/reference/paths.ts
13
+ function pathUnderUncertainPrefix(keyPath, prefix) {
14
+ if (keyPath === prefix) return true;
15
+ if (keyPath.startsWith(`${prefix}.`)) return true;
16
+ if (keyPath.startsWith(`${prefix}[`)) return true;
17
+ return false;
18
+ }
19
+ function pathUnderAnyUncertainPrefix(keyPath, prefixes) {
20
+ for (const p of prefixes) {
21
+ if (pathUnderUncertainPrefix(keyPath, p)) return true;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ // src/cleanup/candidates.ts
27
+ function pathUnderRoot(key, roots) {
28
+ for (const r of roots) {
29
+ if (key === r || key.startsWith(`${r}.`) || key.startsWith(`${r}[`)) return true;
30
+ }
31
+ return false;
32
+ }
33
+ function computeCleanupCandidateKeys(input) {
34
+ const allKeyPaths = new Set(input.leaves.map((l) => l.path));
35
+ const used = /* @__PURE__ */ new Set();
36
+ for (const k of allKeyPaths) {
37
+ if (input.usage.resolvedKeys.has(k)) used.add(k);
38
+ }
39
+ const unused = [...allKeyPaths].filter((k) => !used.has(k));
40
+ let candidates = unused.filter((k) => !isPreservePath(k, input.preserve));
41
+ let excludedUncertain = 0;
42
+ if (input.filterUncertainPrefixes) {
43
+ const before = candidates.length;
44
+ candidates = candidates.filter((k) => !pathUnderAnyUncertainPrefix(k, input.uncertainPrefixes));
45
+ excludedUncertain = before - candidates.length;
46
+ }
47
+ return { allKeyPaths, candidates, excludedUncertain };
48
+ }
49
+
50
+ // src/shared/json/clone.ts
51
+ function deepClone(x) {
52
+ if (x === null || typeof x !== "object") return x;
53
+ if (Array.isArray(x)) return x.map((e) => deepClone(e));
54
+ const o = {};
55
+ for (const k of Object.keys(x)) {
56
+ o[k] = deepClone(x[k]);
57
+ }
58
+ return o;
59
+ }
60
+
61
+ // src/shared/json/path.ts
62
+ function splitPath(pathStr) {
63
+ const parts = [];
64
+ const re = /[^.[\]]+|\[\d+\]/g;
65
+ let m;
66
+ const s = pathStr.trim();
67
+ while ((m = re.exec(s)) !== null) {
68
+ const tok = m[0];
69
+ if (tok.startsWith("[") && tok.endsWith("]")) {
70
+ parts.push(Number.parseInt(tok.slice(1, -1), 10));
71
+ } else {
72
+ parts.push(tok);
73
+ }
74
+ }
75
+ return parts;
76
+ }
77
+ function isPlainObject(x) {
78
+ return typeof x === "object" && x !== null && !Array.isArray(x);
79
+ }
80
+ function getAtPath(root, pathStr) {
81
+ let cur = root;
82
+ for (const seg of splitPath(pathStr)) {
83
+ if (cur === null || cur === void 0) return void 0;
84
+ if (typeof seg === "number") {
85
+ if (!Array.isArray(cur)) return void 0;
86
+ cur = cur[seg];
87
+ } else {
88
+ if (!isPlainObject(cur)) return void 0;
89
+ cur = cur[seg];
90
+ }
91
+ }
92
+ return cur;
93
+ }
94
+ function deleteAtPath(root, pathStr) {
95
+ const segs = splitPath(pathStr);
96
+ if (segs.length === 0) return root;
97
+ const clone = deepClone(root);
98
+ let cur = clone;
99
+ for (let i = 0; i < segs.length - 1; i += 1) {
100
+ const seg = segs[i];
101
+ if (typeof seg === "number") {
102
+ if (!Array.isArray(cur)) return clone;
103
+ cur = cur[seg];
104
+ } else {
105
+ if (!isPlainObject(cur)) return clone;
106
+ cur = cur[seg];
107
+ }
108
+ }
109
+ const last = segs[segs.length - 1];
110
+ if (typeof last === "number") {
111
+ if (Array.isArray(cur)) cur.splice(last, 1);
112
+ } else if (isPlainObject(cur)) {
113
+ delete cur[last];
114
+ }
115
+ return clone;
116
+ }
117
+
118
+ // src/shared/json/localeLeafPath.ts
119
+ function isPlainObject2(x) {
120
+ return typeof x === "object" && x !== null && !Array.isArray(x);
121
+ }
122
+ function getLocaleLeafAtPath(root, pathStr) {
123
+ if (isPlainObject2(root) && Object.prototype.hasOwnProperty.call(root, pathStr)) {
124
+ return root[pathStr];
125
+ }
126
+ return getAtPath(root, pathStr);
127
+ }
128
+ function hasLocaleLeafAtPath(root, pathStr) {
129
+ return getLocaleLeafAtPath(root, pathStr) !== void 0;
130
+ }
131
+ function deleteLocaleLeafAtPath(root, pathStr) {
132
+ if (isPlainObject2(root) && Object.prototype.hasOwnProperty.call(root, pathStr)) {
133
+ const clone = deepClone(root);
134
+ delete clone[pathStr];
135
+ return clone;
136
+ }
137
+ return deleteAtPath(root, pathStr);
138
+ }
139
+
140
+ // src/cleanup/apply.ts
141
+ function applyCleanupKeysToLocaleJson(localeJson, keysToRemove) {
142
+ let next = localeJson;
143
+ const removedPaths = [];
144
+ for (const key of keysToRemove) {
145
+ if (hasLocaleLeafAtPath(next, key)) {
146
+ next = deleteLocaleLeafAtPath(next, key);
147
+ removedPaths.push(key);
148
+ }
149
+ }
150
+ return { next, removedPaths };
151
+ }
152
+
153
+ // src/cleanup/stringPresence.ts
154
+ var CLEANUP_MIN_VALUE_STRING_PRESENCE_LENGTH = 4;
155
+ function cleanupStringPresenceProbe(_key, value) {
156
+ const trimmed = value.trim();
157
+ if (trimmed.length >= CLEANUP_MIN_VALUE_STRING_PRESENCE_LENGTH) {
158
+ return trimmed;
159
+ }
160
+ return null;
161
+ }
162
+ function resolveCleanupKeysWithStringPresencePolicy(input) {
163
+ const safeToRemove = [];
164
+ const evidence = [];
165
+ for (const key of input.candidates) {
166
+ const leafValue = input.leaves.find((l) => l.path === key)?.value ?? key;
167
+ if (input.shouldRunStringPresenceForKey && !input.shouldRunStringPresenceForKey({ key, value: leafValue })) {
168
+ safeToRemove.push(key);
169
+ continue;
170
+ }
171
+ const sample = cleanupStringPresenceProbe(key, leafValue);
172
+ if (input.stringPresence === "off" || input.skipStringPresenceCheck || !input.stringPresenceAvailable || sample === null) {
173
+ safeToRemove.push(key);
174
+ continue;
175
+ }
176
+ const hit = input.hasStringPresence(sample);
177
+ if (input.stringPresence === "guard") {
178
+ if (!hit) {
179
+ safeToRemove.push(key);
180
+ continue;
181
+ }
182
+ evidence.push({
183
+ key,
184
+ kind: "guard_skipped",
185
+ locations: input.getStringPresenceLocations(sample, input.stringPresenceMaxHitsPerKey)
186
+ });
187
+ continue;
188
+ }
189
+ if (hit) {
190
+ evidence.push({
191
+ key,
192
+ kind: "warn_hit",
193
+ locations: input.getStringPresenceLocations(sample, input.stringPresenceMaxHitsPerKey)
194
+ });
195
+ }
196
+ safeToRemove.push(key);
197
+ }
198
+ return { safeToRemove, evidence };
199
+ }
200
+
201
+ // src/shared/locales/layout/requireStructure.ts
202
+ function localesMode(config) {
203
+ return config.mode ?? "flat_file";
204
+ }
205
+ function resolveLocalesStructure(config) {
206
+ if (config.structure !== void 0) {
207
+ return config.structure;
208
+ }
209
+ if (localesMode(config) === "flat_file") {
210
+ return "locale_file";
211
+ }
212
+ throw new Error("locales.structure is required when locales.mode is locale_directory");
213
+ }
214
+
215
+ // src/shared/locales/layout/resolveLayout.ts
216
+ function resolveLocalesLayout(config, directoryAbsolute) {
217
+ const mode = localesMode(config);
218
+ const structure = resolveLocalesStructure(config);
219
+ return { mode, structure, directoryAbsolute, config };
220
+ }
221
+ function resolveLocalesLayoutFromContext(ctx) {
222
+ return resolveLocalesLayout(ctx.config.locales, ctx.paths.localesDir);
223
+ }
224
+ function isLocalesLayoutReadSupported(layout) {
225
+ return layout.mode === "flat_file" && layout.structure === "locale_file" || layout.mode === "locale_directory" && (layout.structure === "locale_per_dir" || layout.structure === "feature_bundle");
226
+ }
227
+ function isLocalesLayoutWriteSupported(layout) {
228
+ return layout.mode === "flat_file" && layout.structure === "locale_file" || layout.mode === "locale_directory" && (layout.structure === "locale_per_dir" || layout.structure === "feature_bundle");
229
+ }
230
+
231
+ // src/shared/languages/normalize.ts
232
+ function normalizeLanguageCode(code) {
233
+ return code.trim().toLowerCase().replace(/_/g, "-");
234
+ }
235
+
236
+ // src/shared/path/posix.ts
237
+ function toPosixPath(value) {
238
+ return value.replace(/\\/g, "/");
239
+ }
240
+
241
+ // src/shared/constants/issueCodes.ts
242
+ var ISSUE_SCAN_DYNAMIC_KEY_SITES = "i18nprune.scan.dynamic_key_sites";
243
+ var ISSUE_CLEANUP_UNCERTAIN_PATHS_EXCLUDED = "i18nprune.cleanup.uncertain_paths_excluded";
244
+ var ISSUE_CLEANUP_RIPGREP_UNAVAILABLE = "i18nprune.cleanup.ripgrep_unavailable";
245
+ var ISSUE_IO_READ_FAILED = "i18nprune.io.read_failed";
246
+
247
+ // src/shared/path/platform.ts
248
+ function normalizePathKeyForCache(projectRoot) {
249
+ return toPosixPath(projectRoot).normalize("NFC").toLowerCase();
250
+ }
251
+
252
+ // src/shared/locales/enumerate/parseSegmentLocale.ts
253
+ function localeCodeForSegment(structure, path, segment) {
254
+ if (structure === "locale_file") {
255
+ if (segment.relativePath.includes("/")) return null;
256
+ return path.basename(segment.absolutePath, ".json");
257
+ }
258
+ if (structure === "locale_per_dir") {
259
+ const slash = segment.relativePath.indexOf("/");
260
+ if (slash < 0) return null;
261
+ const locale = segment.relativePath.slice(0, slash);
262
+ return locale.length > 0 ? locale : null;
263
+ }
264
+ if (structure === "feature_bundle") {
265
+ return path.basename(segment.absolutePath, ".json");
266
+ }
267
+ return null;
268
+ }
269
+
270
+ // src/shared/locales/enumerate/resolveSegmentPath.ts
271
+ function localeSegmentRefFromAbsolute(input) {
272
+ const { layout, path, absolutePath } = input;
273
+ let relativePath = path.relative(layout.directoryAbsolute, absolutePath);
274
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
275
+ return null;
276
+ }
277
+ relativePath = relativePath.replace(/\\/g, "/");
278
+ if (!relativePath.endsWith(".json")) {
279
+ return null;
280
+ }
281
+ const locale = localeCodeForSegment(layout.structure, path, { absolutePath, relativePath });
282
+ if (locale === null) return null;
283
+ return { locale, relativePath, absolutePath: toPosixPath(absolutePath) };
284
+ }
285
+
286
+ // src/shared/errors/internal.ts
287
+ var I18nPruneError = class extends Error {
288
+ code;
289
+ issueCode;
290
+ constructor(message, code, options) {
291
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
292
+ this.name = "I18nPruneError";
293
+ this.code = code;
294
+ this.issueCode = options?.issueCode;
295
+ }
296
+ };
297
+
298
+ // src/runtime/helpers/sync/assert.ts
299
+ function isThenable(value) {
300
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
301
+ }
302
+ function assertSyncPortResult(value, label, at) {
303
+ if (isThenable(value)) {
304
+ throw new I18nPruneError(
305
+ `Synchronous ${label} requires a plain value (got a Promise at ${at})`,
306
+ "USAGE"
307
+ );
308
+ }
309
+ return value;
310
+ }
311
+
312
+ // src/runtime/helpers/sync/fs.ts
313
+ function readRuntimeFsTextSync(filePath, fs) {
314
+ return assertSyncPortResult(fs.readText(filePath), "fs.readText", filePath);
315
+ }
316
+ function existsRuntimeFsSync(filePath, fs) {
317
+ return assertSyncPortResult(fs.exists(filePath), "fs.exists", filePath);
318
+ }
319
+ function listRuntimeFsDirSync(dirPath, fs) {
320
+ return assertSyncPortResult(fs.listDir(dirPath), "fs.listDir", dirPath);
321
+ }
322
+
323
+ // src/shared/constants/locales.ts
324
+ var MAX_LOCALE_SEGMENT_TREE_DEPTH = 16;
325
+
326
+ // src/shared/locales/enumerate/walkJsonTree.ts
327
+ function posixRelative(pathApi, root, absolute) {
328
+ let rel = pathApi.relative(root, absolute);
329
+ if (rel.startsWith("..") || pathApi.isAbsolute(rel)) {
330
+ rel = pathApi.basename(absolute);
331
+ }
332
+ return rel.replace(/\\/g, "/");
333
+ }
334
+ function walkLocaleJsonSegments(input) {
335
+ const { fs, path, rootAbsolute, recursive } = input;
336
+ const maxDepth = input.maxDepth ?? MAX_LOCALE_SEGMENT_TREE_DEPTH;
337
+ const out = [];
338
+ function visit(dirAbsolute, depth) {
339
+ if (!existsRuntimeFsSync(dirAbsolute, fs)) return;
340
+ const entries = listRuntimeFsDirSync(dirAbsolute, fs);
341
+ for (const entry of entries) {
342
+ const childAbsolute = path.join(dirAbsolute, entry.name);
343
+ if (entry.kind === "file" && entry.name.endsWith(".json")) {
344
+ out.push({
345
+ absolutePath: childAbsolute,
346
+ relativePath: posixRelative(path, rootAbsolute, childAbsolute)
347
+ });
348
+ } else if (recursive && entry.kind === "directory" && depth < maxDepth) {
349
+ visit(childAbsolute, depth + 1);
350
+ }
351
+ }
352
+ }
353
+ visit(rootAbsolute, 0);
354
+ return out;
355
+ }
356
+
357
+ // src/shared/locales/diagnostics/structuralParity.ts
358
+ function localeStructuralSlot(structure, relativePath) {
359
+ if (structure === "locale_per_dir") {
360
+ const slash = relativePath.indexOf("/");
361
+ if (slash < 0) return null;
362
+ const slot = relativePath.slice(slash + 1);
363
+ return slot.length > 0 ? slot : null;
364
+ }
365
+ if (structure === "feature_bundle") {
366
+ const slash = relativePath.lastIndexOf("/");
367
+ if (slash < 0) return null;
368
+ const slot = relativePath.slice(0, slash);
369
+ return slot.length > 0 ? slot : null;
370
+ }
371
+ return null;
372
+ }
373
+ function slotsByLocale(structure, segments) {
374
+ const byLocale = /* @__PURE__ */ new Map();
375
+ for (const segment of segments) {
376
+ const slot = localeStructuralSlot(structure, segment.relativePath);
377
+ if (slot === null) continue;
378
+ let set = byLocale.get(segment.locale);
379
+ if (!set) {
380
+ set = /* @__PURE__ */ new Set();
381
+ byLocale.set(segment.locale, set);
382
+ }
383
+ set.add(slot);
384
+ }
385
+ return byLocale;
386
+ }
387
+ function pickReferenceLocale(byLocale, preferred) {
388
+ if (preferred !== void 0 && byLocale.has(preferred)) return preferred;
389
+ let best = null;
390
+ let bestSize = -1;
391
+ for (const [locale, slots] of byLocale) {
392
+ if (slots.size > bestSize) {
393
+ best = locale;
394
+ bestSize = slots.size;
395
+ }
396
+ }
397
+ return best;
398
+ }
399
+ function collectLocaleStructuralParityDiagnostics(input) {
400
+ const { structure, segments } = input;
401
+ if (structure !== "locale_per_dir" && structure !== "feature_bundle") {
402
+ return [];
403
+ }
404
+ const byLocale = slotsByLocale(structure, segments);
405
+ if (byLocale.size < 2) return [];
406
+ const reference = pickReferenceLocale(byLocale, input.referenceLocale);
407
+ if (reference === null) return [];
408
+ const referenceSlots = byLocale.get(reference);
409
+ if (!referenceSlots || referenceSlots.size === 0) return [];
410
+ const diagnostics = [];
411
+ for (const [locale, slots] of byLocale) {
412
+ if (locale === reference) continue;
413
+ for (const slot of referenceSlots) {
414
+ if (!slots.has(slot)) {
415
+ diagnostics.push({
416
+ level: "warn",
417
+ code: "locale_structure_slot_missing",
418
+ message: `locale ${locale} is missing segment slot ${slot} (present for reference locale ${reference})`
419
+ });
420
+ }
421
+ }
422
+ for (const slot of slots) {
423
+ if (!referenceSlots.has(slot)) {
424
+ diagnostics.push({
425
+ level: "warn",
426
+ code: "locale_structure_slot_extra",
427
+ message: `locale ${locale} has extra segment slot ${slot} (not present for reference locale ${reference})`
428
+ });
429
+ }
430
+ }
431
+ }
432
+ diagnostics.sort((a, b) => a.message.localeCompare(b.message));
433
+ return diagnostics;
434
+ }
435
+
436
+ // src/shared/locales/enumerate/listLocaleSegments.ts
437
+ function listLocaleSegments(input) {
438
+ const diagnostics = [];
439
+ const { layout, fs, path } = input;
440
+ const recursive = layout.structure !== "locale_file";
441
+ const walked = walkLocaleJsonSegments({
442
+ fs,
443
+ path,
444
+ rootAbsolute: layout.directoryAbsolute,
445
+ recursive
446
+ });
447
+ const segments = [];
448
+ for (const segment of walked) {
449
+ const locale = localeCodeForSegment(layout.structure, path, segment);
450
+ if (locale === null) continue;
451
+ segments.push({
452
+ locale,
453
+ relativePath: segment.relativePath,
454
+ absolutePath: segment.absolutePath
455
+ });
456
+ }
457
+ segments.sort((a, b) => {
458
+ const byLocale = a.locale.localeCompare(b.locale);
459
+ if (byLocale !== 0) return byLocale;
460
+ return a.relativePath.localeCompare(b.relativePath);
461
+ });
462
+ diagnostics.push(
463
+ ...collectLocaleStructuralParityDiagnostics({
464
+ structure: layout.structure,
465
+ segments
466
+ })
467
+ );
468
+ return { segments, diagnostics };
469
+ }
470
+
471
+ // src/shared/locales/enumerate/fromContext.ts
472
+ function listLocaleSegmentsFromContext(ctx) {
473
+ return listLocaleSegments({
474
+ layout: resolveLocalesLayoutFromContext(ctx),
475
+ fs: ctx.adapters.fs,
476
+ path: ctx.adapters.path
477
+ });
478
+ }
479
+
480
+ // src/shared/locales/targets/context.ts
481
+ function normalizeCode(code) {
482
+ return normalizeLanguageCode(code);
483
+ }
484
+ function listLocaleSegmentTargets(ctx) {
485
+ const { segments } = listLocaleSegmentsFromContext(ctx);
486
+ return segments.map((segment) => ({
487
+ ...segment,
488
+ reportKey: segment.relativePath
489
+ }));
490
+ }
491
+ function sourceLocaleCodeFromContext(ctx) {
492
+ const layout = resolveLocalesLayoutFromContext(ctx);
493
+ const ref = localeSegmentRefFromAbsolute({
494
+ layout,
495
+ path: ctx.adapters.path,
496
+ absolutePath: ctx.paths.sourceLocale
497
+ });
498
+ if (ref !== null) {
499
+ return normalizeCode(ref.locale);
500
+ }
501
+ return normalizeCode(ctx.adapters.path.basename(ctx.paths.sourceLocale, ".json"));
502
+ }
503
+ function segmentsForLocaleCode(ctx, localeCode) {
504
+ const want = normalizeCode(localeCode);
505
+ return listLocaleSegmentTargets(ctx).filter((s) => normalizeCode(s.locale) === want);
506
+ }
507
+ function primarySegmentForLocale(ctx, localeCode) {
508
+ const segments = segmentsForLocaleCode(ctx, localeCode);
509
+ if (segments.length === 0) return void 0;
510
+ const sourceResolved = ctx.adapters.path.resolve(ctx.paths.sourceLocale);
511
+ const sourceMatch = segments.find((s) => ctx.adapters.path.resolve(s.absolutePath) === sourceResolved);
512
+ if (sourceMatch) return sourceMatch;
513
+ return segments.slice().sort((a, b) => a.relativePath.localeCompare(b.relativePath))[0];
514
+ }
515
+
516
+ // src/shared/locales/leaves/walk/translationSurfaceWalk.ts
517
+ function isPlainObject3(x) {
518
+ return typeof x === "object" && x !== null && !Array.isArray(x);
519
+ }
520
+ function isStructuredLocaleLeafNode(x) {
521
+ return isPlainObject3(x) && typeof x.value === "string";
522
+ }
523
+ function readOptionalStatus(o) {
524
+ const s = o.status;
525
+ if (typeof s !== "string" || !s.trim()) return void 0;
526
+ return s;
527
+ }
528
+ function readOptionalLeafSource(o) {
529
+ const s = o.source;
530
+ if (typeof s !== "string" || !s.trim()) return void 0;
531
+ return s;
532
+ }
533
+ function readConfidence(o) {
534
+ const c = o.confidence;
535
+ if (c === null || c === void 0) return null;
536
+ if (typeof c !== "number" || !Number.isFinite(c)) return null;
537
+ const clamped = Math.max(0, Math.min(1, c));
538
+ return Math.round(clamped * 100) / 100;
539
+ }
540
+ function readNeedsReview(o) {
541
+ if (!("needsReview" in o)) return null;
542
+ return typeof o.needsReview === "boolean" ? o.needsReview : null;
543
+ }
544
+ function readNeedsTranslationAgain(o) {
545
+ if (!("needsTranslationAgain" in o)) return null;
546
+ return typeof o.needsTranslationAgain === "boolean" ? o.needsTranslationAgain : null;
547
+ }
548
+ function isCompleteStructuredLocaleLeafMeta(node) {
549
+ if (!isStructuredLocaleLeafNode(node)) return false;
550
+ const o = node;
551
+ if (typeof o.status !== "string" || !o.status.trim()) return false;
552
+ if (!(o.confidence === null || typeof o.confidence === "number" && Number.isFinite(o.confidence))) return false;
553
+ if (typeof o.needsReview !== "boolean") return false;
554
+ if (typeof o.needsTranslationAgain !== "boolean") return false;
555
+ if (typeof o.source !== "string" || !o.source.trim()) return false;
556
+ return true;
557
+ }
558
+ function pushStructuredRow(out, prefix, root, fileOrigin) {
559
+ out.push({
560
+ path: prefix,
561
+ value: root.value,
562
+ shape: "structured",
563
+ status: readOptionalStatus(root),
564
+ confidence: readConfidence(root),
565
+ needsReview: readNeedsReview(root),
566
+ needsTranslationAgain: readNeedsTranslationAgain(root),
567
+ source: readOptionalLeafSource(root),
568
+ structuredMetaComplete: isCompleteStructuredLocaleLeafMeta(root),
569
+ ...fileOrigin ? { fileOrigin } : {}
570
+ });
571
+ }
572
+ function collectTranslationSurfaceLeaves(root, prefix = "", out = [], fileOrigin) {
573
+ if (typeof root === "string") {
574
+ if (prefix) {
575
+ out.push({
576
+ path: prefix,
577
+ value: root,
578
+ shape: "legacy_string",
579
+ confidence: null,
580
+ needsReview: null,
581
+ ...fileOrigin ? { fileOrigin } : {}
582
+ });
583
+ }
584
+ return out;
585
+ }
586
+ if (isStructuredLocaleLeafNode(root)) {
587
+ if (prefix) pushStructuredRow(out, prefix, root, fileOrigin);
588
+ return out;
589
+ }
590
+ if (Array.isArray(root)) {
591
+ root.forEach((item, i) => {
592
+ const p = prefix ? `${prefix}[${i}]` : `[${i}]`;
593
+ collectTranslationSurfaceLeaves(item, p, out, fileOrigin);
594
+ });
595
+ return out;
596
+ }
597
+ if (isPlainObject3(root)) {
598
+ for (const k of Object.keys(root)) {
599
+ const p = prefix ? `${prefix}.${k}` : k;
600
+ collectTranslationSurfaceLeaves(root[k], p, out, fileOrigin);
601
+ }
602
+ }
603
+ return out;
604
+ }
605
+
606
+ // src/shared/json/parse.ts
607
+ var I18nPruneJsonParseError = class extends I18nPruneError {
608
+ filePath;
609
+ line;
610
+ column;
611
+ offset;
612
+ constructor(input) {
613
+ const options = input.issueCode !== void 0 ? { cause: input.cause, issueCode: input.issueCode } : { cause: input.cause };
614
+ super(input.message, input.code, options);
615
+ this.name = "I18nPruneJsonParseError";
616
+ this.filePath = input.filePath;
617
+ this.line = input.location.line;
618
+ this.column = input.location.column;
619
+ this.offset = input.location.offset;
620
+ }
621
+ };
622
+ function locationFromOffset(text, offset) {
623
+ let line = 1;
624
+ let column = 1;
625
+ const capped = Math.max(0, Math.min(offset, text.length));
626
+ for (let i = 0; i < capped; i += 1) {
627
+ if (text.charCodeAt(i) === 10) {
628
+ line += 1;
629
+ column = 1;
630
+ } else {
631
+ column += 1;
632
+ }
633
+ }
634
+ return { line, column, offset };
635
+ }
636
+ function getJsonParseLocation(error, text) {
637
+ const message = error instanceof Error ? error.message : String(error);
638
+ const positionMatch = /\bposition\s+(\d+)\b/i.exec(message);
639
+ if (positionMatch?.[1]) {
640
+ return locationFromOffset(text, Number.parseInt(positionMatch[1], 10));
641
+ }
642
+ const lineColumnMatch = /\bline\s+(\d+)\s+column\s+(\d+)\b/i.exec(message);
643
+ if (lineColumnMatch?.[1] && lineColumnMatch[2]) {
644
+ return {
645
+ line: Number.parseInt(lineColumnMatch[1], 10),
646
+ column: Number.parseInt(lineColumnMatch[2], 10)
647
+ };
648
+ }
649
+ return {};
650
+ }
651
+ function formatJsonParseMessage(filePath, location, cause) {
652
+ const subject = filePath ? `Invalid JSON in ${filePath}` : "Invalid JSON";
653
+ const at = location.line !== void 0 && location.column !== void 0 ? ` at line ${String(location.line)}, column ${String(location.column)}` : location.offset !== void 0 ? ` at offset ${String(location.offset)}` : "";
654
+ const detail = cause instanceof Error ? cause.message : String(cause);
655
+ return `${subject}${at}: ${detail}`;
656
+ }
657
+ function parseJsonText(text, options = {}) {
658
+ try {
659
+ return JSON.parse(text);
660
+ } catch (cause) {
661
+ const location = getJsonParseLocation(cause, text);
662
+ throw new I18nPruneJsonParseError({
663
+ message: formatJsonParseMessage(options.filePath, location, cause),
664
+ code: options.code ?? "IO",
665
+ cause,
666
+ location,
667
+ ...options.issueCode !== void 0 ? { issueCode: options.issueCode } : {},
668
+ ...options.filePath !== void 0 ? { filePath: options.filePath } : {}
669
+ });
670
+ }
671
+ }
672
+ function tryParseJsonText(text, options = {}) {
673
+ try {
674
+ return { ok: true, data: parseJsonText(text, options) };
675
+ } catch (error) {
676
+ if (error instanceof I18nPruneJsonParseError) return { ok: false, error };
677
+ throw error;
678
+ }
679
+ }
680
+
681
+ // src/shared/locales/leaves/segmentSource/localeSegmentSourceForFile.ts
682
+ function localeSegmentSourceForFile(input) {
683
+ const { path, absoluteFile, localesDir, structure } = input;
684
+ let relativePath = path.relative(localesDir, absoluteFile);
685
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
686
+ relativePath = path.basename(absoluteFile);
687
+ }
688
+ relativePath = relativePath.replace(/\\/g, "/");
689
+ const locale = localeCodeForSegment(structure, path, { absolutePath: absoluteFile, relativePath });
690
+ if (locale === null) return null;
691
+ return { file: absoluteFile, locale, relativePath };
692
+ }
693
+
694
+ // src/shared/locales/read/flatFileSurface.ts
695
+ function readFlatLocaleJsonSurface(input) {
696
+ const diagnostics = [];
697
+ const emit = (d) => {
698
+ diagnostics.push(d);
699
+ };
700
+ try {
701
+ const text = readRuntimeFsTextSync(input.absoluteFile, input.fs);
702
+ let json;
703
+ try {
704
+ json = parseJsonText(text, {
705
+ filePath: input.absoluteFile,
706
+ code: "IO",
707
+ issueCode: ISSUE_IO_READ_FAILED
708
+ });
709
+ } catch (e) {
710
+ const message = e instanceof Error ? e.message : String(e);
711
+ emit({ level: "error", code: "locale_json_parse_failed", message, path: input.absoluteFile });
712
+ return { ok: false, leaves: [], diagnostics };
713
+ }
714
+ const fileOrigin = localeSegmentSourceForFile({
715
+ path: input.path,
716
+ absoluteFile: input.absoluteFile,
717
+ localesDir: input.localesDir,
718
+ structure: input.structure
719
+ });
720
+ const leaves = collectTranslationSurfaceLeaves(json, "", [], fileOrigin ?? void 0);
721
+ return { ok: true, document: json, leaves, text, diagnostics };
722
+ } catch (e) {
723
+ const message = e instanceof Error ? e.message : String(e);
724
+ emit({ level: "error", code: "locale_fs_read_failed", message, path: input.absoluteFile });
725
+ return { ok: false, leaves: [], diagnostics };
726
+ }
727
+ }
728
+
729
+ // src/shared/locales/read/bundle.ts
730
+ function readLocaleBundle(input) {
731
+ const diagnostics = [];
732
+ const emit = (d) => {
733
+ diagnostics.push(d);
734
+ };
735
+ if (!isLocalesLayoutReadSupported(input.layout)) {
736
+ emit({
737
+ level: "error",
738
+ code: "locale_layout_unsupported",
739
+ message: `locale read is not implemented for mode=${input.layout.mode} structure=${input.layout.structure}`,
740
+ path: input.absoluteFile
741
+ });
742
+ return { ok: false, leaves: [], diagnostics };
743
+ }
744
+ const segmentRef = localeSegmentRefFromAbsolute({
745
+ layout: input.layout,
746
+ path: input.path,
747
+ absolutePath: input.absoluteFile
748
+ });
749
+ if (segmentRef === null) {
750
+ emit({
751
+ level: "warn",
752
+ code: "locale_read_path_layout_mismatch",
753
+ message: `path does not match configured layout mode=${input.layout.mode} structure=${input.layout.structure}`,
754
+ path: input.absoluteFile
755
+ });
756
+ return { ok: false, leaves: [], diagnostics };
757
+ }
758
+ return readFlatLocaleJsonSurface({
759
+ fs: input.fs,
760
+ path: input.path,
761
+ absoluteFile: input.absoluteFile,
762
+ localesDir: input.layout.directoryAbsolute,
763
+ structure: input.layout.structure,
764
+ onDiagnostic: input.onDiagnostic
765
+ });
766
+ }
767
+
768
+ // src/shared/locales/read/cache.ts
769
+ function invalidateLocaleReadCacheForAbsolutePath(ctx, absolutePath) {
770
+ ctx.localeRead.segments.delete(absolutePath);
771
+ const layout = resolveLocalesLayoutFromContext(ctx);
772
+ const ref = localeSegmentRefFromAbsolute({
773
+ layout,
774
+ path: ctx.adapters.path,
775
+ absolutePath
776
+ });
777
+ if (ref !== null) {
778
+ ctx.localeRead.localeCodes.delete(normalizeLanguageCode(ref.locale));
779
+ }
780
+ }
781
+ function dropLocaleCodeReadCache(ctx, localeCode) {
782
+ ctx.localeRead.localeCodes.delete(normalizeLanguageCode(localeCode));
783
+ }
784
+ function invalidateLocaleReadCacheForLocaleCode(ctx, localeCode) {
785
+ const normalized = normalizeLanguageCode(localeCode);
786
+ dropLocaleCodeReadCache(ctx, normalized);
787
+ for (const segment of segmentsForLocaleCode(ctx, normalized)) {
788
+ ctx.localeRead.segments.delete(segment.absolutePath);
789
+ }
790
+ }
791
+
792
+ // src/shared/locales/read/fromContext.ts
793
+ function storeSegmentSnapshot(ctx, absoluteFile, snapshot) {
794
+ ctx.localeRead.segments.set(absoluteFile, snapshot);
795
+ if (snapshot.ok) {
796
+ const layout = resolveLocalesLayoutFromContext(ctx);
797
+ const ref = localeSegmentRefFromAbsolute({
798
+ layout,
799
+ path: ctx.adapters.path,
800
+ absolutePath: absoluteFile
801
+ });
802
+ if (ref !== null) {
803
+ dropLocaleCodeReadCache(ctx, ref.locale);
804
+ }
805
+ }
806
+ }
807
+ function segmentSnapshotToResult(snapshot) {
808
+ if (snapshot.ok) {
809
+ return {
810
+ ok: true,
811
+ document: snapshot.document,
812
+ leaves: snapshot.leaves,
813
+ text: snapshot.text,
814
+ diagnostics: []
815
+ };
816
+ }
817
+ return { ok: false, leaves: [], diagnostics: snapshot.diagnostics };
818
+ }
819
+ function readLocaleSegmentFromContext(ctx, absoluteFile, onDiagnostic) {
820
+ const cached = ctx.localeRead.segments.get(absoluteFile);
821
+ if (cached !== void 0) {
822
+ return segmentSnapshotToResult(cached);
823
+ }
824
+ const result = readLocaleBundle({
825
+ layout: resolveLocalesLayoutFromContext(ctx),
826
+ fs: ctx.adapters.fs,
827
+ path: ctx.adapters.path,
828
+ absoluteFile,
829
+ onDiagnostic
830
+ });
831
+ if (result.ok) {
832
+ storeSegmentSnapshot(ctx, absoluteFile, {
833
+ ok: true,
834
+ absolutePath: absoluteFile,
835
+ document: result.document,
836
+ leaves: result.leaves,
837
+ text: result.text
838
+ });
839
+ } else {
840
+ storeSegmentSnapshot(ctx, absoluteFile, {
841
+ ok: false,
842
+ absolutePath: absoluteFile,
843
+ diagnostics: result.diagnostics
844
+ });
845
+ }
846
+ return result;
847
+ }
848
+ function storeLocaleCodeSnapshot(ctx, snapshot) {
849
+ ctx.localeRead.localeCodes.set(normalizeLanguageCode(snapshot.localeCode), snapshot);
850
+ }
851
+ function readLocaleCodeSurfaceFromContext(ctx, localeCode, onDiagnostic) {
852
+ const normalized = normalizeLanguageCode(localeCode);
853
+ const cached = ctx.localeRead.localeCodes.get(normalized);
854
+ if (cached !== void 0) {
855
+ return {
856
+ ok: true,
857
+ document: cached.document,
858
+ leaves: cached.leaves,
859
+ text: "",
860
+ diagnostics: []
861
+ };
862
+ }
863
+ const layout = resolveLocalesLayoutFromContext(ctx);
864
+ if (layout.mode === "flat_file") {
865
+ const segment = primarySegmentForLocale(ctx, normalized);
866
+ const absoluteFile = segment?.absolutePath ?? ctx.paths.sourceLocale;
867
+ const read = readLocaleSegmentFromContext(ctx, absoluteFile, onDiagnostic);
868
+ if (!read.ok) return read;
869
+ storeLocaleCodeSnapshot(ctx, {
870
+ localeCode: normalized,
871
+ document: read.document,
872
+ leaves: read.leaves
873
+ });
874
+ return read;
875
+ }
876
+ const diagnostics = [];
877
+ const { segments, diagnostics: listDiagnostics } = listLocaleSegments({
878
+ layout,
879
+ fs: ctx.adapters.fs,
880
+ path: ctx.adapters.path
881
+ });
882
+ diagnostics.push(...listDiagnostics);
883
+ const forLocale = segments.filter((s) => normalizeLanguageCode(s.locale) === normalized);
884
+ if (forLocale.length === 0) {
885
+ const empty = { localeCode: normalized, document: {}, leaves: [] };
886
+ storeLocaleCodeSnapshot(ctx, empty);
887
+ return { ok: true, document: {}, leaves: [], text: "{}", diagnostics };
888
+ }
889
+ const allLeaves = [];
890
+ const documents = [];
891
+ let combinedText = "";
892
+ for (const segment of forLocale) {
893
+ const read = readLocaleSegmentFromContext(ctx, segment.absolutePath, onDiagnostic);
894
+ diagnostics.push(...read.diagnostics);
895
+ if (!read.ok) return { ok: false, leaves: [], diagnostics };
896
+ allLeaves.push(...read.leaves);
897
+ documents.push(read.document);
898
+ combinedText = read.text;
899
+ }
900
+ const document = documents.length === 1 ? documents[0] : documents;
901
+ storeLocaleCodeSnapshot(ctx, {
902
+ localeCode: normalized,
903
+ document,
904
+ leaves: allLeaves
905
+ });
906
+ return {
907
+ ok: true,
908
+ document,
909
+ leaves: allLeaves,
910
+ text: combinedText,
911
+ diagnostics
912
+ };
913
+ }
914
+ function readLocaleJsonFromContextSync(ctx, absoluteFile) {
915
+ const read = readLocaleSegmentFromContext(ctx, absoluteFile);
916
+ if (!read.ok) {
917
+ const message = read.diagnostics.map((d) => d.message).join(" \xB7 ") || "failed to read locale JSON";
918
+ throw new I18nPruneError(message, "IO", { issueCode: ISSUE_IO_READ_FAILED });
919
+ }
920
+ return read.document;
921
+ }
922
+
923
+ // src/shared/json/sortKeys.ts
924
+ function isPlainObject4(x) {
925
+ return typeof x === "object" && x !== null && !Array.isArray(x);
926
+ }
927
+ function sortJsonObjectKeysAsc(value) {
928
+ if (Array.isArray(value)) {
929
+ return value.map((item) => sortJsonObjectKeysAsc(item));
930
+ }
931
+ if (!isPlainObject4(value)) return value;
932
+ const sorted = {};
933
+ for (const key of Object.keys(value).sort()) {
934
+ sorted[key] = sortJsonObjectKeysAsc(value[key]);
935
+ }
936
+ return sorted;
937
+ }
938
+
939
+ // src/shared/locales/write/flatFileLocaleJson.ts
940
+ function writeFlatLocaleJsonDocument(input) {
941
+ const diagnostics = [];
942
+ const emit = (d) => {
943
+ diagnostics.push(d);
944
+ input.onDiagnostic?.(d);
945
+ };
946
+ const indent = input.indent ?? 2;
947
+ let body;
948
+ try {
949
+ body = `${JSON.stringify(sortJsonObjectKeysAsc(input.data), null, indent)}
950
+ `;
951
+ } catch (e) {
952
+ const message = e instanceof Error ? e.message : String(e);
953
+ emit({ level: "error", code: "locale_json_serialize_failed", message, path: input.absoluteFile });
954
+ return { ok: false, diagnostics };
955
+ }
956
+ try {
957
+ input.fs.mkdirp(input.path.dirname(input.absoluteFile));
958
+ input.fs.writeText(input.absoluteFile, body);
959
+ return { ok: true, diagnostics };
960
+ } catch (e) {
961
+ const message = e instanceof Error ? e.message : String(e);
962
+ emit({ level: "error", code: "locale_fs_write_failed", message, path: input.absoluteFile });
963
+ return { ok: false, diagnostics };
964
+ }
965
+ }
966
+
967
+ // src/shared/locales/write/bundle.ts
968
+ function writeLocaleBundle(input) {
969
+ const diagnostics = [];
970
+ const emit = (d) => {
971
+ diagnostics.push(d);
972
+ input.onDiagnostic?.(d);
973
+ };
974
+ if (!isLocalesLayoutWriteSupported(input.layout)) {
975
+ emit({
976
+ level: "error",
977
+ code: "locale_layout_unsupported",
978
+ message: `locale write is not implemented for mode=${input.layout.mode} structure=${input.layout.structure}`,
979
+ path: input.absoluteFile
980
+ });
981
+ return { ok: false, diagnostics };
982
+ }
983
+ const segmentRef = localeSegmentRefFromAbsolute({
984
+ layout: input.layout,
985
+ path: input.path,
986
+ absolutePath: input.absoluteFile
987
+ });
988
+ if (segmentRef === null) {
989
+ emit({
990
+ level: "warn",
991
+ code: "locale_write_path_layout_mismatch",
992
+ message: `path does not match configured layout mode=${input.layout.mode} structure=${input.layout.structure}`,
993
+ path: input.absoluteFile
994
+ });
995
+ return { ok: false, diagnostics };
996
+ }
997
+ return writeFlatLocaleJsonDocument({
998
+ fs: input.fs,
999
+ path: input.path,
1000
+ absoluteFile: input.absoluteFile,
1001
+ data: input.data,
1002
+ indent: input.indent,
1003
+ onDiagnostic: input.onDiagnostic
1004
+ });
1005
+ }
1006
+ function writeLocaleJsonFromContextSync(ctx, absoluteFile, data) {
1007
+ const result = writeLocaleBundle({
1008
+ layout: resolveLocalesLayoutFromContext(ctx),
1009
+ fs: ctx.adapters.fs,
1010
+ path: ctx.adapters.path,
1011
+ absoluteFile,
1012
+ data
1013
+ });
1014
+ if (!result.ok) {
1015
+ const message = result.diagnostics.map((d) => d.message).join(" \xB7 ") || "failed to write locale JSON";
1016
+ throw new I18nPruneError(message, "IO", { issueCode: ISSUE_IO_READ_FAILED });
1017
+ }
1018
+ invalidateLocaleReadCacheForAbsolutePath(ctx, absoluteFile);
1019
+ }
1020
+
1021
+ // src/shared/locales/surface/localeSurface.ts
1022
+ function readLocaleLeavesForCode(ctx, localeCode) {
1023
+ const read = readLocaleCodeSurfaceFromContext(ctx, localeCode);
1024
+ if (!read.ok) return [];
1025
+ return read.leaves;
1026
+ }
1027
+ function readSourceLocaleLeaves(ctx) {
1028
+ return readLocaleLeavesForCode(ctx, sourceLocaleCodeFromContext(ctx));
1029
+ }
1030
+
1031
+ // src/cleanup/sourceSurface.ts
1032
+ function readCleanupSourceLeaves(ctx) {
1033
+ return readLocaleLeavesForCode(ctx, sourceLocaleCodeFromContext(ctx));
1034
+ }
1035
+ function listCleanupSourceSegmentPaths(ctx) {
1036
+ const layout = resolveLocalesLayoutFromContext(ctx);
1037
+ if (layout.mode === "flat_file") {
1038
+ const rel = ctx.adapters.path.relative(ctx.paths.localesDir, ctx.paths.sourceLocale);
1039
+ return [
1040
+ {
1041
+ absolutePath: ctx.paths.sourceLocale,
1042
+ relativePath: rel.length > 0 ? rel : ctx.adapters.path.basename(ctx.paths.sourceLocale)
1043
+ }
1044
+ ];
1045
+ }
1046
+ const sourceCode = sourceLocaleCodeFromContext(ctx);
1047
+ return segmentsForLocaleCode(ctx, sourceCode).map((s) => ({
1048
+ absolutePath: s.absolutePath,
1049
+ relativePath: s.relativePath
1050
+ }));
1051
+ }
1052
+ function listCleanupSourceSegmentsForKeys(ctx, keys) {
1053
+ if (keys.length === 0) return [];
1054
+ return listCleanupSourceSegmentPaths(ctx).filter((segment) => {
1055
+ const raw = readLocaleJsonFromContextSync(ctx, segment.absolutePath);
1056
+ return keys.some((key) => hasLocaleLeafAtPath(raw, key));
1057
+ });
1058
+ }
1059
+
1060
+ // src/shared/reference/resolveConfig.ts
1061
+ var BASE = {
1062
+ treatCommentedCallSitesAsRuntime: false,
1063
+ treatNonSourceFileSitesAsRuntime: false,
1064
+ uncertainKeyPolicy: "protect",
1065
+ stringPresence: "guard",
1066
+ stringPresenceMaxHitsPerKey: 5,
1067
+ respectPreserve: true
1068
+ };
1069
+ function resolveReferenceConfig(operation, config) {
1070
+ const ref = config.reference;
1071
+ const d = ref?.defaults ?? {};
1072
+ const cmds = ref?.commands;
1073
+ const cmd = cmds?.[operation] ?? {};
1074
+ const merged = { ...BASE, ...d, ...cmd };
1075
+ return {
1076
+ treatCommentedCallSitesAsRuntime: merged.treatCommentedCallSitesAsRuntime ?? BASE.treatCommentedCallSitesAsRuntime,
1077
+ treatNonSourceFileSitesAsRuntime: merged.treatNonSourceFileSitesAsRuntime ?? BASE.treatNonSourceFileSitesAsRuntime,
1078
+ uncertainKeyPolicy: merged.uncertainKeyPolicy ?? BASE.uncertainKeyPolicy,
1079
+ stringPresence: merged.stringPresence ?? BASE.stringPresence,
1080
+ stringPresenceMaxHitsPerKey: merged.stringPresenceMaxHitsPerKey ?? BASE.stringPresenceMaxHitsPerKey,
1081
+ respectPreserve: merged.respectPreserve ?? BASE.respectPreserve
1082
+ };
1083
+ }
1084
+
1085
+ // src/extractor/bindings/ident.ts
1086
+ var IMPORT_BINDING_IDENT_PATTERN = String.raw`[$_\p{ID_Start}][$_\p{ID_Continue}]*`;
1087
+
1088
+ // src/extractor/bindings/expand.ts
1089
+ var RX_SAFE_BRACKET_METHOD = new RegExp(String.raw`^` + IMPORT_BINDING_IDENT_PATTERN + String.raw`$`, "u");
1090
+ function isRuntimeBinding(b) {
1091
+ return b.isTypeOnly !== true;
1092
+ }
1093
+ function isSafeBracketMethodKey(method) {
1094
+ if (method.length === 0) return false;
1095
+ if (/['"\\\s\n\r]/.test(method)) return false;
1096
+ return RX_SAFE_BRACKET_METHOD.test(method);
1097
+ }
1098
+ function addMemberCallVariants(effective, objectLocal, method) {
1099
+ effective.add(`${objectLocal}.${method}`);
1100
+ effective.add(`${objectLocal}?.${method}`);
1101
+ if (isSafeBracketMethodKey(method)) {
1102
+ effective.add(`${objectLocal}['${method}']`);
1103
+ effective.add(`${objectLocal}?.['${method}']`);
1104
+ effective.add(`${objectLocal}["${method}"]`);
1105
+ effective.add(`${objectLocal}?.["${method}"]`);
1106
+ }
1107
+ }
1108
+ function methodSuffixesForModuleExpansion(configuredSet, simpleRoots, bindings) {
1109
+ const suffixes = new Set(simpleRoots);
1110
+ for (const b of bindings) {
1111
+ if (!isRuntimeBinding(b) || b.kind !== "named") continue;
1112
+ if (configuredSet.has(b.imported) || configuredSet.has(b.local)) {
1113
+ suffixes.add(b.imported);
1114
+ suffixes.add(b.local);
1115
+ }
1116
+ }
1117
+ return suffixes;
1118
+ }
1119
+ function expandFunctionsWithBindings(configuredFunctions, bindings) {
1120
+ const uniqConfigured = [...new Set(configuredFunctions)];
1121
+ const configuredSet = new Set(uniqConfigured);
1122
+ const effective = new Set(uniqConfigured);
1123
+ const simpleRoots = uniqConfigured.filter((f) => !f.includes("."));
1124
+ for (const b of bindings) {
1125
+ if (!isRuntimeBinding(b)) continue;
1126
+ if (b.kind === "named" && (configuredSet.has(b.imported) || configuredSet.has(b.local))) {
1127
+ effective.add(b.local);
1128
+ }
1129
+ }
1130
+ const methodSuffixes = methodSuffixesForModuleExpansion(configuredSet, simpleRoots, bindings);
1131
+ for (const method of methodSuffixes) {
1132
+ for (const b of bindings) {
1133
+ if (!isRuntimeBinding(b)) continue;
1134
+ if (b.kind === "module") {
1135
+ addMemberCallVariants(effective, b.local, method);
1136
+ }
1137
+ }
1138
+ }
1139
+ const discovered = [...effective].filter((f) => !configuredSet.has(f));
1140
+ discovered.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
1141
+ return [...uniqConfigured, ...discovered];
1142
+ }
1143
+
1144
+ // src/extractor/shared/jslikeTextRanges.ts
1145
+ function skipString(text, start, quote) {
1146
+ let i = start + 1;
1147
+ while (i < text.length) {
1148
+ const ch = text[i];
1149
+ if (ch === "\\") {
1150
+ i += 2;
1151
+ continue;
1152
+ }
1153
+ if (ch === quote) return i + 1;
1154
+ i += 1;
1155
+ }
1156
+ return text.length;
1157
+ }
1158
+ function skipTemplate(text, start) {
1159
+ let i = start + 1;
1160
+ while (i < text.length) {
1161
+ const ch = text[i];
1162
+ if (ch === "\\") {
1163
+ i += 2;
1164
+ continue;
1165
+ }
1166
+ if (ch === "`") return i + 1;
1167
+ if (ch === "$" && text[i + 1] === "{") {
1168
+ i += 2;
1169
+ let depth = 1;
1170
+ while (i < text.length && depth > 0) {
1171
+ const c2 = text[i];
1172
+ if (c2 === "{") depth += 1;
1173
+ else if (c2 === "}") depth -= 1;
1174
+ i += 1;
1175
+ }
1176
+ continue;
1177
+ }
1178
+ i += 1;
1179
+ }
1180
+ return text.length;
1181
+ }
1182
+ function mergeRanges(ranges) {
1183
+ if (ranges.length === 0) return [];
1184
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
1185
+ const out = [];
1186
+ let cur = sorted[0];
1187
+ for (let k = 1; k < sorted.length; k += 1) {
1188
+ const n = sorted[k];
1189
+ if (n.start <= cur.end) {
1190
+ cur = { start: cur.start, end: Math.max(cur.end, n.end) };
1191
+ } else {
1192
+ out.push(cur);
1193
+ cur = n;
1194
+ }
1195
+ }
1196
+ out.push(cur);
1197
+ return out;
1198
+ }
1199
+ function commentRangesForJsLikeText(text) {
1200
+ const ranges = [];
1201
+ let i = 0;
1202
+ const len = text.length;
1203
+ while (i < len) {
1204
+ const c = text[i];
1205
+ if (c === "/" && text[i + 1] === "/") {
1206
+ const start = i;
1207
+ i += 2;
1208
+ while (i < len && text[i] !== "\n") i += 1;
1209
+ ranges.push({ start, end: i });
1210
+ continue;
1211
+ }
1212
+ if (c === "/" && text[i + 1] === "*") {
1213
+ const start = i;
1214
+ i += 2;
1215
+ while (i + 1 < len) {
1216
+ if (text[i] === "*" && text[i + 1] === "/") {
1217
+ i += 2;
1218
+ ranges.push({ start, end: i });
1219
+ break;
1220
+ }
1221
+ i += 1;
1222
+ }
1223
+ if (i >= len && start >= 0 && !ranges.some((r) => r.start === start)) {
1224
+ ranges.push({ start, end: len });
1225
+ }
1226
+ continue;
1227
+ }
1228
+ if (c === "'" || c === '"') {
1229
+ i = skipString(text, i, c);
1230
+ continue;
1231
+ }
1232
+ if (c === "`") {
1233
+ i = skipTemplate(text, i);
1234
+ continue;
1235
+ }
1236
+ i += 1;
1237
+ }
1238
+ return mergeRanges(ranges);
1239
+ }
1240
+ function literalRangesForJsLikeText(text) {
1241
+ const ranges = [];
1242
+ let i = 0;
1243
+ const len = text.length;
1244
+ while (i < len) {
1245
+ const c = text[i];
1246
+ if (c === "/" && text[i + 1] === "/") {
1247
+ i += 2;
1248
+ while (i < len && text[i] !== "\n") i += 1;
1249
+ continue;
1250
+ }
1251
+ if (c === "/" && text[i + 1] === "*") {
1252
+ i += 2;
1253
+ while (i + 1 < len) {
1254
+ if (text[i] === "*" && text[i + 1] === "/") {
1255
+ i += 2;
1256
+ break;
1257
+ }
1258
+ i += 1;
1259
+ }
1260
+ if (i >= len) break;
1261
+ continue;
1262
+ }
1263
+ if (c === "'" || c === '"') {
1264
+ const start = i;
1265
+ i = skipString(text, i, c);
1266
+ if (i > start + 1) ranges.push({ start: start + 1, end: i - 1 });
1267
+ continue;
1268
+ }
1269
+ if (c === "`") {
1270
+ const start = i;
1271
+ i = skipTemplate(text, i);
1272
+ if (i > start + 1) ranges.push({ start: start + 1, end: i - 1 });
1273
+ continue;
1274
+ }
1275
+ i += 1;
1276
+ }
1277
+ return mergeRanges(ranges);
1278
+ }
1279
+ function importBindingScanBlankRanges(text) {
1280
+ return mergeRanges([...commentRangesForJsLikeText(text), ...literalRangesForJsLikeText(text)]);
1281
+ }
1282
+ function offsetInCommentRanges(offset, ranges) {
1283
+ for (const r of ranges) {
1284
+ if (offset >= r.start && offset < r.end) return true;
1285
+ }
1286
+ return false;
1287
+ }
1288
+
1289
+ // src/extractor/bindings/imports.ts
1290
+ var U = "gu";
1291
+ var IMPORT_TYPE_NAMESPACE_RX = new RegExp(
1292
+ String.raw`import\s+type\s*\*\s*as\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
1293
+ U
1294
+ );
1295
+ var IMPORT_TYPE_NAMED_RX = new RegExp(
1296
+ String.raw`import\s+type\s+\{([^}]*)\}\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
1297
+ U
1298
+ );
1299
+ var IMPORT_TYPE_DEFAULT_RX = new RegExp(
1300
+ String.raw`import\s+type\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
1301
+ U
1302
+ );
1303
+ var TS_IMPORT_EQUALS_RX = new RegExp(
1304
+ String.raw`import\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*=\s*require\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
1305
+ U
1306
+ );
1307
+ var NAMESPACE_IMPORT_RX = new RegExp(
1308
+ String.raw`import\s*\*\s*as\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
1309
+ U
1310
+ );
1311
+ var DEFAULT_AND_NAMED_IMPORT_RX = new RegExp(
1312
+ String.raw`import\s+(?!type\b)(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*,\s*\{([^}]*)\}\s+from\s*(['"])(?:(?!\3).|\\.)*\3`,
1313
+ U
1314
+ );
1315
+ var NAMED_IMPORT_ONLY_RX = new RegExp(
1316
+ String.raw`import\s+(?:type\s+)?\{([^}]*)\}\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
1317
+ U
1318
+ );
1319
+ var DEFAULT_IMPORT_ONLY_RX = new RegExp(
1320
+ String.raw`import\s+(?!type\b)(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
1321
+ U
1322
+ );
1323
+ var CJS_REQUIRE_DESTRUCT_RX = new RegExp(
1324
+ String.raw`(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
1325
+ U
1326
+ );
1327
+ var CJS_REQUIRE_MODULE_RX = new RegExp(
1328
+ String.raw`(?:const|let|var)\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*=\s*require\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
1329
+ U
1330
+ );
1331
+ var DYNAMIC_IMPORT_DESTRUCT_RX = new RegExp(
1332
+ String.raw`(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:await\s+)?import\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
1333
+ U
1334
+ );
1335
+ var DYNAMIC_IMPORT_MODULE_RX = new RegExp(
1336
+ String.raw`(?:const|let|var)\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*=\s*(?:await\s+)?import\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
1337
+ U
1338
+ );
1339
+ var RX_AS_PAIR = new RegExp(String.raw`^(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+as\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
1340
+ var RX_IDENT_ONLY = new RegExp(String.raw`^(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
1341
+ var RX_TYPE_INLINE = new RegExp(String.raw`^type\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
1342
+ var RX_DEFAULT_AS_SEGMENT = new RegExp(String.raw`^default\s+as\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
1343
+ var RX_CJS_RENAME = new RegExp(String.raw`^(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*:\s*(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
1344
+ function isTypeOnlyImportStatement(statement) {
1345
+ return /^import\s+type\s+/.test(statement.trimStart());
1346
+ }
1347
+ function blankScanTextForImportBindings(text) {
1348
+ const ranges = importBindingScanBlankRanges(text);
1349
+ if (ranges.length === 0) return text;
1350
+ const chars = [...text];
1351
+ for (const r of ranges) {
1352
+ for (let i = r.start; i < r.end; i += 1) chars[i] = " ";
1353
+ }
1354
+ return chars.join("");
1355
+ }
1356
+ function splitImportSpecifiers(clause) {
1357
+ const out = [];
1358
+ let depth = 0;
1359
+ let cur = "";
1360
+ for (let i = 0; i < clause.length; i += 1) {
1361
+ const c = clause[i];
1362
+ if (c === "{" || c === "[" || c === "(") depth += 1;
1363
+ else if (c === "}" || c === "]" || c === ")") depth -= 1;
1364
+ else if (c === "," && depth === 0) {
1365
+ const t = cur.trim();
1366
+ if (t) out.push(t);
1367
+ cur = "";
1368
+ continue;
1369
+ }
1370
+ cur += c;
1371
+ }
1372
+ const tail = cur.trim();
1373
+ if (tail) out.push(tail);
1374
+ return out;
1375
+ }
1376
+ function parseEsmDefaultAsLocals(clause) {
1377
+ const out = [];
1378
+ for (const raw of splitImportSpecifiers(clause)) {
1379
+ const seg = raw.trim();
1380
+ if (!seg) continue;
1381
+ const m = seg.match(RX_DEFAULT_AS_SEGMENT);
1382
+ if (m) out.push(m[1]);
1383
+ }
1384
+ return out;
1385
+ }
1386
+ function parseEsmNamedImportClause(clause) {
1387
+ const pairs = [];
1388
+ for (const raw of splitImportSpecifiers(clause)) {
1389
+ const seg = raw.trim();
1390
+ if (!seg) continue;
1391
+ if (RX_TYPE_INLINE.test(seg)) continue;
1392
+ if (RX_DEFAULT_AS_SEGMENT.test(seg)) continue;
1393
+ const asMatch = seg.match(RX_AS_PAIR);
1394
+ if (asMatch) {
1395
+ pairs.push({ imported: asMatch[1], local: asMatch[2] });
1396
+ continue;
1397
+ }
1398
+ const ident = seg.match(RX_IDENT_ONLY);
1399
+ if (ident) {
1400
+ const name = ident[1];
1401
+ pairs.push({ imported: name, local: name });
1402
+ }
1403
+ }
1404
+ return pairs;
1405
+ }
1406
+ function parseCjsDestructuringClause(clause) {
1407
+ const pairs = [];
1408
+ for (const raw of splitImportSpecifiers(clause)) {
1409
+ const seg = raw.trim();
1410
+ if (!seg) continue;
1411
+ const rename = seg.match(RX_CJS_RENAME);
1412
+ if (rename) {
1413
+ pairs.push({ imported: rename[1], local: rename[2] });
1414
+ continue;
1415
+ }
1416
+ const ident = seg.match(RX_IDENT_ONLY);
1417
+ if (ident) {
1418
+ const name = ident[1];
1419
+ pairs.push({ imported: name, local: name });
1420
+ }
1421
+ }
1422
+ return pairs;
1423
+ }
1424
+ function pushNamed(out, imported, local, source, typeOnly) {
1425
+ out.push(
1426
+ typeOnly === true ? { kind: "named", imported, local, source, isTypeOnly: true } : { kind: "named", imported, local, source }
1427
+ );
1428
+ }
1429
+ function pushModule(out, local, moduleKind, source, typeOnly) {
1430
+ out.push(
1431
+ typeOnly === true ? { kind: "module", local, moduleKind, source, isTypeOnly: true } : { kind: "module", local, moduleKind, source }
1432
+ );
1433
+ }
1434
+ function scanImportBindings(text) {
1435
+ const scanText = blankScanTextForImportBindings(text);
1436
+ const out = [];
1437
+ let m;
1438
+ const typeNsRe = new RegExp(IMPORT_TYPE_NAMESPACE_RX.source, U);
1439
+ while ((m = typeNsRe.exec(scanText)) !== null) {
1440
+ pushModule(out, m[1], "namespace", "esm", true);
1441
+ }
1442
+ const typeNamedRe = new RegExp(IMPORT_TYPE_NAMED_RX.source, U);
1443
+ while ((m = typeNamedRe.exec(scanText)) !== null) {
1444
+ const clause = m[1];
1445
+ for (const pair of parseEsmNamedImportClause(clause)) {
1446
+ pushNamed(out, pair.imported, pair.local, "esm", true);
1447
+ }
1448
+ for (const local of parseEsmDefaultAsLocals(clause)) {
1449
+ pushModule(out, local, "default", "esm", true);
1450
+ }
1451
+ }
1452
+ const typeDefRe = new RegExp(IMPORT_TYPE_DEFAULT_RX.source, U);
1453
+ while ((m = typeDefRe.exec(scanText)) !== null) {
1454
+ pushModule(out, m[1], "default", "esm", true);
1455
+ }
1456
+ const tsEqRe = new RegExp(TS_IMPORT_EQUALS_RX.source, U);
1457
+ while ((m = tsEqRe.exec(scanText)) !== null) {
1458
+ pushModule(out, m[1], "default", "ts_import_equals");
1459
+ }
1460
+ const nsRe = new RegExp(NAMESPACE_IMPORT_RX.source, U);
1461
+ while ((m = nsRe.exec(scanText)) !== null) {
1462
+ const stmt = m[0];
1463
+ if (isTypeOnlyImportStatement(stmt)) continue;
1464
+ pushModule(out, m[1], "namespace", "esm");
1465
+ }
1466
+ const dnRe = new RegExp(DEFAULT_AND_NAMED_IMPORT_RX.source, U);
1467
+ while ((m = dnRe.exec(scanText)) !== null) {
1468
+ const stmt = m[0];
1469
+ if (isTypeOnlyImportStatement(stmt)) continue;
1470
+ pushModule(out, m[1], "default", "esm");
1471
+ const clause = m[2];
1472
+ for (const pair of parseEsmNamedImportClause(clause)) {
1473
+ pushNamed(out, pair.imported, pair.local, "esm");
1474
+ }
1475
+ for (const local of parseEsmDefaultAsLocals(clause)) {
1476
+ pushModule(out, local, "default", "esm");
1477
+ }
1478
+ }
1479
+ const namedRe = new RegExp(NAMED_IMPORT_ONLY_RX.source, U);
1480
+ while ((m = namedRe.exec(scanText)) !== null) {
1481
+ const stmt = m[0];
1482
+ if (isTypeOnlyImportStatement(stmt)) continue;
1483
+ const clause = m[1];
1484
+ for (const pair of parseEsmNamedImportClause(clause)) {
1485
+ pushNamed(out, pair.imported, pair.local, "esm");
1486
+ }
1487
+ for (const local of parseEsmDefaultAsLocals(clause)) {
1488
+ pushModule(out, local, "default", "esm");
1489
+ }
1490
+ }
1491
+ const defRe = new RegExp(DEFAULT_IMPORT_ONLY_RX.source, U);
1492
+ while ((m = defRe.exec(scanText)) !== null) {
1493
+ const stmt = m[0];
1494
+ if (isTypeOnlyImportStatement(stmt)) continue;
1495
+ pushModule(out, m[1], "default", "esm");
1496
+ }
1497
+ const cjsDestRe = new RegExp(CJS_REQUIRE_DESTRUCT_RX.source, U);
1498
+ while ((m = cjsDestRe.exec(scanText)) !== null) {
1499
+ for (const pair of parseCjsDestructuringClause(m[1])) {
1500
+ pushNamed(out, pair.imported, pair.local, "cjs_require");
1501
+ }
1502
+ }
1503
+ const cjsModRe = new RegExp(CJS_REQUIRE_MODULE_RX.source, U);
1504
+ while ((m = cjsModRe.exec(scanText)) !== null) {
1505
+ const clauseStart = m.index;
1506
+ const pre = scanText.slice(Math.max(0, clauseStart - 32), clauseStart + 1);
1507
+ if (/\{\s*$/u.test(pre)) continue;
1508
+ pushModule(out, m[1], "default", "cjs_require");
1509
+ }
1510
+ const dynDestRe = new RegExp(DYNAMIC_IMPORT_DESTRUCT_RX.source, U);
1511
+ while ((m = dynDestRe.exec(scanText)) !== null) {
1512
+ for (const pair of parseCjsDestructuringClause(m[1])) {
1513
+ pushNamed(out, pair.imported, pair.local, "dynamic_import");
1514
+ }
1515
+ }
1516
+ const dynModRe = new RegExp(DYNAMIC_IMPORT_MODULE_RX.source, U);
1517
+ while ((m = dynModRe.exec(scanText)) !== null) {
1518
+ const clauseStart = m.index;
1519
+ const pre = scanText.slice(Math.max(0, clauseStart - 32), clauseStart + 1);
1520
+ if (/\{\s*$/u.test(pre)) continue;
1521
+ pushModule(out, m[1], "default", "dynamic_import");
1522
+ }
1523
+ return dedupeBindings(out);
1524
+ }
1525
+ function dedupeBindings(bindings) {
1526
+ const seen = /* @__PURE__ */ new Set();
1527
+ const out = [];
1528
+ for (const b of bindings) {
1529
+ const key = b.kind === "named" ? `named:${b.source}:${b.imported}:${b.local}:${b.isTypeOnly === true ? "t" : "r"}` : `module:${b.source}:${b.local}:${b.moduleKind}:${b.isTypeOnly === true ? "t" : "r"}`;
1530
+ if (seen.has(key)) continue;
1531
+ seen.add(key);
1532
+ out.push(b);
1533
+ }
1534
+ return out;
1535
+ }
1536
+
1537
+ // src/shared/options/runOptions.ts
1538
+ var run = {
1539
+ json: false,
1540
+ jsonPretty: true,
1541
+ quiet: false,
1542
+ silent: false,
1543
+ debugScan: false,
1544
+ debugCache: false,
1545
+ onScanDebug: void 0
1546
+ };
1547
+ function getRunOptions() {
1548
+ return run;
1549
+ }
1550
+
1551
+ // src/shared/scanner/presets.ts
1552
+ var SCAN_EXCLUDE_PRESETS = {
1553
+ production: {
1554
+ dirs: ["node_modules", "dist", "build", "compiled", "tests", "bench"],
1555
+ files: ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"],
1556
+ extensions: ["test.ts", "test.tsx", "spec.ts", "spec.tsx", "test.js", "test.jsx", "spec.js", "spec.jsx"]
1557
+ }
1558
+ };
1559
+ function mergeExcludeRules(base, extra) {
1560
+ if (!base && !extra) return void 0;
1561
+ return {
1562
+ useDefaultSkip: base?.useDefaultSkip ?? extra?.useDefaultSkip,
1563
+ dirs: [...extra?.dirs ?? [], ...base?.dirs ?? []],
1564
+ files: [...extra?.files ?? [], ...base?.files ?? []],
1565
+ extensions: [...extra?.extensions ?? [], ...base?.extensions ?? []],
1566
+ patterns: [...extra?.patterns ?? [], ...base?.patterns ?? []]
1567
+ };
1568
+ }
1569
+ function resolveScanExcludeConfig(exclude) {
1570
+ if (!exclude) return void 0;
1571
+ const fromPreset = exclude.preset ? SCAN_EXCLUDE_PRESETS[exclude.preset] : void 0;
1572
+ return mergeExcludeRules(exclude, fromPreset);
1573
+ }
1574
+
1575
+ // src/shared/scanner/files.ts
1576
+ function resolveScanDebugSink(listOpts) {
1577
+ const g = getRunOptions();
1578
+ return listOpts?.onScanDebug ?? g.onScanDebug;
1579
+ }
1580
+ function emitScanDebug(sink, event) {
1581
+ if (sink) sink(event);
1582
+ }
1583
+ var DEFAULT_SCAN_SKIP_DIR_NAMES = [
1584
+ "node_modules",
1585
+ "dist",
1586
+ "build",
1587
+ ".git",
1588
+ "coverage",
1589
+ ".next",
1590
+ "out"
1591
+ ];
1592
+ var DEFAULT_SCAN_SKIP_DIR_SET = new Set(DEFAULT_SCAN_SKIP_DIR_NAMES);
1593
+ var SOURCE_FILE_NAME = /\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/i;
1594
+ var MAX_SOURCE_TREE_WALK_DEPTH = 48;
1595
+ function walkDirectoryKey(dir, pathPort, fs) {
1596
+ if (fs.realpath) {
1597
+ try {
1598
+ return fs.realpath(dir);
1599
+ } catch {
1600
+ return pathPort.resolve(dir);
1601
+ }
1602
+ }
1603
+ return pathPort.resolve(dir);
1604
+ }
1605
+ function normExtToken(s) {
1606
+ const t = s.trim().toLowerCase();
1607
+ return t.startsWith(".") ? t.slice(1) : t;
1608
+ }
1609
+ function basenameExtensionSuffixes(fileBase) {
1610
+ const parts = fileBase.split(".");
1611
+ if (parts.length < 2) return [];
1612
+ const out = [];
1613
+ for (let i = 1; i < parts.length; i++) {
1614
+ out.push(parts.slice(i).join(".").toLowerCase());
1615
+ }
1616
+ out.sort((a, b) => b.length - a.length);
1617
+ return out;
1618
+ }
1619
+ function relPosix(pathPort, rootDir, absPath) {
1620
+ return pathPort.relative(rootDir, absPath).replace(/\\/g, "/");
1621
+ }
1622
+ function partitionRules(rules) {
1623
+ const strings = /* @__PURE__ */ new Set();
1624
+ const regexes = [];
1625
+ if (!rules) return { strings, regexes };
1626
+ for (const r of rules) {
1627
+ if (typeof r === "string") {
1628
+ const t = r.trim();
1629
+ if (t) strings.add(t);
1630
+ } else {
1631
+ regexes.push(r);
1632
+ }
1633
+ }
1634
+ return { strings, regexes };
1635
+ }
1636
+ function partitionExtRules(rules) {
1637
+ const strings = /* @__PURE__ */ new Set();
1638
+ const regexes = [];
1639
+ if (!rules) return { strings, regexes };
1640
+ for (const r of rules) {
1641
+ if (typeof r === "string") {
1642
+ const n = normExtToken(r);
1643
+ if (n) strings.add(n);
1644
+ } else {
1645
+ regexes.push(r);
1646
+ }
1647
+ }
1648
+ return { strings, regexes };
1649
+ }
1650
+ function compileScanExclude(exclude) {
1651
+ const resolved = resolveScanExcludeConfig(exclude);
1652
+ const useDefault = resolved?.useDefaultSkip !== false;
1653
+ const defaultDirs = useDefault ? DEFAULT_SCAN_SKIP_DIR_SET : null;
1654
+ const dirs = partitionRules(resolved?.dirs);
1655
+ const files = partitionRules(resolved?.files);
1656
+ const exts = partitionExtRules(resolved?.extensions);
1657
+ const pathPatterns = resolved?.patterns ?? [];
1658
+ const userRulesEmpty = dirs.strings.size === 0 && dirs.regexes.length === 0 && files.strings.size === 0 && files.regexes.length === 0 && exts.strings.size === 0 && exts.regexes.length === 0 && pathPatterns.length === 0;
1659
+ return {
1660
+ defaultDirs,
1661
+ dirStrings: dirs.strings,
1662
+ dirRegexes: dirs.regexes,
1663
+ fileStrings: files.strings,
1664
+ fileRegexes: files.regexes,
1665
+ extStrings: exts.strings,
1666
+ extRegexes: exts.regexes,
1667
+ pathPatterns,
1668
+ userRulesEmpty
1669
+ };
1670
+ }
1671
+ function explainDirSkip(name, c) {
1672
+ if (c.defaultDirs?.has(name)) return `built-in directory skip (${name})`;
1673
+ if (c.userRulesEmpty) return null;
1674
+ if (c.dirStrings.has(name)) return `exclude.dirs (${name})`;
1675
+ for (const re of c.dirRegexes) {
1676
+ if (re.test(name)) return `exclude.dirs regex /${re.source}/`;
1677
+ }
1678
+ return null;
1679
+ }
1680
+ function explainFileSkip(relPosix2, baseName, c) {
1681
+ if (c.userRulesEmpty) return null;
1682
+ if (c.fileStrings.has(baseName)) return `exclude.files (${baseName})`;
1683
+ for (const re of c.fileRegexes) {
1684
+ if (re.test(baseName)) return `exclude.files regex /${re.source}/`;
1685
+ }
1686
+ if (c.extStrings.size > 0 || c.extRegexes.length > 0) {
1687
+ for (const suf of basenameExtensionSuffixes(baseName)) {
1688
+ if (c.extStrings.has(suf)) return `exclude.extensions (${suf})`;
1689
+ for (const re of c.extRegexes) {
1690
+ if (re.test(suf)) return `exclude.extensions regex /${re.source}/`;
1691
+ }
1692
+ }
1693
+ }
1694
+ for (const re of c.pathPatterns) {
1695
+ if (re.test(relPosix2)) return `exclude.patterns /${re.source}/ \u2190 ${relPosix2}`;
1696
+ }
1697
+ return null;
1698
+ }
1699
+ function listSourceFiles(runtime, rootDir, exclude, listOpts) {
1700
+ const { fs, path } = runtime;
1701
+ const compiled = compileScanExclude(exclude);
1702
+ const out = [];
1703
+ const debugSink = resolveScanDebugSink(listOpts);
1704
+ const visitedDirs = /* @__PURE__ */ new Set();
1705
+ function walk(dir, depth) {
1706
+ if (!existsRuntimeFsSync(dir, fs)) return;
1707
+ const dirKey = walkDirectoryKey(dir, path, fs);
1708
+ if (visitedDirs.has(dirKey)) {
1709
+ emitScanDebug(debugSink, {
1710
+ kind: "skip_directory",
1711
+ relativePath: relPosix(path, rootDir, dir),
1712
+ basename: path.basename(dir),
1713
+ reason: "directory already visited (symlink cycle or duplicate path)"
1714
+ });
1715
+ return;
1716
+ }
1717
+ visitedDirs.add(dirKey);
1718
+ if (depth > MAX_SOURCE_TREE_WALK_DEPTH) {
1719
+ emitScanDebug(debugSink, {
1720
+ kind: "skip_directory",
1721
+ relativePath: relPosix(path, rootDir, dir),
1722
+ basename: path.basename(dir),
1723
+ reason: `scan depth limit (${String(MAX_SOURCE_TREE_WALK_DEPTH)})`
1724
+ });
1725
+ return;
1726
+ }
1727
+ const entries = listRuntimeFsDirSync(dir, fs);
1728
+ for (const e of entries) {
1729
+ const p = path.join(dir, e.name);
1730
+ if (e.kind === "directory") {
1731
+ const why = explainDirSkip(e.name, compiled);
1732
+ if (why) {
1733
+ emitScanDebug(debugSink, {
1734
+ kind: "skip_directory",
1735
+ relativePath: relPosix(path, rootDir, p),
1736
+ basename: e.name,
1737
+ reason: why
1738
+ });
1739
+ continue;
1740
+ }
1741
+ walk(p, depth + 1);
1742
+ } else if (e.kind === "file") {
1743
+ const rel = relPosix(path, rootDir, p);
1744
+ if (!SOURCE_FILE_NAME.test(e.name)) {
1745
+ emitScanDebug(debugSink, {
1746
+ kind: "skip_file",
1747
+ relativePath: rel,
1748
+ basename: e.name,
1749
+ reason: `not a scanned source extension (${e.name})`
1750
+ });
1751
+ continue;
1752
+ }
1753
+ const whyF = explainFileSkip(rel, e.name, compiled);
1754
+ if (whyF) {
1755
+ emitScanDebug(debugSink, {
1756
+ kind: "skip_file",
1757
+ relativePath: rel,
1758
+ basename: e.name,
1759
+ reason: whyF
1760
+ });
1761
+ continue;
1762
+ }
1763
+ out.push(p);
1764
+ } else if (e.kind === "other") {
1765
+ emitScanDebug(debugSink, {
1766
+ kind: "skip_file",
1767
+ relativePath: relPosix(path, rootDir, p),
1768
+ basename: e.name,
1769
+ reason: "not a regular file or directory (symlink or special entry)"
1770
+ });
1771
+ }
1772
+ }
1773
+ }
1774
+ walk(rootDir, 0);
1775
+ return out;
1776
+ }
1777
+
1778
+ // src/extractor/shared/projectScan.ts
1779
+ function resolveScanIo(input) {
1780
+ const { readFile, listFiles, runtime, path: pathOverride } = input;
1781
+ const pathPort = runtime?.path ?? pathOverride;
1782
+ if (readFile && listFiles) {
1783
+ if (!pathPort) {
1784
+ throw new I18nPruneError(
1785
+ "scanProjectSourceFiles: with custom `readFile`/`listFiles`, pass `runtime` or `path` for display paths.",
1786
+ "USAGE"
1787
+ );
1788
+ }
1789
+ if (input.cwd !== void 0) {
1790
+ return { cwd: input.cwd, listFiles, readFile, pathPort };
1791
+ }
1792
+ if (runtime) {
1793
+ return { cwd: input.cwd ?? runtime.system.cwd(), listFiles, readFile, pathPort };
1794
+ }
1795
+ throw new I18nPruneError(
1796
+ "scanProjectSourceFiles: when using custom `readFile` and `listFiles`, set `cwd` or pass `runtime` for `system.cwd()`.",
1797
+ "USAGE"
1798
+ );
1799
+ }
1800
+ if (readFile || listFiles) {
1801
+ throw new I18nPruneError(
1802
+ "scanProjectSourceFiles: provide both `readFile` and `listFiles`, or omit both and pass `runtime`.",
1803
+ "USAGE"
1804
+ );
1805
+ }
1806
+ if (!runtime) {
1807
+ throw new I18nPruneError(
1808
+ "scanProjectSourceFiles: pass `runtime` ({ fs, path, system }) or both `readFile` and `listFiles`.",
1809
+ "USAGE"
1810
+ );
1811
+ }
1812
+ const cwd = input.cwd ?? runtime.system.cwd();
1813
+ return {
1814
+ cwd,
1815
+ listFiles: (srcRoot) => listSourceFiles(runtime, srcRoot, input.exclude, input.scanDebug ? { onScanDebug: input.scanDebug } : void 0),
1816
+ readFile: (filePath) => readRuntimeFsTextSync(filePath, runtime.fs),
1817
+ pathPort: runtime.path
1818
+ };
1819
+ }
1820
+ function scanProjectSourceFiles(input) {
1821
+ const { cwd, listFiles, readFile, pathPort } = resolveScanIo(input);
1822
+ const files = listFiles(input.srcRoot);
1823
+ const out = [];
1824
+ for (const filePath of files) {
1825
+ let text;
1826
+ try {
1827
+ text = readFile(filePath);
1828
+ } catch {
1829
+ continue;
1830
+ }
1831
+ const relFromSrc = pathPort.relative(input.srcRoot, filePath).replace(/\\/g, "/");
1832
+ const displayPath = relFromSrc && !relFromSrc.startsWith("..") ? relFromSrc : (() => {
1833
+ const relCwd = pathPort.relative(cwd, filePath);
1834
+ return relCwd && !relCwd.startsWith("..") ? relCwd.replace(/\\/g, "/") : filePath;
1835
+ })();
1836
+ const rows = input.scanFile({ filePath, displayPath, text });
1837
+ out.push(...rows);
1838
+ }
1839
+ return out;
1840
+ }
1841
+
1842
+ // src/extractor/constmap/build.ts
1843
+ function buildConstStringMap(source) {
1844
+ const map = {};
1845
+ const re = /\bconst\s+([A-Za-z_$][\w$]*)(?:\s*:\s*[^=]+)?\s*=\s*(['"`])([^'"`]+)\2\s*;?/g;
1846
+ let m;
1847
+ while ((m = re.exec(source)) !== null) {
1848
+ map[m[1]] = m[3];
1849
+ }
1850
+ return map;
1851
+ }
1852
+
1853
+ // src/extractor/shared/pattern.ts
1854
+ function escapeRegex(s) {
1855
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1856
+ }
1857
+ function buildFunctionsPattern(functions) {
1858
+ const parts = functions.map((f) => {
1859
+ const escaped = escapeRegex(f);
1860
+ return escaped.replace(/\\\./g, "\\.");
1861
+ });
1862
+ return `(?:${parts.join("|")})`;
1863
+ }
1864
+
1865
+ // src/extractor/shared/calls.ts
1866
+ function findTranslationCallSites(text, functions) {
1867
+ const out = [];
1868
+ if (functions.length === 0) return out;
1869
+ const fn = buildFunctionsPattern(functions);
1870
+ const callStart = new RegExp(`\\b(${fn})\\s*\\(`, "g");
1871
+ let m;
1872
+ while ((m = callStart.exec(text)) !== null) {
1873
+ const functionName = m[1];
1874
+ const matchIndex = m.index;
1875
+ const fnEsc = escapeRegex(functionName);
1876
+ const decl = new RegExp(`\\b(?:export\\s+)?function\\s+${fnEsc}\\s*\\(`);
1877
+ if (decl.test(text.slice(Math.max(0, matchIndex - 64), matchIndex + functionName.length + 2))) {
1878
+ continue;
1879
+ }
1880
+ const openParenIndex = matchIndex + m[0].length - 1;
1881
+ const closeParenIndex = findMatchingCloseParen(text, openParenIndex);
1882
+ if (closeParenIndex === -1) continue;
1883
+ const first = parseFirstArgument(text, openParenIndex, closeParenIndex);
1884
+ if (!first.empty && firstArgLooksLikeAsciiLowercaseProse(first.raw)) {
1885
+ continue;
1886
+ }
1887
+ out.push({
1888
+ functionName,
1889
+ matchIndex,
1890
+ openParenIndex,
1891
+ closeParenIndex,
1892
+ firstArgStart: first.start,
1893
+ firstArgEnd: first.end,
1894
+ firstArgRaw: first.raw,
1895
+ isEmptyCall: first.empty,
1896
+ isMultilineCall: text.slice(matchIndex, closeParenIndex + 1).includes("\n")
1897
+ });
1898
+ }
1899
+ return out;
1900
+ }
1901
+ function firstArgLooksLikeAsciiLowercaseProse(raw) {
1902
+ const trimmed = raw.trim();
1903
+ if (trimmed.length === 0) return false;
1904
+ if (!/\s/.test(trimmed)) return false;
1905
+ const parts = trimmed.split(/\s+/).filter(Boolean);
1906
+ if (parts.length < 2) return false;
1907
+ for (const p of parts) {
1908
+ if (!/^[a-z]+$/.test(p)) return false;
1909
+ }
1910
+ return true;
1911
+ }
1912
+ function findMatchingCloseParen(text, openParenIndex) {
1913
+ let i = openParenIndex + 1;
1914
+ let depth = 1;
1915
+ while (i < text.length) {
1916
+ const c = text[i];
1917
+ if (c === "'" || c === '"') {
1918
+ i = skipString2(text, i, c);
1919
+ continue;
1920
+ }
1921
+ if (c === "`") {
1922
+ i = skipTemplate2(text, i);
1923
+ continue;
1924
+ }
1925
+ if (c === "/" && text[i + 1] === "/") {
1926
+ i = skipLineComment(text, i);
1927
+ continue;
1928
+ }
1929
+ if (c === "/" && text[i + 1] === "*") {
1930
+ i = skipBlockComment(text, i);
1931
+ continue;
1932
+ }
1933
+ if (c === "(") depth += 1;
1934
+ else if (c === ")") {
1935
+ depth -= 1;
1936
+ if (depth === 0) return i;
1937
+ }
1938
+ i += 1;
1939
+ }
1940
+ return -1;
1941
+ }
1942
+ function parseFirstArgument(text, openParenIndex, closeParenIndex) {
1943
+ let i = skipTrivia(text, openParenIndex + 1, closeParenIndex);
1944
+ if (i >= closeParenIndex) return { start: i, end: i, raw: "", empty: true };
1945
+ let parenDepth = 0;
1946
+ let bracketDepth = 0;
1947
+ let braceDepth = 0;
1948
+ const start = i;
1949
+ while (i < closeParenIndex) {
1950
+ const c = text[i];
1951
+ if (c === "'" || c === '"') {
1952
+ i = skipString2(text, i, c);
1953
+ continue;
1954
+ }
1955
+ if (c === "`") {
1956
+ i = skipTemplate2(text, i);
1957
+ continue;
1958
+ }
1959
+ if (c === "/" && text[i + 1] === "/") {
1960
+ i = skipLineComment(text, i);
1961
+ continue;
1962
+ }
1963
+ if (c === "/" && text[i + 1] === "*") {
1964
+ i = skipBlockComment(text, i);
1965
+ continue;
1966
+ }
1967
+ if (c === "(") parenDepth += 1;
1968
+ else if (c === ")") {
1969
+ if (parenDepth > 0) parenDepth -= 1;
1970
+ } else if (c === "[") bracketDepth += 1;
1971
+ else if (c === "]") {
1972
+ if (bracketDepth > 0) bracketDepth -= 1;
1973
+ } else if (c === "{") braceDepth += 1;
1974
+ else if (c === "}") {
1975
+ if (braceDepth > 0) braceDepth -= 1;
1976
+ } else if (c === "," && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
1977
+ break;
1978
+ }
1979
+ i += 1;
1980
+ }
1981
+ const end = rtrimIndex(text, start, i);
1982
+ return {
1983
+ start,
1984
+ end,
1985
+ raw: text.slice(start, end),
1986
+ empty: false
1987
+ };
1988
+ }
1989
+ function skipTrivia(text, start, end) {
1990
+ let i = start;
1991
+ while (i < end) {
1992
+ const c = text[i];
1993
+ if (/\s/.test(c)) {
1994
+ i += 1;
1995
+ continue;
1996
+ }
1997
+ if (c === "/" && text[i + 1] === "/") {
1998
+ i = skipLineComment(text, i);
1999
+ continue;
2000
+ }
2001
+ if (c === "/" && text[i + 1] === "*") {
2002
+ i = skipBlockComment(text, i);
2003
+ continue;
2004
+ }
2005
+ break;
2006
+ }
2007
+ return i;
2008
+ }
2009
+ function rtrimIndex(text, start, end) {
2010
+ let i = end;
2011
+ while (i > start && /\s/.test(text[i - 1])) i -= 1;
2012
+ return i;
2013
+ }
2014
+ function skipLineComment(text, start) {
2015
+ let i = start + 2;
2016
+ while (i < text.length && text[i] !== "\n") i += 1;
2017
+ return i;
2018
+ }
2019
+ function skipBlockComment(text, start) {
2020
+ let i = start + 2;
2021
+ while (i + 1 < text.length) {
2022
+ if (text[i] === "*" && text[i + 1] === "/") return i + 2;
2023
+ i += 1;
2024
+ }
2025
+ return text.length;
2026
+ }
2027
+ function skipString2(text, start, quote) {
2028
+ let i = start + 1;
2029
+ while (i < text.length) {
2030
+ const ch = text[i];
2031
+ if (ch === "\\") {
2032
+ i += 2;
2033
+ continue;
2034
+ }
2035
+ if (ch === quote) return i + 1;
2036
+ i += 1;
2037
+ }
2038
+ return text.length;
2039
+ }
2040
+ function skipTemplate2(text, start) {
2041
+ let i = start + 1;
2042
+ while (i < text.length) {
2043
+ const ch = text[i];
2044
+ if (ch === "\\") {
2045
+ i += 2;
2046
+ continue;
2047
+ }
2048
+ if (ch === "`") return i + 1;
2049
+ if (ch === "$" && text[i + 1] === "{") {
2050
+ i += 2;
2051
+ let depth = 1;
2052
+ while (i < text.length && depth > 0) {
2053
+ const c2 = text[i];
2054
+ if (c2 === "'" || c2 === '"') {
2055
+ i = skipString2(text, i, c2);
2056
+ continue;
2057
+ }
2058
+ if (c2 === "`") {
2059
+ i = skipTemplate2(text, i);
2060
+ continue;
2061
+ }
2062
+ if (c2 === "/" && text[i + 1] === "/") {
2063
+ i = skipLineComment(text, i);
2064
+ continue;
2065
+ }
2066
+ if (c2 === "/" && text[i + 1] === "*") {
2067
+ i = skipBlockComment(text, i);
2068
+ continue;
2069
+ }
2070
+ if (c2 === "{") depth += 1;
2071
+ else if (c2 === "}") depth -= 1;
2072
+ i += 1;
2073
+ }
2074
+ continue;
2075
+ }
2076
+ i += 1;
2077
+ }
2078
+ return text.length;
2079
+ }
2080
+
2081
+ // src/extractor/dynamic/helpers.ts
2082
+ function snippetRange(text, start, end, maxLen) {
2083
+ const boundedEnd = Math.max(start, Math.min(end, text.length));
2084
+ const slice = text.slice(start, boundedEnd).replace(/\s+/g, " ").trim();
2085
+ if (slice.length <= maxLen) return slice;
2086
+ return `${slice.slice(0, maxLen - 1)}\u2026`;
2087
+ }
2088
+ function offsetToLineColumn(text, offset) {
2089
+ let line = 1;
2090
+ let col = 1;
2091
+ for (let i = 0; i < offset && i < text.length; i += 1) {
2092
+ const c = text[i];
2093
+ if (c === "\n") {
2094
+ line += 1;
2095
+ col = 1;
2096
+ } else {
2097
+ col += 1;
2098
+ }
2099
+ }
2100
+ return { line, column: col };
2101
+ }
2102
+
2103
+ // src/extractor/constmap/resolve.ts
2104
+ function resolveKeyPlaceholdersWithTrace(fragment, constMap) {
2105
+ let out = fragment;
2106
+ const substitutions = [];
2107
+ const re = /\$\{([A-Za-z_$][\w$]*)\}/;
2108
+ for (let i = 0; i < 64; i += 1) {
2109
+ const m = re.exec(out);
2110
+ if (!m) break;
2111
+ const name = m[1];
2112
+ const val = constMap[name];
2113
+ if (val === void 0) {
2114
+ return { resolved: null, substitutions, remainder: out };
2115
+ }
2116
+ substitutions.push({ identifier: name, value: val });
2117
+ out = out.replace(m[0], val);
2118
+ }
2119
+ if (/\$\{[^}]+\}/.test(out)) {
2120
+ return { resolved: null, substitutions, remainder: out };
2121
+ }
2122
+ return { resolved: out, substitutions, remainder: out };
2123
+ }
2124
+
2125
+ // src/extractor/dynamic/rebuild.ts
2126
+ function tryRebuildTemplateKeyFromConsts(inner, constMap) {
2127
+ return resolveKeyPlaceholdersWithTrace(inner, constMap).resolved;
2128
+ }
2129
+ function tryResolveTemplatePrefixBeforeUnknown(inner, constMap) {
2130
+ const interpRe = /\$\{([^}]+)\}/g;
2131
+ const matches = [...inner.matchAll(interpRe)];
2132
+ if (matches.length === 0) {
2133
+ const t = inner.trim();
2134
+ return t.includes(".") ? t : null;
2135
+ }
2136
+ let cutIndex = -1;
2137
+ for (const m of matches) {
2138
+ const expr = m[1]?.trim();
2139
+ const ident = expr?.match(/^[A-Za-z_$][\w$]*$/)?.[0];
2140
+ if (!ident) {
2141
+ cutIndex = m.index ?? -1;
2142
+ break;
2143
+ }
2144
+ if (!Object.prototype.hasOwnProperty.call(constMap, ident)) {
2145
+ cutIndex = m.index ?? -1;
2146
+ break;
2147
+ }
2148
+ }
2149
+ const prefix = (cutIndex === -1 ? inner : inner.slice(0, cutIndex)).trim();
2150
+ const resolved = prefix.replace(/\$\{([^}]+)\}/g, (whole, innerExpr) => {
2151
+ const ident = String(innerExpr).trim();
2152
+ if (/^[A-Za-z_$][\w$]*$/.test(ident) && Object.prototype.hasOwnProperty.call(constMap, ident)) {
2153
+ return constMap[ident];
2154
+ }
2155
+ return whole;
2156
+ });
2157
+ const trimmed = resolved.replace(/\.$/, "").trim();
2158
+ if (!trimmed.includes(".")) return null;
2159
+ return trimmed;
2160
+ }
2161
+
2162
+ // src/extractor/dynamic/providers/javascript.ts
2163
+ var PREVIEW = 72;
2164
+ var JAVASCRIPT_LIKE_EXTENSIONS = /* @__PURE__ */ new Set([
2165
+ ".ts",
2166
+ ".tsx",
2167
+ ".js",
2168
+ ".jsx",
2169
+ ".mjs",
2170
+ ".cjs",
2171
+ ".vue",
2172
+ ".svelte"
2173
+ ]);
2174
+ function isJavascriptLikePath(filePath) {
2175
+ const lower = filePath.toLowerCase();
2176
+ const dot = lower.lastIndexOf(".");
2177
+ if (dot < 0) return false;
2178
+ return JAVASCRIPT_LIKE_EXTENSIONS.has(lower.slice(dot));
2179
+ }
2180
+ function findDynamicKeySitesInJavascriptFile(text, functions, filePath) {
2181
+ const commentRanges = commentRangesForJsLikeText(text);
2182
+ const constMap = buildConstStringMap(text);
2183
+ const raw = findDynamicKeySitesRaw(text, functions, constMap);
2184
+ const out = [];
2185
+ for (const site of raw) {
2186
+ const at = site.matchIndex;
2187
+ const commented = offsetInCommentRanges(at, commentRanges);
2188
+ const { line, column } = offsetToLineColumn(text, at);
2189
+ const kind = commented ? "commented" : site.kind;
2190
+ out.push({
2191
+ kind,
2192
+ functionName: site.functionName,
2193
+ preview: snippetRange(text, at, site.closeParenIndex + 1, PREVIEW),
2194
+ filePath,
2195
+ line,
2196
+ column,
2197
+ isMultilineCall: site.isMultilineCall,
2198
+ isCommented: commented,
2199
+ isSourceFile: true,
2200
+ ...site.resolvedPrefix !== void 0 ? { resolvedPrefix: site.resolvedPrefix } : {}
2201
+ });
2202
+ }
2203
+ return out;
2204
+ }
2205
+ function findDynamicKeySitesRaw(text, functions, constMap) {
2206
+ const out = [];
2207
+ const calls = findTranslationCallSites(text, functions);
2208
+ for (const call of calls) {
2209
+ const at = call.matchIndex;
2210
+ if (call.isEmptyCall) {
2211
+ out.push({
2212
+ kind: "empty_call",
2213
+ functionName: call.functionName,
2214
+ matchIndex: at,
2215
+ closeParenIndex: call.closeParenIndex,
2216
+ isMultilineCall: call.isMultilineCall
2217
+ });
2218
+ continue;
2219
+ }
2220
+ const arg = call.firstArgRaw.trim();
2221
+ const first = arg[0];
2222
+ if (first === "'" || first === '"') continue;
2223
+ if (first === "`" && arg.endsWith("`")) {
2224
+ const inner = arg.slice(1, -1);
2225
+ if (!/\$\{/.test(inner)) continue;
2226
+ const rebuilt = tryRebuildTemplateKeyFromConsts(inner, constMap);
2227
+ if (rebuilt !== null) continue;
2228
+ const resolvedPrefix = tryResolveTemplatePrefixBeforeUnknown(inner, constMap) ?? void 0;
2229
+ out.push({
2230
+ kind: "template_interpolation",
2231
+ functionName: call.functionName,
2232
+ matchIndex: at,
2233
+ closeParenIndex: call.closeParenIndex,
2234
+ isMultilineCall: call.isMultilineCall,
2235
+ ...resolvedPrefix !== void 0 ? { resolvedPrefix } : {}
2236
+ });
2237
+ continue;
2238
+ }
2239
+ out.push({
2240
+ kind: "non_literal",
2241
+ functionName: call.functionName,
2242
+ matchIndex: at,
2243
+ closeParenIndex: call.closeParenIndex,
2244
+ isMultilineCall: call.isMultilineCall
2245
+ });
2246
+ }
2247
+ return out;
2248
+ }
2249
+
2250
+ // src/extractor/dynamic/providers/index.ts
2251
+ function findDynamicKeySitesForFile(filePath, content, functions) {
2252
+ if (!isJavascriptLikePath(filePath)) {
2253
+ return [];
2254
+ }
2255
+ return findDynamicKeySitesInJavascriptFile(content, functions, filePath);
2256
+ }
2257
+
2258
+ // src/extractor/dynamic/orchestrate.ts
2259
+ function scanProjectDynamicKeySites(input) {
2260
+ return scanProjectSourceFiles({
2261
+ srcRoot: input.srcRoot,
2262
+ cwd: input.cwd,
2263
+ runtime: input.runtime,
2264
+ path: input.path,
2265
+ readFile: input.readFile,
2266
+ listFiles: input.listFiles,
2267
+ exclude: input.exclude,
2268
+ scanFile: ({ filePath, displayPath, text }) => {
2269
+ const functions = expandFunctionsWithBindings(input.functions, scanImportBindings(text));
2270
+ const sites = findDynamicKeySitesForFile(filePath, text, functions);
2271
+ return sites.map((site) => ({ ...site, filePath: displayPath }));
2272
+ }
2273
+ });
2274
+ }
2275
+
2276
+ // src/extractor/keySites/line.ts
2277
+ function lineNumberAtIndex(text, index) {
2278
+ if (index <= 0) return 1;
2279
+ let line = 1;
2280
+ const end = Math.min(index, text.length);
2281
+ for (let i = 0; i < end; i++) {
2282
+ if (text.charCodeAt(i) === 10) line += 1;
2283
+ }
2284
+ return line;
2285
+ }
2286
+
2287
+ // src/extractor/keySites/scan.ts
2288
+ function spanAtOffset(text, offset, functionName, isMultilineCall) {
2289
+ return {
2290
+ line: lineNumberAtIndex(text, offset),
2291
+ functionName,
2292
+ isMultilineCall,
2293
+ charOffset: offset
2294
+ };
2295
+ }
2296
+ function scanKeyObservations(text, functions, constMap, options) {
2297
+ const out = [];
2298
+ const calls = findTranslationCallSites(text, functions);
2299
+ const ranges = options?.commentRanges;
2300
+ for (const call of calls) {
2301
+ if (ranges?.length && offsetInCommentRanges(call.matchIndex, ranges)) continue;
2302
+ if (call.isEmptyCall) continue;
2303
+ const arg = call.firstArgRaw.trim();
2304
+ const stringMatch = arg.match(/^(['"])([\s\S]*)\1$/);
2305
+ if (stringMatch) {
2306
+ const key = stringMatch[2];
2307
+ out.push({
2308
+ kind: "literal",
2309
+ resolvedKey: key,
2310
+ raw: key,
2311
+ span: spanAtOffset(text, call.matchIndex, call.functionName, call.isMultilineCall)
2312
+ });
2313
+ continue;
2314
+ }
2315
+ const tplMatch = arg.match(/^`([\s\S]*)`$/);
2316
+ if (!tplMatch) continue;
2317
+ const templateRaw = tplMatch[1];
2318
+ const trace = resolveKeyPlaceholdersWithTrace(templateRaw, constMap);
2319
+ const span = spanAtOffset(text, call.matchIndex, call.functionName, call.isMultilineCall);
2320
+ if (trace.resolved !== null) {
2321
+ out.push({
2322
+ kind: "template_resolved",
2323
+ resolvedKey: trace.resolved,
2324
+ templateRaw,
2325
+ substitutions: trace.substitutions,
2326
+ span
2327
+ });
2328
+ } else {
2329
+ const unresolved = [];
2330
+ const rem = trace.remainder;
2331
+ const ph = /\$\{([A-Za-z_$][\w$]*)\}/g;
2332
+ let pm;
2333
+ while ((pm = ph.exec(rem)) !== null) {
2334
+ unresolved.push(pm[1]);
2335
+ }
2336
+ const uncertainPrefix = tryResolveTemplatePrefixBeforeUnknown(templateRaw, constMap) ?? void 0;
2337
+ out.push({
2338
+ kind: "template_partial",
2339
+ templateRaw,
2340
+ substitutions: trace.substitutions,
2341
+ unresolvedPlaceholders: unresolved,
2342
+ span,
2343
+ ...uncertainPrefix !== void 0 ? { uncertainPrefix } : {}
2344
+ });
2345
+ }
2346
+ }
2347
+ return out;
2348
+ }
2349
+
2350
+ // src/extractor/keySites/orchestrate.ts
2351
+ function scanProjectKeyObservations(input) {
2352
+ return scanProjectSourceFiles({
2353
+ srcRoot: input.srcRoot,
2354
+ cwd: input.cwd,
2355
+ runtime: input.runtime,
2356
+ path: input.path,
2357
+ readFile: input.readFile,
2358
+ listFiles: input.listFiles,
2359
+ exclude: input.exclude,
2360
+ scanFile: ({ text, displayPath }) => {
2361
+ const functions = expandFunctionsWithBindings(input.functions, scanImportBindings(text));
2362
+ const constMap = buildConstStringMap(text);
2363
+ const commentRanges = commentRangesForJsLikeText(text);
2364
+ const observations = scanKeyObservations(text, functions, constMap, {
2365
+ commentRanges
2366
+ });
2367
+ return observations.map((obs) => ({
2368
+ ...obs,
2369
+ span: { ...obs.span, filePath: displayPath }
2370
+ }));
2371
+ }
2372
+ });
2373
+ }
2374
+
2375
+ // src/extractor/keySites/projectUsage.ts
2376
+ function topPathSegment(path) {
2377
+ const first = path.split(/[.[\]]/).find(Boolean);
2378
+ return first ?? null;
2379
+ }
2380
+ function literalKeyUsageFromObservations(observations) {
2381
+ const resolvedKeys = /* @__PURE__ */ new Set();
2382
+ const uncertainPrefixes = /* @__PURE__ */ new Set();
2383
+ const usedRoots = /* @__PURE__ */ new Set();
2384
+ for (const o of observations) {
2385
+ if (o.kind === "literal" || o.kind === "template_resolved") {
2386
+ resolvedKeys.add(o.resolvedKey);
2387
+ const root = topPathSegment(o.resolvedKey);
2388
+ if (root) usedRoots.add(root);
2389
+ continue;
2390
+ }
2391
+ if (o.kind === "template_partial" && o.uncertainPrefix) {
2392
+ uncertainPrefixes.add(o.uncertainPrefix);
2393
+ const root = topPathSegment(o.uncertainPrefix);
2394
+ if (root) usedRoots.add(root);
2395
+ }
2396
+ }
2397
+ return { resolvedKeys, uncertainPrefixes, usedRoots };
2398
+ }
2399
+
2400
+ // src/shared/reference/context.ts
2401
+ function pushUnique(out, v) {
2402
+ if (!v || out.includes(v)) return;
2403
+ out.push(v);
2404
+ }
2405
+ function includeDynamicSite(site, eff) {
2406
+ if ((site.kind === "commented" || site.isCommented) && !eff.treatCommentedCallSitesAsRuntime) return false;
2407
+ if (site.isSourceFile === false && !eff.treatNonSourceFileSitesAsRuntime) return false;
2408
+ return true;
2409
+ }
2410
+ function collectUncertainPrefixesFromDynamic(sites, out, eff) {
2411
+ for (const site of sites) {
2412
+ if (!includeDynamicSite(site, eff)) continue;
2413
+ if (site.resolvedPrefix) pushUnique(out, site.resolvedPrefix);
2414
+ }
2415
+ }
2416
+ function buildKeyReferenceContextFromLiteralUsageAndDynamicSites(usage, dynamicSites, eff) {
2417
+ const uncertainPrefixes = [];
2418
+ for (const p of usage.uncertainPrefixes) pushUnique(uncertainPrefixes, p);
2419
+ collectUncertainPrefixesFromDynamic([...dynamicSites], uncertainPrefixes, eff);
2420
+ return { provenKeys: usage.resolvedKeys, uncertainPrefixes };
2421
+ }
2422
+
2423
+ // src/cache/io/hash.ts
2424
+ function fallbackHashText(text) {
2425
+ let hash = 0xcbf29ce484222325n;
2426
+ const prime = 0x100000001b3n;
2427
+ for (let i = 0; i < text.length; i += 1) {
2428
+ hash ^= BigInt(text.charCodeAt(i));
2429
+ hash = BigInt.asUintN(64, hash * prime);
2430
+ }
2431
+ return hash.toString(16).padStart(16, "0");
2432
+ }
2433
+ function normalizeProjectRoot(projectRoot, pathPort) {
2434
+ const resolved = pathPort ? pathPort.resolve(projectRoot) : projectRoot;
2435
+ return normalizePathKeyForCache(resolved);
2436
+ }
2437
+ function computeCacheProjectId(projectRoot, input = {}) {
2438
+ return (input.hashText ?? fallbackHashText)(normalizeProjectRoot(projectRoot, input.path)).slice(0, 16);
2439
+ }
2440
+ function computeCacheContentHash(text, hashText) {
2441
+ return (hashText ?? fallbackHashText)(text);
2442
+ }
2443
+
2444
+ // src/cache/io/helpers.ts
2445
+ function nowIso(runtime) {
2446
+ return new Date(runtime?.system.now() ?? Date.now()).toISOString();
2447
+ }
2448
+ function textByteLength(text, runtime) {
2449
+ return runtime.byteLength ? runtime.byteLength(text) : new TextEncoder().encode(text).length;
2450
+ }
2451
+ function readJsonFileWithLimit(filePath, maxBytes, runtime) {
2452
+ try {
2453
+ if (!assertSyncPortResult(runtime.fs.exists(filePath), "fs.exists", filePath)) return {};
2454
+ const kind = assertSyncPortResult(runtime.fs.statKind(filePath), "fs.statKind", filePath);
2455
+ if (kind !== "file") {
2456
+ return {
2457
+ warning: {
2458
+ code: "cache_io_error",
2459
+ message: "cache path is not a file; skipping",
2460
+ path: filePath
2461
+ }
2462
+ };
2463
+ }
2464
+ const knownSize = runtime.fileSize?.(filePath);
2465
+ if (knownSize !== void 0 && knownSize > maxBytes) {
2466
+ return {
2467
+ warning: {
2468
+ code: "cache_oversize",
2469
+ message: `cache file exceeds size limit (${String(knownSize)} > ${String(maxBytes)} bytes); skipping`,
2470
+ path: filePath
2471
+ }
2472
+ };
2473
+ }
2474
+ const raw = assertSyncPortResult(runtime.fs.readText(filePath), "fs.readText", filePath);
2475
+ const size = knownSize ?? textByteLength(raw, runtime);
2476
+ if (size > maxBytes) {
2477
+ return {
2478
+ warning: {
2479
+ code: "cache_oversize",
2480
+ message: `cache file exceeds size limit (${String(size)} > ${String(maxBytes)} bytes); skipping`,
2481
+ path: filePath
2482
+ }
2483
+ };
2484
+ }
2485
+ const parsed = tryParseJsonText(raw, { filePath, issueCode: "i18nprune.cache.json" });
2486
+ if (!parsed.ok) {
2487
+ return {
2488
+ warning: {
2489
+ code: "cache_malformed",
2490
+ message: parsed.error.message,
2491
+ path: filePath
2492
+ }
2493
+ };
2494
+ }
2495
+ return { data: parsed.data };
2496
+ } catch (err) {
2497
+ return {
2498
+ warning: {
2499
+ code: "cache_io_error",
2500
+ message: `cache read error: ${err instanceof Error ? err.message : String(err)}`,
2501
+ path: filePath
2502
+ }
2503
+ };
2504
+ }
2505
+ }
2506
+ function writeJsonAtomic(filePath, data, runtime) {
2507
+ try {
2508
+ const content = JSON.stringify(data, null, 2);
2509
+ if (runtime.writeTextAtomic) {
2510
+ runtime.writeTextAtomic(filePath, content);
2511
+ return void 0;
2512
+ }
2513
+ assertSyncPortResult(runtime.fs.mkdirp(runtime.path.dirname(filePath)), "fs.mkdirp", runtime.path.dirname(filePath));
2514
+ assertSyncPortResult(runtime.fs.writeText(filePath, content), "fs.writeText", filePath);
2515
+ return void 0;
2516
+ } catch (err) {
2517
+ return {
2518
+ code: "cache_io_error",
2519
+ message: `cache write error: ${err instanceof Error ? err.message : String(err)}`,
2520
+ path: filePath
2521
+ };
2522
+ }
2523
+ }
2524
+
2525
+ // src/shared/constants/cache.ts
2526
+ var CACHE_SCHEMA_VERSION = 1;
2527
+ var DEFAULT_CACHE_PROFILE_ID = "balanced";
2528
+ var CACHE_PROFILE_DEFAULTS = {
2529
+ safe: {
2530
+ rebuild: "full",
2531
+ fullRescanThresholdPercent: 10,
2532
+ mode: "readWrite"
2533
+ },
2534
+ balanced: {
2535
+ rebuild: "partial",
2536
+ fullRescanThresholdPercent: 40,
2537
+ mode: "readWrite"
2538
+ },
2539
+ fast: {
2540
+ rebuild: "partial",
2541
+ fullRescanThresholdPercent: 70,
2542
+ mode: "readWrite"
2543
+ }
2544
+ };
2545
+ var TRANSLATIONS_DIR = "translations";
2546
+ var MAX_PROJECTS_INDEX_BYTES = 2 * 1024 * 1024;
2547
+ var MAX_PROJECT_FILES_BYTES = 32 * 1024 * 1024;
2548
+ var MAX_ANALYSIS_BYTES = 16 * 1024 * 1024;
2549
+ var MAX_TRANSLATIONS_CACHE_BYTES = 32 * 1024 * 1024;
2550
+ var DEFAULT_HEAL_EVERY_RUNS = 20;
2551
+
2552
+ // src/cache/setup/policy.ts
2553
+ function isProjectCacheWritable(state) {
2554
+ return state.enabled && !state.readOnly;
2555
+ }
2556
+ function isFileRecordMap(value) {
2557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2558
+ }
2559
+ function validateProjectFilesPayload(data, _filePath) {
2560
+ if (!data || typeof data !== "object") {
2561
+ return { ok: false, message: "root is not an object" };
2562
+ }
2563
+ const o = data;
2564
+ if (!isFileRecordMap(o.files)) {
2565
+ return { ok: false, message: "missing or invalid field: files" };
2566
+ }
2567
+ if (o.localeSegments !== void 0 && !isFileRecordMap(o.localeSegments)) {
2568
+ return { ok: false, message: "invalid field: localeSegments" };
2569
+ }
2570
+ if (o.localesLayout !== void 0) {
2571
+ const layout = o.localesLayout;
2572
+ if (!layout || typeof layout !== "object" || typeof layout.mode !== "string" || typeof layout.structure !== "string" || typeof layout.directory !== "string" || typeof layout.source !== "string") {
2573
+ return { ok: false, message: "invalid field: localesLayout" };
2574
+ }
2575
+ }
2576
+ if (o.version !== void 0 && o.version !== CACHE_SCHEMA_VERSION) {
2577
+ return { ok: false, message: `unsupported cache schema version: ${String(o.version)}` };
2578
+ }
2579
+ return { ok: true, files: data };
2580
+ }
2581
+ function validateProjectRunEnvelope(data, _filePath) {
2582
+ if (!data || typeof data !== "object") {
2583
+ return { ok: false, message: "root is not an object" };
2584
+ }
2585
+ const o = data;
2586
+ if (!("data" in o)) {
2587
+ return { ok: false, message: "missing required field: data" };
2588
+ }
2589
+ if (o.inputFilesEpoch !== void 0 && typeof o.inputFilesEpoch !== "string") {
2590
+ return { ok: false, message: "invalid field type: inputFilesEpoch" };
2591
+ }
2592
+ return { ok: true, run: data };
2593
+ }
2594
+ function loadProjectRunEnvelope(state, runtime) {
2595
+ const warnings = [];
2596
+ const filePath = state.analysisPath;
2597
+ const { data, warning } = readJsonFileWithLimit(filePath, MAX_ANALYSIS_BYTES, runtime);
2598
+ if (warning) {
2599
+ warnings.push({ ...warning, path: filePath });
2600
+ return { warnings };
2601
+ }
2602
+ if (data === void 0) return { warnings };
2603
+ const validated = validateProjectRunEnvelope(data);
2604
+ if (!validated.ok) {
2605
+ warnings.push({
2606
+ code: "cache_malformed",
2607
+ message: `cache analysis envelope invalid (${validated.message})`,
2608
+ path: filePath
2609
+ });
2610
+ return { warnings };
2611
+ }
2612
+ return { run: validated.run, warnings };
2613
+ }
2614
+ function tryDeleteCacheFile(runtime, filePath) {
2615
+ try {
2616
+ if (!assertSyncPortResult(runtime.fs.exists(filePath), "fs.exists", filePath)) return;
2617
+ assertSyncPortResult(runtime.fs.deleteFile(filePath), "fs.deleteFile", filePath);
2618
+ } catch {
2619
+ }
2620
+ }
2621
+
2622
+ // src/cache/io/projects.ts
2623
+ function defaultProjectsIndex(runtime) {
2624
+ return {
2625
+ version: CACHE_SCHEMA_VERSION,
2626
+ updatedAt: nowIso(runtime),
2627
+ projects: {},
2628
+ maintenance: { runCount: 0, healEveryRuns: DEFAULT_HEAL_EVERY_RUNS }
2629
+ };
2630
+ }
2631
+ function loadProjectsIndex(state, runtime) {
2632
+ const warnings = [];
2633
+ if (!state.enabled) return { index: defaultProjectsIndex(runtime), warnings };
2634
+ const { data, warning } = readJsonFileWithLimit(state.metaPath, MAX_PROJECTS_INDEX_BYTES, runtime);
2635
+ if (warning) warnings.push(warning);
2636
+ if (!data || typeof data !== "object" || typeof data.projects !== "object" || typeof data.maintenance !== "object" || typeof data.maintenance.runCount !== "number" || typeof data.maintenance.healEveryRuns !== "number") {
2637
+ return { index: defaultProjectsIndex(runtime), warnings };
2638
+ }
2639
+ return { index: data, warnings };
2640
+ }
2641
+ function saveProjectsIndex(state, index, runtime) {
2642
+ if (!state.enabled) return void 0;
2643
+ if (!isProjectCacheWritable(state)) {
2644
+ return {
2645
+ code: "cache_read_only",
2646
+ message: "cache is read-only; skipped persisting meta index",
2647
+ path: state.metaPath
2648
+ };
2649
+ }
2650
+ return writeJsonAtomic(state.metaPath, { ...index, updatedAt: nowIso(runtime), version: CACHE_SCHEMA_VERSION }, runtime);
2651
+ }
2652
+ function normalizeProjectRootKey(projectRoot) {
2653
+ const normalized = toPosixPath(projectRoot).normalize("NFC").replace(/\/+$/g, "");
2654
+ return `${normalized}/`;
2655
+ }
2656
+ function touchProjectIndex(state, index, runtime) {
2657
+ const next = {
2658
+ ...index,
2659
+ projects: { ...index.projects },
2660
+ maintenance: { ...index.maintenance }
2661
+ };
2662
+ const key = normalizeProjectRootKey(state.projectRoot);
2663
+ next.projects[key] = computeCacheProjectId(state.projectRoot, {
2664
+ path: runtime?.path,
2665
+ hashText: runtime?.hashText
2666
+ });
2667
+ next.maintenance.runCount += 1;
2668
+ return next;
2669
+ }
2670
+ function shouldHeal(index) {
2671
+ const every = Math.max(1, Math.trunc(index.maintenance.healEveryRuns || DEFAULT_HEAL_EVERY_RUNS));
2672
+ return index.maintenance.runCount % every === 0;
2673
+ }
2674
+ function maybeHealCacheIndex(state, index, runtime) {
2675
+ const warnings = [];
2676
+ if (!state.enabled || !shouldHeal(index)) {
2677
+ return { index, warnings, healed: false };
2678
+ }
2679
+ const next = {
2680
+ ...index,
2681
+ projects: { ...index.projects },
2682
+ maintenance: { ...index.maintenance, lastHealAt: nowIso(runtime) }
2683
+ };
2684
+ const projectsRoot = runtime.path.join(state.rootDir, "projects");
2685
+ for (const [projectRoot, id] of Object.entries(next.projects)) {
2686
+ const projectDir = runtime.path.join(projectsRoot, id);
2687
+ const kind = assertSyncPortResult(runtime.fs.statKind(projectDir), "fs.statKind", projectDir);
2688
+ if (kind !== "directory") {
2689
+ delete next.projects[projectRoot];
2690
+ }
2691
+ }
2692
+ const referencedIds = new Set(Object.values(next.projects));
2693
+ try {
2694
+ if (assertSyncPortResult(runtime.fs.exists(projectsRoot), "fs.exists", projectsRoot)) {
2695
+ const dirs = assertSyncPortResult(runtime.fs.listDir(projectsRoot), "fs.listDir", projectsRoot);
2696
+ for (const entry of dirs) {
2697
+ if (entry.kind !== "directory") continue;
2698
+ if (referencedIds.has(entry.name)) continue;
2699
+ const orphanDir = runtime.path.join(projectsRoot, entry.name);
2700
+ if (!runtime.deleteDir) continue;
2701
+ runtime.deleteDir(orphanDir);
2702
+ }
2703
+ }
2704
+ } catch (err) {
2705
+ warnings.push({
2706
+ code: "cache_io_error",
2707
+ message: `cache self-heal failed: ${err instanceof Error ? err.message : String(err)}`,
2708
+ path: projectsRoot
2709
+ });
2710
+ }
2711
+ return { index: next, warnings, healed: true };
2712
+ }
2713
+
2714
+ // src/cache/io/state.ts
2715
+ function defaultProjectFilesState(runtime) {
2716
+ return { version: CACHE_SCHEMA_VERSION, updatedAt: nowIso(runtime), files: {} };
2717
+ }
2718
+ function loadProjectFilesState(state, runtime) {
2719
+ const warnings = [];
2720
+ if (!state.enabled) return { files: defaultProjectFilesState(runtime), warnings };
2721
+ const { data, warning } = readJsonFileWithLimit(state.filesPath, MAX_PROJECT_FILES_BYTES, runtime);
2722
+ if (warning) warnings.push(warning);
2723
+ if (data === void 0) {
2724
+ return { files: defaultProjectFilesState(runtime), warnings };
2725
+ }
2726
+ const validated = validateProjectFilesPayload(data, state.filesPath);
2727
+ if (!validated.ok) {
2728
+ warnings.push({
2729
+ code: "cache_malformed",
2730
+ message: `cache files index invalid (${validated.message})`,
2731
+ path: state.filesPath
2732
+ });
2733
+ return { files: defaultProjectFilesState(runtime), warnings };
2734
+ }
2735
+ return { files: validated.files, warnings };
2736
+ }
2737
+ function saveProjectFilesState(state, files, runtime) {
2738
+ if (!state.enabled) return void 0;
2739
+ if (!isProjectCacheWritable(state)) {
2740
+ return {
2741
+ code: "cache_read_only",
2742
+ message: "cache is read-only; skipped persisting files index",
2743
+ path: state.filesPath
2744
+ };
2745
+ }
2746
+ return writeJsonAtomic(state.filesPath, { ...files, updatedAt: nowIso(runtime), version: CACHE_SCHEMA_VERSION }, runtime);
2747
+ }
2748
+ function loadProjectRunState(state, runtime) {
2749
+ if (!state.enabled) return { warnings: [] };
2750
+ return loadProjectRunEnvelope(state, runtime);
2751
+ }
2752
+ function saveProjectRunState(state, runtime, input) {
2753
+ if (!state.enabled) return void 0;
2754
+ if (!isProjectCacheWritable(state)) {
2755
+ return {
2756
+ code: "cache_read_only",
2757
+ message: "cache is read-only; skipped persisting analysis cache",
2758
+ path: state.analysisPath
2759
+ };
2760
+ }
2761
+ const payload = {
2762
+ version: CACHE_SCHEMA_VERSION,
2763
+ updatedAt: nowIso(runtime),
2764
+ projectId: state.projectId,
2765
+ data: input.data,
2766
+ ...input.inputFilesEpoch !== void 0 ? { inputFilesEpoch: input.inputFilesEpoch } : {}
2767
+ };
2768
+ return writeJsonAtomic(state.analysisPath, payload, runtime);
2769
+ }
2770
+
2771
+ // src/translator/cache/l2Io.ts
2772
+ function isTranslationCacheEntry(value) {
2773
+ if (!value || typeof value !== "object") return false;
2774
+ const o = value;
2775
+ return typeof o.text === "string" && typeof o.leafMeta === "object" && o.leafMeta !== null && typeof o.providerId === "string" && typeof o.createdAt === "string";
2776
+ }
2777
+ function isTranslationProviderId(value) {
2778
+ return value === "google" || value === "mymemory" || value === "deepl" || value === "libre" || value === "llm";
2779
+ }
2780
+ function validateTranslationLocaleCacheFile(data, _filePath) {
2781
+ if (!data || typeof data !== "object") {
2782
+ return { ok: false, message: "root is not an object" };
2783
+ }
2784
+ const o = data;
2785
+ if (typeof o.targetLang !== "string") {
2786
+ return { ok: false, message: "missing or invalid field: targetLang" };
2787
+ }
2788
+ if (typeof o.translateConfigEpoch !== "string") {
2789
+ return { ok: false, message: "missing or invalid field: translateConfigEpoch" };
2790
+ }
2791
+ if (typeof o.inputFilesEpoch !== "string") {
2792
+ return { ok: false, message: "missing or invalid field: inputFilesEpoch" };
2793
+ }
2794
+ if (!o.entries || typeof o.entries !== "object" || Array.isArray(o.entries)) {
2795
+ return { ok: false, message: "missing or invalid field: entries" };
2796
+ }
2797
+ for (const entry of Object.values(o.entries)) {
2798
+ if (!isTranslationCacheEntry(entry)) {
2799
+ return { ok: false, message: "invalid translation cache entry" };
2800
+ }
2801
+ if (!isTranslationProviderId(entry.providerId)) {
2802
+ return { ok: false, message: "invalid translation cache entry providerId" };
2803
+ }
2804
+ }
2805
+ if (o.version !== void 0 && o.version !== CACHE_SCHEMA_VERSION) {
2806
+ return { ok: false, message: `unsupported cache schema version: ${String(o.version)}` };
2807
+ }
2808
+ return { ok: true, state: data };
2809
+ }
2810
+ function loadTranslationLocaleCacheFile(filePath, runtime) {
2811
+ const warnings = [];
2812
+ const { data, warning } = readJsonFileWithLimit(filePath, MAX_TRANSLATIONS_CACHE_BYTES, runtime);
2813
+ if (warning) {
2814
+ warnings.push({ ...warning, path: filePath });
2815
+ return { warnings };
2816
+ }
2817
+ if (data === void 0) return { warnings };
2818
+ const validated = validateTranslationLocaleCacheFile(data);
2819
+ if (!validated.ok) {
2820
+ warnings.push({
2821
+ code: "cache_malformed",
2822
+ message: `cache translations store invalid (${validated.message})`,
2823
+ path: filePath
2824
+ });
2825
+ return { warnings };
2826
+ }
2827
+ return { locale: validated.state, warnings };
2828
+ }
2829
+
2830
+ // src/translator/cache/paths.ts
2831
+ function resolveTranslationsDir(state, runtime) {
2832
+ return runtime.path.join(state.projectDir, TRANSLATIONS_DIR);
2833
+ }
2834
+
2835
+ // src/translator/cache/maintenance.ts
2836
+ function prepareTranslationCacheLayout(state, runtime) {
2837
+ const warnings = [];
2838
+ if (!state.enabled) return warnings;
2839
+ const translationsDir = resolveTranslationsDir(state, runtime);
2840
+ try {
2841
+ assertSyncPortResult(runtime.fs.mkdirp(translationsDir), "fs.mkdirp", translationsDir);
2842
+ } catch (err) {
2843
+ warnings.push({
2844
+ code: "cache_dir_unavailable",
2845
+ message: `unable to create translations cache dir: ${err instanceof Error ? err.message : String(err)}`,
2846
+ path: translationsDir
2847
+ });
2848
+ return warnings;
2849
+ }
2850
+ return [...warnings, ...healTranslationCacheFiles(state, runtime)];
2851
+ }
2852
+ function healTranslationCacheFiles(state, runtime) {
2853
+ const warnings = [];
2854
+ if (!state.enabled) return warnings;
2855
+ const translationsDir = resolveTranslationsDir(state, runtime);
2856
+ let entries = [];
2857
+ try {
2858
+ const kind = assertSyncPortResult(runtime.fs.statKind(translationsDir), "fs.statKind", translationsDir);
2859
+ if (kind !== "directory") return warnings;
2860
+ entries = assertSyncPortResult(runtime.fs.listDir(translationsDir), "fs.listDir", translationsDir);
2861
+ } catch {
2862
+ return warnings;
2863
+ }
2864
+ for (const entry of entries) {
2865
+ if (entry.kind !== "file" || !entry.name.endsWith(".json")) continue;
2866
+ const filePath = runtime.path.join(translationsDir, entry.name);
2867
+ const loaded = loadTranslationLocaleCacheFile(filePath, runtime);
2868
+ warnings.push(...loaded.warnings);
2869
+ const invalid = loaded.warnings.some((w) => w.code === "cache_malformed" || w.code === "cache_oversize");
2870
+ if (invalid) {
2871
+ tryDeleteCacheFile(runtime, filePath);
2872
+ }
2873
+ }
2874
+ return warnings;
2875
+ }
2876
+
2877
+ // src/cache/setup/maintenance.ts
2878
+ function prepareCacheForRun(state, runtime) {
2879
+ const warnings = [];
2880
+ const loaded = loadProjectsIndex(state, runtime);
2881
+ warnings.push(...loaded.warnings);
2882
+ const touched = touchProjectIndex(state, loaded.index, runtime);
2883
+ const healed = maybeHealCacheIndex(state, touched, runtime);
2884
+ warnings.push(...healed.warnings);
2885
+ const saveWarn = saveProjectsIndex(state, healed.index, runtime);
2886
+ if (saveWarn) warnings.push(saveWarn);
2887
+ try {
2888
+ assertSyncPortResult(runtime.fs.mkdirp(state.projectDir), "fs.mkdirp", state.projectDir);
2889
+ } catch (err) {
2890
+ warnings.push({
2891
+ code: "cache_dir_unavailable",
2892
+ message: `unable to create cache project dir: ${err instanceof Error ? err.message : String(err)}`,
2893
+ path: state.projectDir
2894
+ });
2895
+ return { index: healed.index, warnings };
2896
+ }
2897
+ const fileState = loadProjectFilesState(state, runtime);
2898
+ warnings.push(...fileState.warnings);
2899
+ const analysisState = loadProjectRunState(state, runtime);
2900
+ warnings.push(...analysisState.warnings);
2901
+ warnings.push(...prepareTranslationCacheLayout(state, runtime));
2902
+ const hasInvalidFiles = fileState.warnings.some((w) => w.code === "cache_malformed" || w.code === "cache_oversize");
2903
+ const hasInvalidAnalysis = analysisState.warnings.some((w) => w.code === "cache_malformed" || w.code === "cache_oversize");
2904
+ if (hasInvalidFiles) {
2905
+ try {
2906
+ assertSyncPortResult(runtime.fs.deleteFile(state.filesPath), "fs.deleteFile", state.filesPath);
2907
+ } catch {
2908
+ }
2909
+ }
2910
+ if (hasInvalidAnalysis) {
2911
+ tryDeleteCacheFile(runtime, state.analysisPath);
2912
+ }
2913
+ try {
2914
+ assertSyncPortResult(runtime.fs.mkdirp(runtime.path.dirname(state.filesPath)), "fs.mkdirp", runtime.path.dirname(state.filesPath));
2915
+ } catch {
2916
+ }
2917
+ return { index: healed.index, warnings };
2918
+ }
2919
+
2920
+ // src/cache/engine.ts
2921
+ function computeInputFilesEpoch(files, hashText) {
2922
+ const keys = Object.keys(files).sort();
2923
+ const canonical = keys.map((k) => {
2924
+ const r = files[k];
2925
+ return `${k} ${r.hash} ${String(r.size)}
2926
+ `;
2927
+ }).join("");
2928
+ return computeCacheContentHash(canonical, hashText);
2929
+ }
2930
+ function diffProjectFiles(previous, current) {
2931
+ const added = [];
2932
+ const changed = [];
2933
+ const deleted = [];
2934
+ const unchanged = [];
2935
+ for (const [k, next] of Object.entries(current)) {
2936
+ const prev = previous[k];
2937
+ if (!prev) {
2938
+ added.push(k);
2939
+ continue;
2940
+ }
2941
+ if (prev.hash !== next.hash || prev.size !== next.size || prev.mtimeMs !== next.mtimeMs) {
2942
+ changed.push(k);
2943
+ } else {
2944
+ unchanged.push(k);
2945
+ }
2946
+ }
2947
+ for (const k of Object.keys(previous)) {
2948
+ if (!(k in current)) deleted.push(k);
2949
+ }
2950
+ return { added, changed, deleted, unchanged };
2951
+ }
2952
+
2953
+ // src/cache/localesLayout.ts
2954
+ function resolveCachedLocalesLayout(locales) {
2955
+ return {
2956
+ mode: locales.mode ?? "flat_file",
2957
+ structure: locales.structure ?? "locale_file",
2958
+ directory: locales.directory,
2959
+ source: locales.source
2960
+ };
2961
+ }
2962
+ function layoutMatches(a, b) {
2963
+ if (a === void 0) return false;
2964
+ return a.mode === b.mode && a.structure === b.structure && a.directory === b.directory && a.source === b.source;
2965
+ }
2966
+
2967
+ // src/cache/trackedFiles.ts
2968
+ var SYNTHETIC_SOURCE_LOCALE_KEY = "__source_locale__";
2969
+ function omitSyntheticSourceKey(files) {
2970
+ if (!(SYNTHETIC_SOURCE_LOCALE_KEY in files)) return files;
2971
+ const out = { ...files };
2972
+ delete out[SYNTHETIC_SOURCE_LOCALE_KEY];
2973
+ return out;
2974
+ }
2975
+ function mergeTrackedFileMaps(files, localeSegments) {
2976
+ return { ...files, ...localeSegments };
2977
+ }
2978
+ function hashFileRecord(absPath, runtime, now) {
2979
+ const content = readRuntimeFsTextSync(absPath, runtime.fs);
2980
+ return {
2981
+ hash: computeCacheContentHash(content, runtime.hashText),
2982
+ size: textByteLength(content, runtime),
2983
+ mtimeMs: 0,
2984
+ updatedAt: now
2985
+ };
2986
+ }
2987
+ function buildSrcFileRecords(input) {
2988
+ const paths = listSourceFiles({ fs: input.runtime.fs, path: input.runtime.path }, input.srcRoot, input.exclude);
2989
+ const now = new Date(input.runtime.system.now()).toISOString();
2990
+ const out = {};
2991
+ for (const absPath of paths) {
2992
+ const rel = input.runtime.path.relative(input.srcRoot, absPath).replace(/\\/g, "/");
2993
+ out[rel] = hashFileRecord(absPath, input.runtime, now);
2994
+ }
2995
+ return out;
2996
+ }
2997
+ function buildLocaleSegmentRecords(input) {
2998
+ const layout = resolveLocalesLayout(input.locales, input.localesDir);
2999
+ const { segments } = listLocaleSegments({ layout, fs: input.runtime.fs, path: input.runtime.path });
3000
+ const now = new Date(input.runtime.system.now()).toISOString();
3001
+ const out = {};
3002
+ for (const segment of segments) {
3003
+ out[segment.relativePath] = hashFileRecord(segment.absolutePath, input.runtime, now);
3004
+ }
3005
+ return out;
3006
+ }
3007
+ function buildTrackedProjectFilesCurrent(input) {
3008
+ const localesLayout = resolveCachedLocalesLayout(input.locales);
3009
+ const files = input.scanSrc ? buildSrcFileRecords(input) : omitSyntheticSourceKey(input.reuseSrcFiles ?? {});
3010
+ const localeSegments = buildLocaleSegmentRecords(input);
3011
+ return {
3012
+ files,
3013
+ localeSegments,
3014
+ localesLayout,
3015
+ merged: mergeTrackedFileMaps(files, localeSegments)
3016
+ };
3017
+ }
3018
+
3019
+ // src/types/cache/filesIndex.ts
3020
+ function filesIndexIsUsable(status) {
3021
+ return status.kind === "ok";
3022
+ }
3023
+
3024
+ // src/cache/deltaClassify.ts
3025
+ function normalizeRelPath(path) {
3026
+ return path.replace(/\\/g, "/");
3027
+ }
3028
+ function unionKeys(a, b) {
3029
+ return /* @__PURE__ */ new Set([...a, ...b]);
3030
+ }
3031
+ function classifyCacheFileDelta(input) {
3032
+ const src = { added: [], changed: [], deleted: [] };
3033
+ const sourceLocale = [];
3034
+ const targetLocale = [];
3035
+ const sourceKey = normalizeRelPath(input.sourceLocaleSegmentKey);
3036
+ const knownSrcKeys = unionKeys(input.currentSrcFileKeys, input.baselineSrcFileKeys);
3037
+ const knownLocaleKeys = unionKeys(input.currentLocaleSegmentKeys, input.baselineLocaleSegmentKeys);
3038
+ const classifyPath = (path, kind) => {
3039
+ if (knownSrcKeys.has(path)) {
3040
+ src[kind].push(path);
3041
+ return;
3042
+ }
3043
+ if (knownLocaleKeys.has(path)) {
3044
+ if (path === sourceKey) sourceLocale.push(path);
3045
+ else targetLocale.push(path);
3046
+ }
3047
+ };
3048
+ for (const path of input.delta.added.map(normalizeRelPath)) {
3049
+ classifyPath(path, "added");
3050
+ }
3051
+ for (const path of input.delta.changed.map(normalizeRelPath)) {
3052
+ classifyPath(path, "changed");
3053
+ }
3054
+ for (const path of input.delta.deleted.map(normalizeRelPath)) {
3055
+ classifyPath(path, "deleted");
3056
+ }
3057
+ const layoutChanged = filesIndexIsUsable(input.filesIndexStatus) && !layoutMatches(input.previousLayout, input.currentLayout);
3058
+ return {
3059
+ src,
3060
+ sourceLocale,
3061
+ targetLocale,
3062
+ layoutChanged,
3063
+ filesIndexStatus: input.filesIndexStatus
3064
+ };
3065
+ }
3066
+ function srcDeltaIsEmpty(src) {
3067
+ return src.added.length + src.changed.length + src.deleted.length === 0;
3068
+ }
3069
+ function countSrcDeltaAffected(src) {
3070
+ return src.added.length + src.changed.length + src.deleted.length;
3071
+ }
3072
+
3073
+ // src/cache/filesIndexStatus.ts
3074
+ function isDefaultEmptyIndex(prev) {
3075
+ const noSrc = Object.keys(prev.files).length === 0;
3076
+ const noLocales = !prev.localeSegments || Object.keys(prev.localeSegments).length === 0;
3077
+ const noLayout = prev.localesLayout === void 0;
3078
+ return noSrc && noLocales && noLayout;
3079
+ }
3080
+ function resolveFilesIndexStatus(input) {
3081
+ const indexWarnings = input.warnings.filter((w) => w.path === void 0 || w.path === input.filesPath);
3082
+ if (indexWarnings.some((w) => w.code === "cache_malformed" || w.code === "cache_oversize")) {
3083
+ return { kind: "malformed" };
3084
+ }
3085
+ const exists = input.runtime.fs.exists(input.filesPath);
3086
+ if (!exists) {
3087
+ return { kind: "missing" };
3088
+ }
3089
+ if (isDefaultEmptyIndex(input.prev)) {
3090
+ return { kind: "empty" };
3091
+ }
3092
+ return { kind: "ok" };
3093
+ }
3094
+
3095
+ // src/cache/resolveConfig.ts
3096
+ function clampThresholdPercent(value) {
3097
+ if (value < 0) return 0;
3098
+ if (value > 100) return 100;
3099
+ return value;
3100
+ }
3101
+ function resolveCacheConfig(cache) {
3102
+ const profileId = cache?.profile ?? DEFAULT_CACHE_PROFILE_ID;
3103
+ const profile = CACHE_PROFILE_DEFAULTS[profileId] ?? CACHE_PROFILE_DEFAULTS[DEFAULT_CACHE_PROFILE_ID];
3104
+ const threshold = cache?.fullRescanThresholdPercent !== void 0 ? clampThresholdPercent(cache.fullRescanThresholdPercent) : profile.fullRescanThresholdPercent;
3105
+ return {
3106
+ enabled: cache?.enabled ?? true,
3107
+ profile: profileId,
3108
+ mode: cache?.mode ?? profile.mode,
3109
+ rebuild: cache?.rebuild ?? profile.rebuild,
3110
+ fullRescanThresholdPercent: threshold,
3111
+ ...cache?.dir !== void 0 ? { dir: cache.dir } : {}
3112
+ };
3113
+ }
3114
+ function resolveCacheRebuildConfig(cache) {
3115
+ const resolved = resolveCacheConfig(cache);
3116
+ return {
3117
+ rebuild: resolved.rebuild,
3118
+ fullRescanThresholdPercent: resolved.fullRescanThresholdPercent
3119
+ };
3120
+ }
3121
+
3122
+ // src/cache/rebuildPolicy.ts
3123
+ function filesIndexRebuildReason(status) {
3124
+ switch (status.kind) {
3125
+ case "missing":
3126
+ return "files_index_missing";
3127
+ case "malformed":
3128
+ return "files_index_malformed";
3129
+ case "empty":
3130
+ return "files_index_empty";
3131
+ default:
3132
+ return "files_index_missing";
3133
+ }
3134
+ }
3135
+ function decideAnalysisRebuild(input) {
3136
+ if (input.config.rebuild === "full") {
3137
+ return { strategy: "full", reason: "config_rebuild_full" };
3138
+ }
3139
+ if (!input.hasPrevious) {
3140
+ if (!filesIndexIsUsable(input.classified.filesIndexStatus)) {
3141
+ return { strategy: "full", reason: filesIndexRebuildReason(input.classified.filesIndexStatus) };
3142
+ }
3143
+ return { strategy: "full", reason: "no_previous_cache" };
3144
+ }
3145
+ if (!filesIndexIsUsable(input.classified.filesIndexStatus)) {
3146
+ return { strategy: "full", reason: "files_index_stale" };
3147
+ }
3148
+ if (input.classified.layoutChanged) {
3149
+ return { strategy: "full", reason: "layout_changed" };
3150
+ }
3151
+ if (input.classified.sourceLocale.length > 0) {
3152
+ const sourceOnly = input.classified.targetLocale.length === 0 && srcDeltaIsEmpty(input.classified.src);
3153
+ if (sourceOnly) {
3154
+ return { strategy: "partial", reason: "source_locale_partial" };
3155
+ }
3156
+ return { strategy: "full", reason: "source_locale_changed" };
3157
+ }
3158
+ if (input.classified.targetLocale.length > 0 && srcDeltaIsEmpty(input.classified.src)) {
3159
+ return { strategy: "reuse", reason: "target_locale_only" };
3160
+ }
3161
+ if (srcDeltaIsEmpty(input.classified.src)) {
3162
+ return { strategy: "full", reason: "locale_or_non_src_changed" };
3163
+ }
3164
+ const srcAffected = countSrcDeltaAffected(input.classified.src);
3165
+ const trackedSrcCount = input.trackedSrcCount;
3166
+ const threshold = input.config.fullRescanThresholdPercent;
3167
+ const percent = trackedSrcCount > 0 ? srcAffected / trackedSrcCount * 100 : 100;
3168
+ if (percent >= threshold) {
3169
+ return {
3170
+ strategy: "full",
3171
+ reason: "src_threshold",
3172
+ thresholdPercent: threshold,
3173
+ srcAffected,
3174
+ trackedSrcCount
3175
+ };
3176
+ }
3177
+ return {
3178
+ strategy: "partial",
3179
+ reason: "src_partial",
3180
+ srcAffected,
3181
+ trackedSrcCount
3182
+ };
3183
+ }
3184
+
3185
+ // src/cache/dispatch.ts
3186
+ function assertLocalesInput(input) {
3187
+ if (input.localesDir === void 0 || input.locales === void 0) {
3188
+ throw new Error("cache dispatch requires localesDir and locales");
3189
+ }
3190
+ }
3191
+ function baselineMergedFromDisk(prev) {
3192
+ return mergeTrackedFileMaps(omitSyntheticSourceKey(prev.files), prev.localeSegments ?? {});
3193
+ }
3194
+ function resolveTrackedCurrent(input, prev) {
3195
+ const locales = input.locales;
3196
+ const localesDir = input.localesDir;
3197
+ const hasCachedLayout = prev.localesLayout !== void 0;
3198
+ const layoutMatch = layoutMatches(prev.localesLayout, resolveCachedLocalesLayout(locales));
3199
+ const scanSrc = !hasCachedLayout || layoutMatch;
3200
+ return buildTrackedProjectFilesCurrent({
3201
+ runtime: input.runtime,
3202
+ srcRoot: input.srcRoot,
3203
+ exclude: input.exclude,
3204
+ localesDir,
3205
+ locales,
3206
+ scanSrc,
3207
+ ...scanSrc ? {} : { reuseSrcFiles: omitSyntheticSourceKey(prev.files) }
3208
+ });
3209
+ }
3210
+ function sourceLocaleSegmentKey(input) {
3211
+ return input.runtime.path.relative(input.localesDir, input.sourceLocalePath).replace(/\\/g, "/");
3212
+ }
3213
+ function buildProducerContext(input) {
3214
+ const currentSrcFileKeys = new Set(Object.keys(omitSyntheticSourceKey(input.tracked.files)));
3215
+ const baselineSrcFileKeys = new Set(Object.keys(omitSyntheticSourceKey(input.prev.files)));
3216
+ const currentLocaleSegmentKeys = new Set(Object.keys(input.tracked.localeSegments));
3217
+ const baselineLocaleSegmentKeys = new Set(Object.keys(input.prev.localeSegments ?? {}));
3218
+ const classified = classifyCacheFileDelta({
3219
+ delta: input.delta,
3220
+ currentSrcFileKeys,
3221
+ baselineSrcFileKeys,
3222
+ currentLocaleSegmentKeys,
3223
+ baselineLocaleSegmentKeys,
3224
+ sourceLocaleSegmentKey: sourceLocaleSegmentKey(input.input),
3225
+ previousLayout: input.prev.localesLayout,
3226
+ currentLayout: input.tracked.localesLayout,
3227
+ filesIndexStatus: input.filesIndexStatus
3228
+ });
3229
+ const rebuildConfig = input.input.rebuildConfig ?? resolveCacheRebuildConfig({ profile: "balanced" });
3230
+ const trackedSrcCount = currentSrcFileKeys.size;
3231
+ const analysisRebuild = {
3232
+ ...decideAnalysisRebuild({
3233
+ config: rebuildConfig,
3234
+ classified,
3235
+ hasPrevious: input.previous !== void 0,
3236
+ trackedSrcCount
3237
+ }),
3238
+ srcDelta: classified.src
3239
+ };
3240
+ return {
3241
+ delta: input.delta,
3242
+ classified,
3243
+ previous: input.previous,
3244
+ trackedSrcCount,
3245
+ rebuildConfig,
3246
+ analysisRebuild
3247
+ };
3248
+ }
3249
+ function persistFilesAndRunState(input) {
3250
+ const saveFilesWarn = saveProjectFilesState(input.state, input.nextIndex, input.runtime);
3251
+ if (saveFilesWarn) input.warnings.push(saveFilesWarn);
3252
+ const saveRunWarn = saveProjectRunState(input.state, input.runtime, {
3253
+ data: input.data,
3254
+ inputFilesEpoch: input.inputFilesEpoch
3255
+ });
3256
+ if (saveRunWarn) input.warnings.push(saveRunWarn);
3257
+ }
3258
+ function getOrBuildCachedProjectData(input) {
3259
+ const warnings = [];
3260
+ const state = input.state;
3261
+ const paths = {
3262
+ meta: state.metaPath,
3263
+ files: state.filesPath,
3264
+ analysis: state.analysisPath,
3265
+ projectDir: state.projectDir
3266
+ };
3267
+ const rebuildConfig = input.rebuildConfig ?? resolveCacheRebuildConfig({ profile: "balanced" });
3268
+ if (!state.enabled) {
3269
+ return {
3270
+ data: input.producer(),
3271
+ cache: {
3272
+ status: state.reason === "cli_no_cache" ? "bypass" : "disabled",
3273
+ reason: "no_cache",
3274
+ warnings,
3275
+ paths
3276
+ }
3277
+ };
3278
+ }
3279
+ const prepared = prepareCacheForRun(state, input.runtime);
3280
+ warnings.push(...prepared.warnings);
3281
+ const prevFiles = loadProjectFilesState(state, input.runtime);
3282
+ warnings.push(...prevFiles.warnings);
3283
+ const prev = prevFiles.files;
3284
+ const filesIndexStatus = resolveFilesIndexStatus({
3285
+ prev,
3286
+ warnings: prevFiles.warnings,
3287
+ filesPath: state.filesPath,
3288
+ runtime: input.runtime
3289
+ });
3290
+ assertLocalesInput(input);
3291
+ const tracked = resolveTrackedCurrent(input, prev);
3292
+ const currentFiles = tracked.merged;
3293
+ const baseline = input.baselineFiles ?? baselineMergedFromDisk(prev);
3294
+ const delta = diffProjectFiles(baseline, currentFiles);
3295
+ const hasFileChanges = delta.added.length + delta.changed.length + delta.deleted.length > 0;
3296
+ const inputFilesEpoch = computeInputFilesEpoch(currentFiles, input.runtime.hashText);
3297
+ const prevRun = loadProjectRunState(state, input.runtime);
3298
+ warnings.push(...prevRun.warnings);
3299
+ let previous;
3300
+ if (prevRun.run?.data !== void 0 && input.parseCachedData) {
3301
+ const parsedPrevious = input.parseCachedData(prevRun.run.data);
3302
+ if (parsedPrevious.ok) previous = parsedPrevious.data;
3303
+ }
3304
+ const filesIndexRecoverable = !filesIndexIsUsable(filesIndexStatus) && hasFileChanges && previous !== void 0 && prevRun.run?.inputFilesEpoch === inputFilesEpoch;
3305
+ if (filesIndexRecoverable && previous !== void 0) {
3306
+ const nextIndex2 = {
3307
+ ...prev,
3308
+ files: tracked.files,
3309
+ localeSegments: tracked.localeSegments,
3310
+ localesLayout: tracked.localesLayout
3311
+ };
3312
+ persistFilesAndRunState({
3313
+ state,
3314
+ runtime: input.runtime,
3315
+ nextIndex: nextIndex2,
3316
+ data: previous,
3317
+ inputFilesEpoch,
3318
+ warnings
3319
+ });
3320
+ return {
3321
+ data: previous,
3322
+ cache: {
3323
+ status: "hit",
3324
+ reason: "files_index_recovered",
3325
+ warnings,
3326
+ delta,
3327
+ paths,
3328
+ analysisRebuild: { strategy: "reuse", reason: "files_index_recovered" },
3329
+ filesIndexStatus
3330
+ }
3331
+ };
3332
+ }
3333
+ let missReason = hasFileChanges ? "files_changed" : "run_missing";
3334
+ let inputFilesEpochDebug;
3335
+ if (!hasFileChanges && prevRun.run?.data !== void 0) {
3336
+ if (prevRun.run.inputFilesEpoch !== inputFilesEpoch) {
3337
+ missReason = "run_binding_stale";
3338
+ inputFilesEpochDebug = { cached: prevRun.run.inputFilesEpoch, current: inputFilesEpoch };
3339
+ } else {
3340
+ const parsed = input.parseCachedData ? input.parseCachedData(prevRun.run.data) : { ok: true, data: prevRun.run.data };
3341
+ if (parsed.ok) {
3342
+ return {
3343
+ data: parsed.data,
3344
+ cache: {
3345
+ status: "hit",
3346
+ reason: "cache_hit",
3347
+ warnings,
3348
+ delta,
3349
+ paths
3350
+ }
3351
+ };
3352
+ }
3353
+ missReason = "run_invalid";
3354
+ }
3355
+ }
3356
+ const producerCtx = buildProducerContext({
3357
+ input: { ...input, rebuildConfig },
3358
+ delta,
3359
+ tracked,
3360
+ prev,
3361
+ previous,
3362
+ filesIndexStatus
3363
+ });
3364
+ if (previous !== void 0 && producerCtx.analysisRebuild?.strategy === "reuse" && producerCtx.analysisRebuild.reason === "target_locale_only") {
3365
+ const nextIndex2 = {
3366
+ ...prev,
3367
+ files: tracked.files,
3368
+ localeSegments: tracked.localeSegments,
3369
+ localesLayout: tracked.localesLayout
3370
+ };
3371
+ persistFilesAndRunState({
3372
+ state,
3373
+ runtime: input.runtime,
3374
+ nextIndex: nextIndex2,
3375
+ data: previous,
3376
+ inputFilesEpoch,
3377
+ warnings
3378
+ });
3379
+ return {
3380
+ data: previous,
3381
+ cache: {
3382
+ status: "miss",
3383
+ reason: missReason,
3384
+ warnings,
3385
+ delta,
3386
+ paths,
3387
+ analysisRebuild: producerCtx.analysisRebuild
3388
+ }
3389
+ };
3390
+ }
3391
+ const fresh = input.producer(producerCtx);
3392
+ const nextIndex = {
3393
+ ...prev,
3394
+ files: tracked.files,
3395
+ localeSegments: tracked.localeSegments,
3396
+ localesLayout: tracked.localesLayout
3397
+ };
3398
+ persistFilesAndRunState({
3399
+ state,
3400
+ runtime: input.runtime,
3401
+ nextIndex,
3402
+ data: fresh,
3403
+ inputFilesEpoch,
3404
+ warnings
3405
+ });
3406
+ return {
3407
+ data: fresh,
3408
+ cache: {
3409
+ status: "miss",
3410
+ reason: missReason,
3411
+ warnings,
3412
+ delta,
3413
+ paths,
3414
+ analysisRebuild: producerCtx.analysisRebuild,
3415
+ filesIndexStatus: filesIndexIsUsable(filesIndexStatus) ? void 0 : filesIndexStatus,
3416
+ ...inputFilesEpochDebug !== void 0 ? { inputFilesEpochDebug } : {}
3417
+ }
3418
+ };
3419
+ }
3420
+
3421
+ // src/shared/run/index.ts
3422
+ function emitRunEvent(emit, event) {
3423
+ if (!emit) return;
3424
+ try {
3425
+ emit(event);
3426
+ } catch {
3427
+ }
3428
+ }
3429
+ function emitRunMessage(emit, input) {
3430
+ emitRunEvent(emit, {
3431
+ type: "run.message",
3432
+ op: input.op,
3433
+ runId: input.runId,
3434
+ at: input.at ?? nowMs(),
3435
+ level: input.level,
3436
+ ...input.channel !== void 0 ? { channel: input.channel } : {},
3437
+ message: input.message,
3438
+ ...input.target !== void 0 ? { target: input.target } : {},
3439
+ ...input.path !== void 0 ? { path: input.path } : {},
3440
+ ...input.data !== void 0 ? { data: input.data } : {}
3441
+ });
3442
+ }
3443
+ function nowMs() {
3444
+ return Date.now();
3445
+ }
3446
+
3447
+ // src/cache/events.ts
3448
+ function describeCacheInvalidation(reason) {
3449
+ switch (reason) {
3450
+ case "files_changed":
3451
+ return "source files changed";
3452
+ case "files_index_recovered":
3453
+ return "files index rebuilt; analysis reused (project files unchanged)";
3454
+ case "run_binding_stale":
3455
+ return "stale (source files changed since last cache write)";
3456
+ case "run_invalid":
3457
+ return "cached data failed validation";
3458
+ case "run_missing":
3459
+ return "no cached data";
3460
+ case "cache_unavailable":
3461
+ return "cache unavailable";
3462
+ case "no_cache":
3463
+ return "cache disabled";
3464
+ case "cache_hit":
3465
+ case "producer_succeeded":
3466
+ return void 0;
3467
+ default:
3468
+ return void 0;
3469
+ }
3470
+ }
3471
+ function emitCacheDetail(input) {
3472
+ emitRunMessage(input.emit, {
3473
+ op: input.op,
3474
+ runId: input.runId,
3475
+ channel: "cache",
3476
+ level: "detail",
3477
+ message: input.message
3478
+ });
3479
+ }
3480
+ function emitCacheDeltaFiles(input) {
3481
+ const limit = 12;
3482
+ for (const file of input.files.slice(0, limit)) {
3483
+ emitCacheDetail({ ...input, message: ` ${input.kind}: ${file}` });
3484
+ }
3485
+ if (input.files.length > limit) {
3486
+ emitCacheDetail({ ...input, message: ` ${input.kind}: ... ${String(input.files.length - limit)} more` });
3487
+ }
3488
+ }
3489
+ function emitCacheDispatchMessages(input) {
3490
+ emitRunMessage(input.emit, {
3491
+ op: input.op,
3492
+ runId: input.runId,
3493
+ channel: "cache",
3494
+ level: "info",
3495
+ message: `${input.label} cache ${input.cache.status} (${input.cache.reason})`
3496
+ });
3497
+ const invalidation = describeCacheInvalidation(input.cache.reason);
3498
+ if (invalidation) {
3499
+ emitCacheDetail({ ...input, message: ` invalidated: ${invalidation}` });
3500
+ }
3501
+ if (input.cache.paths) {
3502
+ emitCacheDetail({ ...input, message: ` meta: ${input.cache.paths.meta}` });
3503
+ emitCacheDetail({ ...input, message: ` project: ${input.cache.paths.projectDir}` });
3504
+ emitCacheDetail({ ...input, message: ` files: ${input.cache.paths.files}` });
3505
+ emitCacheDetail({ ...input, message: ` analysis: ${input.cache.paths.analysis}` });
3506
+ }
3507
+ if (input.cache.delta) {
3508
+ const d = input.cache.delta;
3509
+ emitCacheDetail({
3510
+ ...input,
3511
+ message: ` file delta: +${String(d.added.length)} ~${String(d.changed.length)} -${String(d.deleted.length)} =${String(d.unchanged.length)}`
3512
+ });
3513
+ emitCacheDeltaFiles({ ...input, kind: "added", files: d.added });
3514
+ emitCacheDeltaFiles({ ...input, kind: "changed", files: d.changed });
3515
+ emitCacheDeltaFiles({ ...input, kind: "deleted", files: d.deleted });
3516
+ }
3517
+ if (input.cache.analysisRebuild) {
3518
+ const r = input.cache.analysisRebuild;
3519
+ if (r.strategy === "reuse") {
3520
+ const detail = r.reason === "target_locale_only" ? "target locale only (reusing analysis.json)" : "reusing analysis.json";
3521
+ emitCacheDetail({ ...input, message: ` analysis rebuild: skipped (${detail})` });
3522
+ } else if (r.strategy === "partial") {
3523
+ if (r.reason === "source_locale_partial") {
3524
+ emitCacheDetail({ ...input, message: " analysis rebuild: partial (source locale only, missingKeys updated)" });
3525
+ } else {
3526
+ const src = r.srcDelta;
3527
+ const changed = src?.changed.length ?? 0;
3528
+ const added = src?.added.length ?? 0;
3529
+ const deleted = src?.deleted.length ?? 0;
3530
+ emitCacheDetail({
3531
+ ...input,
3532
+ message: ` analysis rebuild: partial (${String(changed)} changed, ${String(added)} added, ${String(deleted)} deleted)`
3533
+ });
3534
+ }
3535
+ } else {
3536
+ let detail = "config rebuild=full";
3537
+ if (r.reason === "src_threshold") {
3538
+ const pct = r.trackedSrcCount && r.trackedSrcCount > 0 ? Math.round((r.srcAffected ?? 0) / r.trackedSrcCount * 100) : 100;
3539
+ detail = `threshold ${String(pct)}% (limit ${String(r.thresholdPercent ?? 40)}%)`;
3540
+ } else if (r.reason === "layout_changed") {
3541
+ detail = "layout changed";
3542
+ } else if (r.reason === "source_locale_changed") {
3543
+ detail = "source locale changed (with other deltas)";
3544
+ } else if (r.reason === "locale_or_non_src_changed") {
3545
+ detail = "locale or non-src delta";
3546
+ } else if (r.reason === "no_previous_cache") {
3547
+ detail = "no previous analysis cache";
3548
+ } else if (r.reason === "files_index_missing") {
3549
+ detail = "files.json missing";
3550
+ } else if (r.reason === "files_index_malformed") {
3551
+ detail = "files.json invalid or oversized";
3552
+ } else if (r.reason === "files_index_empty") {
3553
+ detail = "files.json empty";
3554
+ } else if (r.reason === "files_index_stale") {
3555
+ detail = "files index unusable and project files changed since last analysis";
3556
+ }
3557
+ emitCacheDetail({ ...input, message: ` analysis rebuild: full (${detail})` });
3558
+ }
3559
+ }
3560
+ if (input.cache.filesIndexStatus !== void 0 && input.cache.filesIndexStatus.kind !== "ok") {
3561
+ const kind = input.cache.filesIndexStatus.kind;
3562
+ const note = kind === "missing" ? "files.json missing \u2014 rebuilding fingerprints from disk" : kind === "malformed" ? "files.json invalid or oversized \u2014 rebuilding fingerprints from disk" : "files.json empty \u2014 rebuilding fingerprints from disk";
3563
+ emitCacheDetail({ ...input, message: ` files index: ${note}` });
3564
+ }
3565
+ for (const warn of input.cache.warnings) {
3566
+ const path = warn.path ? ` (${warn.path})` : "";
3567
+ emitRunMessage(input.emit, {
3568
+ op: input.op,
3569
+ runId: input.runId,
3570
+ channel: "cache",
3571
+ level: "info",
3572
+ message: `warning: ${warn.message}${path}`
3573
+ });
3574
+ }
3575
+ }
3576
+
3577
+ // src/validate/missingLiterals.ts
3578
+ function logicalPathsFromSourceLeaves(leaves) {
3579
+ return new Set(leaves.map((l) => l.path));
3580
+ }
3581
+ function computeMissingLiteralKeysFromLeaves(sourceLeaves, resolvedKeys) {
3582
+ const keySet = logicalPathsFromSourceLeaves(sourceLeaves);
3583
+ return [...resolvedKeys].filter((k) => !keySet.has(k)).sort(compareDottedPathDepth);
3584
+ }
3585
+ function compareDottedPathDepth(a, b) {
3586
+ const da = splitPath(a).length;
3587
+ const db = splitPath(b).length;
3588
+ if (da !== db) return da - db;
3589
+ return a.localeCompare(b);
3590
+ }
3591
+
3592
+ // src/analysis/rebuild.ts
3593
+ function normalizeRelPath2(path) {
3594
+ return path.replace(/\\/g, "/");
3595
+ }
3596
+ function pathSet(paths) {
3597
+ return new Set(paths.map(normalizeRelPath2));
3598
+ }
3599
+ function observationPath(obs) {
3600
+ return obs.span.filePath !== void 0 ? normalizeRelPath2(obs.span.filePath) : void 0;
3601
+ }
3602
+ function sitePath(site) {
3603
+ return site.filePath !== void 0 ? normalizeRelPath2(site.filePath) : void 0;
3604
+ }
3605
+ function filterByPaths(rows, pathOf, remove) {
3606
+ return rows.filter((row) => {
3607
+ const filePath = pathOf(row);
3608
+ return filePath === void 0 || !remove.has(filePath);
3609
+ });
3610
+ }
3611
+ function scanInputFromContext(ctx) {
3612
+ return {
3613
+ srcRoot: ctx.paths.srcRoot,
3614
+ functions: ctx.config.functions,
3615
+ runtime: ctx.adapters,
3616
+ exclude: ctx.config.exclude
3617
+ };
3618
+ }
3619
+ function absolutePathsForRel(ctx, relPaths) {
3620
+ const { path, srcRoot } = { path: ctx.adapters.path, srcRoot: ctx.paths.srcRoot };
3621
+ return relPaths.map((rel) => path.join(srcRoot, rel));
3622
+ }
3623
+ function partialScanIo(ctx, relPaths) {
3624
+ const absPaths = absolutePathsForRel(ctx, relPaths);
3625
+ return {
3626
+ listFiles: () => absPaths,
3627
+ readFile: (filePath) => readRuntimeFsTextSync(filePath, ctx.adapters.fs)
3628
+ };
3629
+ }
3630
+ function scanKeyObservationsForPaths(ctx, relPaths) {
3631
+ if (relPaths.length === 0) return [];
3632
+ return scanProjectKeyObservations({
3633
+ ...scanInputFromContext(ctx),
3634
+ ...partialScanIo(ctx, relPaths)
3635
+ });
3636
+ }
3637
+ function scanDynamicSitesForPaths(ctx, relPaths) {
3638
+ if (relPaths.length === 0) return [];
3639
+ return scanProjectDynamicKeySites({
3640
+ ...scanInputFromContext(ctx),
3641
+ ...partialScanIo(ctx, relPaths)
3642
+ });
3643
+ }
3644
+ function patchProjectAnalysisFromSrcDelta(ctx, previous, srcDelta) {
3645
+ const remove = pathSet([...srcDelta.deleted, ...srcDelta.changed]);
3646
+ const rescan = [...srcDelta.added, ...srcDelta.changed];
3647
+ const keyObservations = [
3648
+ ...filterByPaths(previous.keyObservations, observationPath, remove),
3649
+ ...scanKeyObservationsForPaths(ctx, rescan)
3650
+ ];
3651
+ const dynamicSites = [
3652
+ ...filterByPaths(previous.dynamicSites, sitePath, remove),
3653
+ ...scanDynamicSitesForPaths(ctx, rescan)
3654
+ ];
3655
+ const usage = literalKeyUsageFromObservations(keyObservations);
3656
+ const sourceLeaves = readSourceLocaleLeaves(ctx);
3657
+ const missingKeys = computeMissingLiteralKeysFromLeaves(sourceLeaves, usage.resolvedKeys);
3658
+ const sourceFilesScanned = previous.counts.sourceFilesScanned + srcDelta.added.length - srcDelta.deleted.length;
3659
+ return {
3660
+ version: 1,
3661
+ keyObservations,
3662
+ dynamicSites,
3663
+ missingKeys,
3664
+ counts: {
3665
+ keyObservations: keyObservations.length,
3666
+ dynamicSites: dynamicSites.length,
3667
+ sourceFilesScanned: Math.max(0, sourceFilesScanned),
3668
+ missingKeys: missingKeys.length
3669
+ }
3670
+ };
3671
+ }
3672
+ function patchProjectAnalysisFromSourceLocaleDelta(ctx, previous) {
3673
+ invalidateLocaleReadCacheForLocaleCode(ctx, sourceLocaleCodeFromContext(ctx));
3674
+ const usage = literalKeyUsageFromObservations(previous.keyObservations);
3675
+ const sourceLeaves = readSourceLocaleLeaves(ctx);
3676
+ const missingKeys = computeMissingLiteralKeysFromLeaves(sourceLeaves, usage.resolvedKeys);
3677
+ return {
3678
+ ...previous,
3679
+ missingKeys,
3680
+ counts: {
3681
+ ...previous.counts,
3682
+ missingKeys: missingKeys.length
3683
+ }
3684
+ };
3685
+ }
3686
+
3687
+ // src/analysis/project.ts
3688
+ function isRecord(v) {
3689
+ return typeof v === "object" && v !== null;
3690
+ }
3691
+ function isAnalysisCounts(v) {
3692
+ if (!isRecord(v)) return false;
3693
+ return typeof v.keyObservations === "number" && typeof v.dynamicSites === "number" && typeof v.sourceFilesScanned === "number" && typeof v.missingKeys === "number";
3694
+ }
3695
+ function parseProjectAnalysisCacheData(data) {
3696
+ if (!isRecord(data)) return { ok: false };
3697
+ if (data.version !== 1) return { ok: false };
3698
+ if (!Array.isArray(data.keyObservations) || !Array.isArray(data.dynamicSites)) return { ok: false };
3699
+ if (!Array.isArray(data.missingKeys)) return { ok: false };
3700
+ if (!isAnalysisCounts(data.counts)) return { ok: false };
3701
+ return {
3702
+ ok: true,
3703
+ data: {
3704
+ version: 1,
3705
+ keyObservations: data.keyObservations,
3706
+ dynamicSites: data.dynamicSites,
3707
+ missingKeys: data.missingKeys,
3708
+ counts: data.counts
3709
+ }
3710
+ };
3711
+ }
3712
+ function scanProjectAnalysis(ctx) {
3713
+ invalidateLocaleReadCacheForLocaleCode(ctx, sourceLocaleCodeFromContext(ctx));
3714
+ const scanInput = {
3715
+ srcRoot: ctx.paths.srcRoot,
3716
+ functions: ctx.config.functions,
3717
+ runtime: ctx.adapters,
3718
+ exclude: ctx.config.exclude
3719
+ };
3720
+ const keyObservations = scanProjectKeyObservations(scanInput);
3721
+ const dynamicSites = scanProjectDynamicKeySites(scanInput);
3722
+ const usage = literalKeyUsageFromObservations(keyObservations);
3723
+ const sourceLeaves = readSourceLocaleLeaves(ctx);
3724
+ const missingKeys = computeMissingLiteralKeysFromLeaves(sourceLeaves, usage.resolvedKeys);
3725
+ const projectFs = { fs: ctx.adapters.fs, path: ctx.adapters.path };
3726
+ const sourceFilesScanned = listSourceFiles(projectFs, ctx.paths.srcRoot, ctx.config.exclude).length;
3727
+ return {
3728
+ version: 1,
3729
+ keyObservations,
3730
+ dynamicSites,
3731
+ missingKeys,
3732
+ counts: {
3733
+ keyObservations: keyObservations.length,
3734
+ dynamicSites: dynamicSites.length,
3735
+ sourceFilesScanned,
3736
+ missingKeys: missingKeys.length
3737
+ }
3738
+ };
3739
+ }
3740
+ function produceProjectAnalysis(ctx, rebuild) {
3741
+ if (rebuild?.previous !== void 0 && rebuild.analysisRebuild?.strategy === "reuse") {
3742
+ return rebuild.previous;
3743
+ }
3744
+ if (rebuild?.previous !== void 0 && rebuild.analysisRebuild?.strategy === "partial") {
3745
+ if (rebuild.analysisRebuild.reason === "source_locale_partial") {
3746
+ return patchProjectAnalysisFromSourceLocaleDelta(ctx, rebuild.previous);
3747
+ }
3748
+ return patchProjectAnalysisFromSrcDelta(ctx, rebuild.previous, rebuild.classified.src);
3749
+ }
3750
+ return scanProjectAnalysis(ctx);
3751
+ }
3752
+ function withDerivedUsage(data, cache) {
3753
+ return {
3754
+ ...data,
3755
+ usage: literalKeyUsageFromObservations(data.keyObservations),
3756
+ ...cache !== void 0 ? { cache } : {}
3757
+ };
3758
+ }
3759
+ function resolveProjectAnalysis(ctx, opts = {}) {
3760
+ const cacheCtx = ctx.cache;
3761
+ if (cacheCtx === void 0) {
3762
+ return withDerivedUsage(scanProjectAnalysis(ctx));
3763
+ }
3764
+ const rebuildConfig = resolveCacheRebuildConfig(ctx.config.cache);
3765
+ const result = getOrBuildCachedProjectData({
3766
+ state: cacheCtx.state,
3767
+ runtime: cacheCtx.runtime,
3768
+ sourceLocalePath: ctx.paths.sourceLocale,
3769
+ srcRoot: ctx.paths.srcRoot,
3770
+ localesDir: ctx.paths.localesDir,
3771
+ locales: ctx.config.locales,
3772
+ exclude: ctx.config.exclude,
3773
+ rebuildConfig,
3774
+ producer: (rebuild) => produceProjectAnalysis(ctx, rebuild),
3775
+ parseCachedData: parseProjectAnalysisCacheData,
3776
+ baselineFiles: cacheCtx.baselineFiles
3777
+ });
3778
+ if (opts.emit !== void 0 && opts.op !== void 0) {
3779
+ emitCacheDispatchMessages({
3780
+ emit: opts.emit,
3781
+ op: opts.op,
3782
+ runId: opts.runId,
3783
+ label: "project analysis",
3784
+ cache: result.cache
3785
+ });
3786
+ }
3787
+ return withDerivedUsage(result.data, result.cache);
3788
+ }
3789
+
3790
+ // src/cleanup/run.ts
3791
+ function issuesFromDynamicScanCount(count) {
3792
+ if (count <= 0) return [];
3793
+ return [
3794
+ {
3795
+ severity: "warning",
3796
+ code: ISSUE_SCAN_DYNAMIC_KEY_SITES,
3797
+ message: `${String(count)} translation call(s) use a non-literal key \u2014 static analysis cannot enumerate computed keys as fixed paths.`,
3798
+ docPath: "dynamic/README"
3799
+ }
3800
+ ];
3801
+ }
3802
+ function issuesFromCleanupUncertainExcluded(excludedCount) {
3803
+ if (excludedCount <= 0) return [];
3804
+ return [
3805
+ {
3806
+ severity: "info",
3807
+ code: ISSUE_CLEANUP_UNCERTAIN_PATHS_EXCLUDED,
3808
+ message: `${String(excludedCount)} path(s) excluded under uncertain key prefix policy.`,
3809
+ docPath: "commands/cleanup/README"
3810
+ }
3811
+ ];
3812
+ }
3813
+ function issuesFromCleanupStringPresenceUnavailable(input) {
3814
+ if (input.skipStringPresenceCheck || input.stringPresenceAvailable) return [];
3815
+ return [
3816
+ {
3817
+ severity: "warning",
3818
+ code: ISSUE_CLEANUP_RIPGREP_UNAVAILABLE,
3819
+ message: "String-presence safety is unavailable \u2014 cleanup uses a narrower reference check. Configure a host string-presence probe for stronger safety.",
3820
+ docPath: "commands/cleanup/README"
3821
+ }
3822
+ ];
3823
+ }
3824
+ function emitCleanupMessage(host, input) {
3825
+ emitRunMessage(host.emit, { op: "cleanup", runId: host.runId, ...input });
3826
+ }
3827
+ function createCleanupSourceWritePlan(ctx, keysToRemove) {
3828
+ const writes = listCleanupSourceSegmentsForKeys(ctx, keysToRemove).flatMap((segment) => {
3829
+ const sourceJson = readLocaleJsonFromContextSync(ctx, segment.absolutePath);
3830
+ const applied = applyCleanupKeysToLocaleJson(sourceJson, keysToRemove);
3831
+ if (applied.removedPaths.length === 0) return [];
3832
+ return [
3833
+ {
3834
+ sourcePath: segment.absolutePath,
3835
+ relativePath: segment.relativePath,
3836
+ nextSourceJson: applied.next,
3837
+ removedPaths: applied.removedPaths
3838
+ }
3839
+ ];
3840
+ });
3841
+ const removedPaths = writes.flatMap((w) => w.removedPaths);
3842
+ const primary = writes[0];
3843
+ return {
3844
+ writes,
3845
+ keys: [...keysToRemove],
3846
+ sourcePath: primary?.sourcePath ?? ctx.paths.sourceLocale,
3847
+ nextSourceJson: primary?.nextSourceJson ?? readLocaleJsonFromContextSync(ctx, ctx.paths.sourceLocale),
3848
+ removedPaths
3849
+ };
3850
+ }
3851
+ function writeCleanupPlan(ctx, plan) {
3852
+ for (const write of plan.writes) {
3853
+ if (write.removedPaths.length === 0) continue;
3854
+ writeLocaleJsonFromContextSync(ctx, write.sourcePath, write.nextSourceJson);
3855
+ }
3856
+ }
3857
+ function emitCleanupAbortMessage(host, reason) {
3858
+ emitCleanupMessage(host, {
3859
+ level: "notice",
3860
+ message: reason === "no_keys_approved" ? "aborted: no keys approved for removal." : "aborted: user declined confirmation.",
3861
+ data: { reason }
3862
+ });
3863
+ }
3864
+ function emitCleanupAskIgnoredMessage(host) {
3865
+ emitCleanupMessage(host, { level: "info", message: "--ask ignored (not an interactive terminal)." });
3866
+ }
3867
+ function emitCleanupWriteIntro(host, input) {
3868
+ const fileHint = input.segmentFileCount > 1 ? `${String(input.segmentFileCount)} source segment file(s)` : "source locale file";
3869
+ emitCleanupMessage(host, {
3870
+ level: "warn",
3871
+ message: `removing ${String(input.removeCount)} path(s) from ${fileHint}. Run \`i18nprune sync\` afterwards to align target locale files.`,
3872
+ data: { removeCount: input.removeCount, segmentFileCount: input.segmentFileCount }
3873
+ });
3874
+ }
3875
+ function emitCleanupWriteDone(host, input) {
3876
+ if (input.wrote) {
3877
+ for (const write of input.plan.writes) {
3878
+ emitCleanupMessage(host, {
3879
+ level: "detail",
3880
+ message: `wrote ${write.sourcePath}`,
3881
+ path: write.sourcePath
3882
+ });
3883
+ }
3884
+ }
3885
+ const filesWritten = input.plan.writes.length;
3886
+ emitCleanupMessage(host, {
3887
+ level: "info",
3888
+ message: `finished \u2014 ${String(filesWritten)} source locale file(s) updated on disk.`,
3889
+ data: { filesWritten }
3890
+ });
3891
+ }
3892
+ function runCleanup(ctx, opts, host) {
3893
+ emitCleanupMessage(host, { level: "info", message: "scanning source locale and project sources for unused key paths..." });
3894
+ const eff = resolveReferenceConfig("cleanup", ctx.config);
3895
+ const analysis = resolveProjectAnalysis(ctx, { emit: host.emit, op: "cleanup", runId: host.runId });
3896
+ const dynamicSites = analysis.dynamicSites;
3897
+ const refCtx = buildKeyReferenceContextFromLiteralUsageAndDynamicSites(
3898
+ analysis.usage,
3899
+ dynamicSites,
3900
+ eff
3901
+ );
3902
+ const leaves = readCleanupSourceLeaves(ctx);
3903
+ const filterUncertain = eff.uncertainKeyPolicy === "protect" || eff.uncertainKeyPolicy === "warn_only";
3904
+ const { allKeyPaths, candidates, excludedUncertain } = computeCleanupCandidateKeys({
3905
+ leaves,
3906
+ usage: analysis.usage,
3907
+ preserve: ctx.config.policies?.preserve,
3908
+ uncertainPrefixes: refCtx.uncertainPrefixes,
3909
+ filterUncertainPrefixes: filterUncertain
3910
+ });
3911
+ if (excludedUncertain > 0) {
3912
+ emitCleanupMessage(host, {
3913
+ level: "info",
3914
+ message: `excluded ${String(excludedUncertain)} path(s) under uncertain key prefix(es) (${eff.uncertainKeyPolicy}).`,
3915
+ data: { excludedUncertain, uncertainKeyPolicy: eff.uncertainKeyPolicy }
3916
+ });
3917
+ }
3918
+ const skipStringPresenceCheck = Boolean(opts.skipStringPresenceCheck);
3919
+ const stringPresenceAvailable = skipStringPresenceCheck ? false : host.isStringPresenceAvailable();
3920
+ if (!skipStringPresenceCheck && !stringPresenceAvailable && eff.stringPresence !== "off") {
3921
+ emitCleanupMessage(host, {
3922
+ level: "warn",
3923
+ message: "string-presence safety is unavailable \u2014 cleanup uses static key analysis only."
3924
+ });
3925
+ }
3926
+ emitCleanupMessage(host, {
3927
+ level: "info",
3928
+ message: `${String(allKeyPaths.size)} key path(s) in source JSON \xB7 ${String(candidates.length)} unused candidate(s) after preserve / reference rules`,
3929
+ data: { keyPaths: allKeyPaths.size, candidates: candidates.length }
3930
+ });
3931
+ if (candidates.length > 0) {
3932
+ const affected = listCleanupSourceSegmentsForKeys(ctx, candidates);
3933
+ if (affected.length > 0) {
3934
+ emitCleanupMessage(host, {
3935
+ level: "info",
3936
+ message: `unused key path(s) only in: ${affected.map((s) => s.relativePath).join(", ")}`,
3937
+ data: { segmentCount: affected.length }
3938
+ });
3939
+ }
3940
+ }
3941
+ if (dynamicSites.length > 0) {
3942
+ emitCleanupMessage(host, {
3943
+ level: "warn",
3944
+ message: `${String(dynamicSites.length)} translation call(s) use a non-literal key \u2014 cleanup literal-key inference may miss usage; tighten \`reference\` uncertain-key rules or inspect \`i18nprune validate\` / \`locales dynamic\`.`,
3945
+ data: { dynamicKeySites: dynamicSites.length }
3946
+ });
3947
+ }
3948
+ const stringPresence = resolveCleanupKeysWithStringPresencePolicy({
3949
+ candidates,
3950
+ leaves,
3951
+ stringPresence: eff.stringPresence,
3952
+ stringPresenceMaxHitsPerKey: eff.stringPresenceMaxHitsPerKey,
3953
+ skipStringPresenceCheck,
3954
+ stringPresenceAvailable,
3955
+ hasStringPresence: host.hasStringPresence,
3956
+ getStringPresenceLocations: host.getStringPresenceLocations,
3957
+ shouldRunStringPresenceForKey: host.shouldRunStringPresenceForKey
3958
+ });
3959
+ for (const ev of stringPresence.evidence) {
3960
+ const hint = ev.locations.join(", ");
3961
+ emitCleanupMessage(host, {
3962
+ level: "detail",
3963
+ message: ev.kind === "guard_skipped" ? `cleanup: skipping key (probe text still in src \u2014 not proof of static key usage): ${ev.key}${hint ? ` \u2014 e.g. ${hint}` : ""}` : `cleanup: probe text in src (removal still allowed \u2014 reference.stringPresence=warn): ${ev.key}${hint ? ` \u2014 ${hint}` : ""}`,
3964
+ data: { key: ev.key, kind: ev.kind }
3965
+ });
3966
+ }
3967
+ const safeToRemove = stringPresence.safeToRemove;
3968
+ const writePlan = createCleanupSourceWritePlan(ctx, safeToRemove);
3969
+ const issues = [
3970
+ ...issuesFromDynamicScanCount(dynamicSites.length),
3971
+ ...issuesFromCleanupUncertainExcluded(excludedUncertain),
3972
+ ...issuesFromCleanupStringPresenceUnavailable({ skipStringPresenceCheck, stringPresenceAvailable })
3973
+ ];
3974
+ const payload = {
3975
+ wouldRemove: safeToRemove.length,
3976
+ keys: safeToRemove,
3977
+ dynamicKeySites: dynamicSites.length,
3978
+ uncertainPrefixes: refCtx.uncertainPrefixes
3979
+ };
3980
+ if (safeToRemove.length === 0) {
3981
+ emitCleanupMessage(host, { level: "info", message: "nothing to remove (no unused keys after filters)." });
3982
+ }
3983
+ return {
3984
+ payload,
3985
+ issues,
3986
+ writePlan,
3987
+ sourceLeaves: leaves,
3988
+ allKeyPathCount: allKeyPaths.size,
3989
+ candidateKeys: candidates,
3990
+ safeToRemove,
3991
+ excludedUncertain,
3992
+ dynamicSites,
3993
+ keyObservationsCount: analysis.keyObservations.length,
3994
+ stringPresenceAvailable,
3995
+ stringPresenceEvidence: stringPresence.evidence
3996
+ };
3997
+ }
3998
+
3999
+ export { applyCleanupKeysToLocaleJson, computeCleanupCandidateKeys, createCleanupSourceWritePlan, emitCleanupAbortMessage, emitCleanupAskIgnoredMessage, emitCleanupWriteDone, emitCleanupWriteIntro, listCleanupSourceSegmentPaths, listCleanupSourceSegmentsForKeys, pathUnderRoot, readCleanupSourceLeaves, resolveCleanupKeysWithStringPresencePolicy, runCleanup, writeCleanupPlan };