@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
package/dist/sync.js ADDED
@@ -0,0 +1,4971 @@
1
+ // src/shared/json/clone.ts
2
+ function deepClone(x) {
3
+ if (x === null || typeof x !== "object") return x;
4
+ if (Array.isArray(x)) return x.map((e) => deepClone(e));
5
+ const o = {};
6
+ for (const k of Object.keys(x)) {
7
+ o[k] = deepClone(x[k]);
8
+ }
9
+ return o;
10
+ }
11
+
12
+ // src/shared/json/path.ts
13
+ function splitPath(pathStr) {
14
+ const parts = [];
15
+ const re = /[^.[\]]+|\[\d+\]/g;
16
+ let m;
17
+ const s = pathStr.trim();
18
+ while ((m = re.exec(s)) !== null) {
19
+ const tok = m[0];
20
+ if (tok.startsWith("[") && tok.endsWith("]")) {
21
+ parts.push(Number.parseInt(tok.slice(1, -1), 10));
22
+ } else {
23
+ parts.push(tok);
24
+ }
25
+ }
26
+ return parts;
27
+ }
28
+ function isPlainObject(x) {
29
+ return typeof x === "object" && x !== null && !Array.isArray(x);
30
+ }
31
+ function getAtPath(root, pathStr) {
32
+ let cur = root;
33
+ for (const seg of splitPath(pathStr)) {
34
+ if (cur === null || cur === void 0) return void 0;
35
+ if (typeof seg === "number") {
36
+ if (!Array.isArray(cur)) return void 0;
37
+ cur = cur[seg];
38
+ } else {
39
+ if (!isPlainObject(cur)) return void 0;
40
+ cur = cur[seg];
41
+ }
42
+ }
43
+ return cur;
44
+ }
45
+ function setAtPath(root, pathStr, value) {
46
+ const segs = splitPath(pathStr);
47
+ if (segs.length === 0) return root;
48
+ const clone = deepClone(root);
49
+ let cur = clone;
50
+ for (let i = 0; i < segs.length - 1; i += 1) {
51
+ const seg = segs[i];
52
+ const next = segs[i + 1];
53
+ if (typeof seg === "number") {
54
+ if (!Array.isArray(cur)) return clone;
55
+ ensureArraySlot(cur, seg, typeof next === "number" ? [] : {});
56
+ cur = cur[seg];
57
+ } else {
58
+ if (!isPlainObject(cur)) return clone;
59
+ if (!(seg in cur)) {
60
+ cur[seg] = typeof next === "number" ? [] : {};
61
+ }
62
+ cur = cur[seg];
63
+ }
64
+ }
65
+ const last = segs[segs.length - 1];
66
+ if (typeof last === "number") {
67
+ if (!Array.isArray(cur)) return clone;
68
+ ensureArraySlot(cur, last, null);
69
+ cur[last] = value;
70
+ } else {
71
+ if (!isPlainObject(cur)) return clone;
72
+ cur[last] = value;
73
+ }
74
+ return clone;
75
+ }
76
+ function ensureArraySlot(arr, index, fill) {
77
+ while (arr.length <= index) {
78
+ arr.push(fill);
79
+ }
80
+ }
81
+ function deleteAtPath(root, pathStr) {
82
+ const segs = splitPath(pathStr);
83
+ if (segs.length === 0) return root;
84
+ const clone = deepClone(root);
85
+ let cur = clone;
86
+ for (let i = 0; i < segs.length - 1; i += 1) {
87
+ const seg = segs[i];
88
+ if (typeof seg === "number") {
89
+ if (!Array.isArray(cur)) return clone;
90
+ cur = cur[seg];
91
+ } else {
92
+ if (!isPlainObject(cur)) return clone;
93
+ cur = cur[seg];
94
+ }
95
+ }
96
+ const last = segs[segs.length - 1];
97
+ if (typeof last === "number") {
98
+ if (Array.isArray(cur)) cur.splice(last, 1);
99
+ } else if (isPlainObject(cur)) {
100
+ delete cur[last];
101
+ }
102
+ return clone;
103
+ }
104
+
105
+ // src/shared/reference/paths.ts
106
+ function pathUnderUncertainPrefix(keyPath, prefix) {
107
+ if (keyPath === prefix) return true;
108
+ if (keyPath.startsWith(`${prefix}.`)) return true;
109
+ if (keyPath.startsWith(`${prefix}[`)) return true;
110
+ return false;
111
+ }
112
+ function pathUnderAnyUncertainPrefix(keyPath, prefixes) {
113
+ for (const p of prefixes) {
114
+ if (pathUnderUncertainPrefix(keyPath, p)) return true;
115
+ }
116
+ return false;
117
+ }
118
+
119
+ // src/shared/json/merge.ts
120
+ function isPlainObject2(x) {
121
+ return typeof x === "object" && x !== null && !Array.isArray(x);
122
+ }
123
+ var OMIT = /* @__PURE__ */ Symbol("i18nprune.merge.omit");
124
+ function mergeToTemplateShape(template, target, preserve, options) {
125
+ return mergeWalk(
126
+ template,
127
+ target,
128
+ preserve,
129
+ "",
130
+ options?.uncertainKeepPrefixes,
131
+ options?.skipFillPaths === void 0 ? void 0 : new Set(options.skipFillPaths),
132
+ options?.forceFillPaths === void 0 ? void 0 : new Set(options.forceFillPaths)
133
+ );
134
+ }
135
+ function mergeWalk(template, target, preserve, path, uncertainKeepPrefixes, skipFillPaths, forceFillPaths) {
136
+ if (typeof template === "string") {
137
+ if (skipFillPaths?.has(path)) return OMIT;
138
+ if (forceFillPaths?.has(path)) return template;
139
+ if (typeof target === "string") return target;
140
+ if (isPlainObject2(target) && typeof target.value === "string") {
141
+ return deepClone(target);
142
+ }
143
+ return template;
144
+ }
145
+ if (Array.isArray(template)) {
146
+ if (!Array.isArray(target)) target = [];
147
+ const out = [];
148
+ const tArr = target;
149
+ for (let i = 0; i < template.length; i += 1) {
150
+ const seg = path ? `${path}[${String(i)}]` : `[${String(i)}]`;
151
+ const next = mergeWalk(template[i], tArr[i], preserve, seg, uncertainKeepPrefixes, skipFillPaths, forceFillPaths);
152
+ if (next !== OMIT) out.push(next);
153
+ }
154
+ return out;
155
+ }
156
+ if (isPlainObject2(template)) {
157
+ const tObj = isPlainObject2(target) ? target : {};
158
+ const out = {};
159
+ for (const k of Object.keys(template)) {
160
+ const p = path ? `${path}.${k}` : k;
161
+ const next = mergeWalk(template[k], tObj[k], preserve, p, uncertainKeepPrefixes, skipFillPaths, forceFillPaths);
162
+ if (next !== OMIT) out[k] = next;
163
+ }
164
+ if (uncertainKeepPrefixes?.length) {
165
+ for (const k of Object.keys(tObj)) {
166
+ if (k in template) continue;
167
+ const p = path ? `${path}.${k}` : k;
168
+ if (pathUnderAnyUncertainPrefix(p, uncertainKeepPrefixes)) {
169
+ out[k] = deepClone(tObj[k]);
170
+ }
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+ return deepClone(template);
176
+ }
177
+
178
+ // src/shared/json/prune.ts
179
+ function isPlainObject3(x) {
180
+ return typeof x === "object" && x !== null && !Array.isArray(x);
181
+ }
182
+ function pruneToTemplateShape(template, target, options) {
183
+ return pruneWalk(template, target, "", options?.uncertainKeepPrefixes);
184
+ }
185
+ function pruneWalk(template, target, path, uncertainKeepPrefixes) {
186
+ if (template === null || typeof template !== "object") {
187
+ return target;
188
+ }
189
+ if (Array.isArray(template)) {
190
+ if (!Array.isArray(target)) return [];
191
+ const len = Math.min(template.length, target.length);
192
+ const out2 = [];
193
+ for (let i = 0; i < len; i += 1) {
194
+ const seg = path ? `${path}[${String(i)}]` : `[${String(i)}]`;
195
+ out2.push(pruneWalk(template[i], target[i], seg, uncertainKeepPrefixes));
196
+ }
197
+ return out2;
198
+ }
199
+ if (!isPlainObject3(template)) return target;
200
+ if (!isPlainObject3(target)) return {};
201
+ const out = {};
202
+ for (const k of Object.keys(template)) {
203
+ if (k in target) {
204
+ const p = path ? `${path}.${k}` : k;
205
+ out[k] = pruneWalk(template[k], target[k], p, uncertainKeepPrefixes);
206
+ }
207
+ }
208
+ if (uncertainKeepPrefixes?.length) {
209
+ for (const k of Object.keys(target)) {
210
+ if (k in template) continue;
211
+ const p = path ? `${path}.${k}` : k;
212
+ if (pathUnderAnyUncertainPrefix(p, uncertainKeepPrefixes)) {
213
+ out[k] = deepClone(target[k]);
214
+ }
215
+ }
216
+ }
217
+ return out;
218
+ }
219
+
220
+ // src/shared/json/sortKeys.ts
221
+ function isPlainObject4(x) {
222
+ return typeof x === "object" && x !== null && !Array.isArray(x);
223
+ }
224
+ function sortJsonObjectKeysAsc(value) {
225
+ if (Array.isArray(value)) {
226
+ return value.map((item) => sortJsonObjectKeysAsc(item));
227
+ }
228
+ if (!isPlainObject4(value)) return value;
229
+ const sorted = {};
230
+ for (const key of Object.keys(value).sort()) {
231
+ sorted[key] = sortJsonObjectKeysAsc(value[key]);
232
+ }
233
+ return sorted;
234
+ }
235
+ function localeJsonContentEquals(a, b) {
236
+ return JSON.stringify(sortJsonObjectKeysAsc(a)) === JSON.stringify(sortJsonObjectKeysAsc(b));
237
+ }
238
+
239
+ // src/sync/apply.ts
240
+ function computeSyncedLocaleJson(template, cur, preserve, mergeOpts) {
241
+ let next = mergeToTemplateShape(template, cur, preserve, mergeOpts);
242
+ next = pruneToTemplateShape(template, next, mergeOpts);
243
+ const wouldChange = !localeJsonContentEquals(cur, next);
244
+ return { next, wouldChange };
245
+ }
246
+ function isPlainObject5(x) {
247
+ return typeof x === "object" && x !== null && !Array.isArray(x);
248
+ }
249
+ function stripStructuredLeafMetadata(root) {
250
+ if (Array.isArray(root)) return root.map((v) => stripStructuredLeafMetadata(v));
251
+ if (!isPlainObject5(root)) return root;
252
+ if (typeof root.value === "string") return root.value;
253
+ const out = {};
254
+ for (const [k, v] of Object.entries(root)) {
255
+ out[k] = stripStructuredLeafMetadata(v);
256
+ }
257
+ return out;
258
+ }
259
+
260
+ // src/shared/json/localeLeafPath.ts
261
+ function isPlainObject6(x) {
262
+ return typeof x === "object" && x !== null && !Array.isArray(x);
263
+ }
264
+ function isLiteralDottedObjectKey(key) {
265
+ return key.includes(".");
266
+ }
267
+ function getLocaleLeafAtPath(root, pathStr) {
268
+ if (isPlainObject6(root) && Object.prototype.hasOwnProperty.call(root, pathStr)) {
269
+ return root[pathStr];
270
+ }
271
+ return getAtPath(root, pathStr);
272
+ }
273
+ function deleteLocaleLeafAtPath(root, pathStr) {
274
+ if (isPlainObject6(root) && Object.prototype.hasOwnProperty.call(root, pathStr)) {
275
+ const clone = deepClone(root);
276
+ delete clone[pathStr];
277
+ return clone;
278
+ }
279
+ return deleteAtPath(root, pathStr);
280
+ }
281
+ function stripLiteralDottedKeysAtLevel(root) {
282
+ if (!isPlainObject6(root)) {
283
+ if (Array.isArray(root)) return root.map((item) => stripLiteralDottedKeysAtLevel(item));
284
+ return root;
285
+ }
286
+ const out = {};
287
+ for (const [key, value] of Object.entries(root)) {
288
+ if (isLiteralDottedObjectKey(key)) continue;
289
+ out[key] = stripLiteralDottedKeysAtLevel(value);
290
+ }
291
+ return out;
292
+ }
293
+ function normalizeLocaleDocumentToNestedCanonical(root, knownLeafPaths) {
294
+ if (Array.isArray(root)) {
295
+ return root.map((item) => normalizeLocaleDocumentToNestedCanonical(item, knownLeafPaths));
296
+ }
297
+ if (!isPlainObject6(root)) return root;
298
+ const preservedNested = {};
299
+ const dottedAssignments = [];
300
+ for (const [key, value] of Object.entries(root)) {
301
+ if (isLiteralDottedObjectKey(key)) {
302
+ dottedAssignments.push({ path: key, value });
303
+ } else {
304
+ preservedNested[key] = normalizeLocaleDocumentToNestedCanonical(value, knownLeafPaths);
305
+ }
306
+ }
307
+ let result = preservedNested;
308
+ for (const { path, value } of dottedAssignments) {
309
+ result = setAtPath(result, path, deepClone(value));
310
+ }
311
+ if (knownLeafPaths) {
312
+ for (const leafPath of knownLeafPaths) {
313
+ if (isLiteralDottedObjectKey(leafPath) && isPlainObject6(result) && Object.prototype.hasOwnProperty.call(result, leafPath)) {
314
+ result = deleteLocaleLeafAtPath(result, leafPath);
315
+ }
316
+ }
317
+ }
318
+ return stripLiteralDottedKeysAtLevel(result);
319
+ }
320
+
321
+ // src/shared/locales/leaves/mode/applyModeHelpers.ts
322
+ function isPlainObjectForLocaleLeaves(x) {
323
+ return typeof x === "object" && x !== null && !Array.isArray(x);
324
+ }
325
+ function bump(m, key) {
326
+ m[key] = (m[key] ?? 0) + 1;
327
+ }
328
+ function initReasonMap() {
329
+ return {
330
+ legacy_string_promoted: 0,
331
+ non_object_replaced: 0,
332
+ missing_value: 0,
333
+ invalid_status: 0,
334
+ invalid_confidence: 0,
335
+ invalid_needs_review: 0,
336
+ invalid_needs_translation_again: 0,
337
+ invalid_source: 0,
338
+ canonical_metadata_materialized: 0
339
+ };
340
+ }
341
+ var MATERIALIZED_STRUCTURED_KEYS = [
342
+ "value",
343
+ "status",
344
+ "confidence",
345
+ "needsReview",
346
+ "source",
347
+ "needsTranslationAgain"
348
+ ];
349
+ function shouldMaterializeCanonicalStructuredFields(cur) {
350
+ for (const k of MATERIALIZED_STRUCTURED_KEYS) {
351
+ if (!(k in cur)) return true;
352
+ }
353
+ return false;
354
+ }
355
+ function classifyLeafRuntimeKind(cur) {
356
+ if (typeof cur === "undefined") return "missing";
357
+ if (typeof cur === "string") return "legacy_string";
358
+ if (!isPlainObjectForLocaleLeaves(cur)) return "other";
359
+ if (typeof cur.value === "string") {
360
+ const validStatus = cur.status === void 0 || typeof cur.status === "string" && cur.status.trim().length > 0;
361
+ const validConfidence = cur.confidence === void 0 || cur.confidence === null || typeof cur.confidence === "number" && Number.isFinite(cur.confidence);
362
+ const validNeedsReview = cur.needsReview === void 0 || typeof cur.needsReview === "boolean";
363
+ const validNeedsTranslationAgain = cur.needsTranslationAgain === void 0 || typeof cur.needsTranslationAgain === "boolean";
364
+ const validSource = cur.source === void 0 || typeof cur.source === "string" && cur.source.trim().length > 0;
365
+ return validStatus && validConfidence && validNeedsReview && validNeedsTranslationAgain && validSource ? "structured_valid" : "structured_corrupt";
366
+ }
367
+ return "structured_corrupt";
368
+ }
369
+ function normalizeStructuredLeaf(cur, sourceValue) {
370
+ const roundConfidence = (v) => Math.round(Math.max(0, Math.min(1, v)) * 100) / 100;
371
+ const reasons = [];
372
+ if (typeof cur === "string") {
373
+ reasons.push("legacy_string_promoted");
374
+ return {
375
+ leaf: {
376
+ value: cur,
377
+ status: "translated",
378
+ confidence: null,
379
+ needsReview: true,
380
+ needsTranslationAgain: false,
381
+ source: "manual"
382
+ },
383
+ changed: true,
384
+ reasons
385
+ };
386
+ }
387
+ if (!isPlainObjectForLocaleLeaves(cur)) {
388
+ reasons.push("non_object_replaced");
389
+ return {
390
+ leaf: {
391
+ value: sourceValue,
392
+ status: "pending",
393
+ confidence: null,
394
+ needsReview: true,
395
+ needsTranslationAgain: true,
396
+ source: "sync"
397
+ },
398
+ changed: true,
399
+ reasons
400
+ };
401
+ }
402
+ let changed = false;
403
+ const out = { value: sourceValue };
404
+ if (typeof cur.value === "string") out.value = cur.value;
405
+ else {
406
+ changed = true;
407
+ reasons.push("missing_value");
408
+ }
409
+ if (typeof cur.status === "string" && cur.status.trim().length > 0) {
410
+ out.status = cur.status;
411
+ } else if (cur.status === void 0) {
412
+ out.status = "translated";
413
+ } else {
414
+ out.status = "translated";
415
+ changed = true;
416
+ reasons.push("invalid_status");
417
+ }
418
+ if (typeof cur.confidence === "number" && Number.isFinite(cur.confidence)) out.confidence = roundConfidence(cur.confidence);
419
+ else if (cur.confidence === null || cur.confidence === void 0) out.confidence = null;
420
+ else {
421
+ out.confidence = null;
422
+ changed = true;
423
+ reasons.push("invalid_confidence");
424
+ }
425
+ if (typeof cur.needsReview === "boolean") {
426
+ out.needsReview = cur.needsReview;
427
+ } else if (cur.needsReview === void 0) {
428
+ out.needsReview = false;
429
+ } else {
430
+ out.needsReview = false;
431
+ changed = true;
432
+ reasons.push("invalid_needs_review");
433
+ }
434
+ if (typeof cur.source === "string" && cur.source.trim().length > 0) {
435
+ out.source = cur.source;
436
+ } else if (cur.source === void 0) {
437
+ out.source = "manual";
438
+ } else {
439
+ out.source = "manual";
440
+ changed = true;
441
+ reasons.push("invalid_source");
442
+ }
443
+ if (typeof cur.needsTranslationAgain === "boolean") out.needsTranslationAgain = cur.needsTranslationAgain;
444
+ else if ("needsTranslationAgain" in cur) {
445
+ out.needsTranslationAgain = false;
446
+ changed = true;
447
+ reasons.push("invalid_needs_translation_again");
448
+ } else {
449
+ out.needsTranslationAgain = false;
450
+ }
451
+ const materialized = isPlainObjectForLocaleLeaves(cur) && typeof cur.value === "string" && shouldMaterializeCanonicalStructuredFields(cur);
452
+ if (materialized && !changed) reasons.push("canonical_metadata_materialized");
453
+ const effectiveChanged = changed || materialized;
454
+ if (changed) {
455
+ out.needsReview = true;
456
+ }
457
+ return { leaf: out, changed: effectiveChanged, reasons };
458
+ }
459
+
460
+ // src/shared/locales/leaves/mode/applyLocaleLeafMode.ts
461
+ function applyLocaleLeafMode(input) {
462
+ const changes = [];
463
+ const leafDecisions = [];
464
+ let next = input.localeJson;
465
+ let unchangedLeaves = 0;
466
+ let structuredLeavesWritten = 0;
467
+ let promotedLegacyLeaves = 0;
468
+ let repairedCorruptLeaves = 0;
469
+ let strippedStructuredLeaves = 0;
470
+ let missingPathsHydratedFromSource = 0;
471
+ const byReason = initReasonMap();
472
+ const sampleLimit = input.sampleLimit ?? 40;
473
+ for (const [leafPath, sourceValue] of input.sourceMap.entries()) {
474
+ const cur = getLocaleLeafAtPath(next, leafPath);
475
+ const beforeKind = classifyLeafRuntimeKind(cur);
476
+ const beforeValue = cur;
477
+ if (input.mode === "legacy_string") {
478
+ const nextValue = typeof cur === "string" ? cur : isPlainObjectForLocaleLeaves(cur) && typeof cur.value === "string" ? cur.value : sourceValue;
479
+ let action2 = "unchanged";
480
+ const reasons = [];
481
+ if (typeof cur === "undefined") {
482
+ missingPathsHydratedFromSource += 1;
483
+ action2 = "hydrated_missing";
484
+ }
485
+ if (isPlainObjectForLocaleLeaves(cur) && typeof cur.value === "string") {
486
+ strippedStructuredLeaves += 1;
487
+ action2 = "stripped_structured";
488
+ }
489
+ if (cur === nextValue) unchangedLeaves += 1;
490
+ else next = setAtPath(next, leafPath, nextValue);
491
+ const afterValue2 = getLocaleLeafAtPath(next, leafPath);
492
+ const afterKind2 = classifyLeafRuntimeKind(afterValue2);
493
+ leafDecisions.push({
494
+ path: leafPath,
495
+ sourceValue,
496
+ beforeKind,
497
+ afterKind: afterKind2,
498
+ action: action2,
499
+ reasons,
500
+ beforeValue,
501
+ afterValue: afterValue2
502
+ });
503
+ continue;
504
+ }
505
+ const normalized = normalizeStructuredLeaf(cur, sourceValue);
506
+ let action = "unchanged";
507
+ if (normalized.changed) {
508
+ next = setAtPath(next, leafPath, normalized.leaf);
509
+ structuredLeavesWritten += 1;
510
+ for (const reason of normalized.reasons) {
511
+ bump(byReason, reason);
512
+ if (changes.length < sampleLimit) changes.push({ path: leafPath, reason });
513
+ }
514
+ const hasLegacy = normalized.reasons.includes("legacy_string_promoted");
515
+ const hasCorruptRepair = normalized.reasons.some(
516
+ (r) => r !== "legacy_string_promoted" && r !== "canonical_metadata_materialized"
517
+ );
518
+ if (hasLegacy) {
519
+ promotedLegacyLeaves += 1;
520
+ action = "promoted_legacy";
521
+ }
522
+ if (hasCorruptRepair) {
523
+ repairedCorruptLeaves += 1;
524
+ if (!hasLegacy) action = "repaired_corrupt";
525
+ }
526
+ } else {
527
+ unchangedLeaves += 1;
528
+ }
529
+ if (typeof cur === "undefined") missingPathsHydratedFromSource += 1;
530
+ const afterValue = getLocaleLeafAtPath(next, leafPath);
531
+ const afterKind = classifyLeafRuntimeKind(afterValue);
532
+ leafDecisions.push({
533
+ path: leafPath,
534
+ sourceValue,
535
+ beforeKind,
536
+ afterKind,
537
+ action,
538
+ reasons: normalized.reasons,
539
+ beforeValue,
540
+ afterValue
541
+ });
542
+ }
543
+ const leafPaths = [...input.sourceMap.keys()];
544
+ next = normalizeLocaleDocumentToNestedCanonical(next, leafPaths);
545
+ return {
546
+ next,
547
+ report: {
548
+ mode: input.mode,
549
+ totalSourceLeafPaths: input.sourceMap.size,
550
+ unchangedLeaves,
551
+ structuredLeavesWritten,
552
+ promotedLegacyLeaves,
553
+ repairedCorruptLeaves,
554
+ strippedStructuredLeaves,
555
+ missingPathsHydratedFromSource,
556
+ byReason,
557
+ changedPathsSample: changes,
558
+ leafDecisions
559
+ }
560
+ };
561
+ }
562
+
563
+ // src/shared/locales/leaves/mode/modeResolve.ts
564
+ function metadataModeEnabledFromConfig(mode) {
565
+ return mode === "structured";
566
+ }
567
+ function resolveLocaleLeafMode(input) {
568
+ if (input.stripMetadataFlag === true) {
569
+ return {
570
+ mode: "legacy_string",
571
+ conflict: input.metadataFlag === true,
572
+ reason: "strip_precedence"
573
+ };
574
+ }
575
+ if (input.metadataFlag === true) return { mode: "structured", conflict: false, reason: "explicit_metadata" };
576
+ if (metadataModeEnabledFromConfig(input.configMode)) {
577
+ return { mode: "structured", conflict: false, reason: "config_structured" };
578
+ }
579
+ return { mode: "legacy_string", conflict: false, reason: "default_legacy" };
580
+ }
581
+
582
+ // src/shared/locales/enumerate/parseSegmentLocale.ts
583
+ function localeCodeForSegment(structure, path, segment) {
584
+ if (structure === "locale_file") {
585
+ if (segment.relativePath.includes("/")) return null;
586
+ return path.basename(segment.absolutePath, ".json");
587
+ }
588
+ if (structure === "locale_per_dir") {
589
+ const slash = segment.relativePath.indexOf("/");
590
+ if (slash < 0) return null;
591
+ const locale = segment.relativePath.slice(0, slash);
592
+ return locale.length > 0 ? locale : null;
593
+ }
594
+ if (structure === "feature_bundle") {
595
+ return path.basename(segment.absolutePath, ".json");
596
+ }
597
+ return null;
598
+ }
599
+
600
+ // src/shared/locales/leaves/segmentSource/localeSegmentSourceForFile.ts
601
+ function localeSegmentSourceForFile(input) {
602
+ const { path, absoluteFile, localesDir, structure } = input;
603
+ let relativePath = path.relative(localesDir, absoluteFile);
604
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
605
+ relativePath = path.basename(absoluteFile);
606
+ }
607
+ relativePath = relativePath.replace(/\\/g, "/");
608
+ const locale = localeCodeForSegment(structure, path, { absolutePath: absoluteFile, relativePath });
609
+ if (locale === null) return null;
610
+ return { file: absoluteFile, locale, relativePath };
611
+ }
612
+
613
+ // src/shared/locales/leaves/walk/translationSurfaceWalk.ts
614
+ function isPlainObject7(x) {
615
+ return typeof x === "object" && x !== null && !Array.isArray(x);
616
+ }
617
+ function isStructuredLocaleLeafNode(x) {
618
+ return isPlainObject7(x) && typeof x.value === "string";
619
+ }
620
+ function readOptionalStatus(o) {
621
+ const s = o.status;
622
+ if (typeof s !== "string" || !s.trim()) return void 0;
623
+ return s;
624
+ }
625
+ function readOptionalLeafSource(o) {
626
+ const s = o.source;
627
+ if (typeof s !== "string" || !s.trim()) return void 0;
628
+ return s;
629
+ }
630
+ function readConfidence(o) {
631
+ const c = o.confidence;
632
+ if (c === null || c === void 0) return null;
633
+ if (typeof c !== "number" || !Number.isFinite(c)) return null;
634
+ const clamped = Math.max(0, Math.min(1, c));
635
+ return Math.round(clamped * 100) / 100;
636
+ }
637
+ function readNeedsReview(o) {
638
+ if (!("needsReview" in o)) return null;
639
+ return typeof o.needsReview === "boolean" ? o.needsReview : null;
640
+ }
641
+ function readNeedsTranslationAgain(o) {
642
+ if (!("needsTranslationAgain" in o)) return null;
643
+ return typeof o.needsTranslationAgain === "boolean" ? o.needsTranslationAgain : null;
644
+ }
645
+ function isCompleteStructuredLocaleLeafMeta(node) {
646
+ if (!isStructuredLocaleLeafNode(node)) return false;
647
+ const o = node;
648
+ if (typeof o.status !== "string" || !o.status.trim()) return false;
649
+ if (!(o.confidence === null || typeof o.confidence === "number" && Number.isFinite(o.confidence))) return false;
650
+ if (typeof o.needsReview !== "boolean") return false;
651
+ if (typeof o.needsTranslationAgain !== "boolean") return false;
652
+ if (typeof o.source !== "string" || !o.source.trim()) return false;
653
+ return true;
654
+ }
655
+ function pushStructuredRow(out, prefix, root, fileOrigin) {
656
+ out.push({
657
+ path: prefix,
658
+ value: root.value,
659
+ shape: "structured",
660
+ status: readOptionalStatus(root),
661
+ confidence: readConfidence(root),
662
+ needsReview: readNeedsReview(root),
663
+ needsTranslationAgain: readNeedsTranslationAgain(root),
664
+ source: readOptionalLeafSource(root),
665
+ structuredMetaComplete: isCompleteStructuredLocaleLeafMeta(root),
666
+ ...fileOrigin ? { fileOrigin } : {}
667
+ });
668
+ }
669
+ function collectTranslationSurfaceLeaves(root, prefix = "", out = [], fileOrigin) {
670
+ if (typeof root === "string") {
671
+ if (prefix) {
672
+ out.push({
673
+ path: prefix,
674
+ value: root,
675
+ shape: "legacy_string",
676
+ confidence: null,
677
+ needsReview: null,
678
+ ...fileOrigin ? { fileOrigin } : {}
679
+ });
680
+ }
681
+ return out;
682
+ }
683
+ if (isStructuredLocaleLeafNode(root)) {
684
+ if (prefix) pushStructuredRow(out, prefix, root, fileOrigin);
685
+ return out;
686
+ }
687
+ if (Array.isArray(root)) {
688
+ root.forEach((item, i) => {
689
+ const p = prefix ? `${prefix}[${i}]` : `[${i}]`;
690
+ collectTranslationSurfaceLeaves(item, p, out, fileOrigin);
691
+ });
692
+ return out;
693
+ }
694
+ if (isPlainObject7(root)) {
695
+ for (const k of Object.keys(root)) {
696
+ const p = prefix ? `${prefix}.${k}` : k;
697
+ collectTranslationSurfaceLeaves(root[k], p, out, fileOrigin);
698
+ }
699
+ }
700
+ return out;
701
+ }
702
+
703
+ // src/extractor/bindings/ident.ts
704
+ var IMPORT_BINDING_IDENT_PATTERN = String.raw`[$_\p{ID_Start}][$_\p{ID_Continue}]*`;
705
+
706
+ // src/extractor/bindings/expand.ts
707
+ var RX_SAFE_BRACKET_METHOD = new RegExp(String.raw`^` + IMPORT_BINDING_IDENT_PATTERN + String.raw`$`, "u");
708
+ function isRuntimeBinding(b) {
709
+ return b.isTypeOnly !== true;
710
+ }
711
+ function isSafeBracketMethodKey(method) {
712
+ if (method.length === 0) return false;
713
+ if (/['"\\\s\n\r]/.test(method)) return false;
714
+ return RX_SAFE_BRACKET_METHOD.test(method);
715
+ }
716
+ function addMemberCallVariants(effective, objectLocal, method) {
717
+ effective.add(`${objectLocal}.${method}`);
718
+ effective.add(`${objectLocal}?.${method}`);
719
+ if (isSafeBracketMethodKey(method)) {
720
+ effective.add(`${objectLocal}['${method}']`);
721
+ effective.add(`${objectLocal}?.['${method}']`);
722
+ effective.add(`${objectLocal}["${method}"]`);
723
+ effective.add(`${objectLocal}?.["${method}"]`);
724
+ }
725
+ }
726
+ function methodSuffixesForModuleExpansion(configuredSet, simpleRoots, bindings) {
727
+ const suffixes = new Set(simpleRoots);
728
+ for (const b of bindings) {
729
+ if (!isRuntimeBinding(b) || b.kind !== "named") continue;
730
+ if (configuredSet.has(b.imported) || configuredSet.has(b.local)) {
731
+ suffixes.add(b.imported);
732
+ suffixes.add(b.local);
733
+ }
734
+ }
735
+ return suffixes;
736
+ }
737
+ function expandFunctionsWithBindings(configuredFunctions, bindings) {
738
+ const uniqConfigured = [...new Set(configuredFunctions)];
739
+ const configuredSet = new Set(uniqConfigured);
740
+ const effective = new Set(uniqConfigured);
741
+ const simpleRoots = uniqConfigured.filter((f) => !f.includes("."));
742
+ for (const b of bindings) {
743
+ if (!isRuntimeBinding(b)) continue;
744
+ if (b.kind === "named" && (configuredSet.has(b.imported) || configuredSet.has(b.local))) {
745
+ effective.add(b.local);
746
+ }
747
+ }
748
+ const methodSuffixes = methodSuffixesForModuleExpansion(configuredSet, simpleRoots, bindings);
749
+ for (const method of methodSuffixes) {
750
+ for (const b of bindings) {
751
+ if (!isRuntimeBinding(b)) continue;
752
+ if (b.kind === "module") {
753
+ addMemberCallVariants(effective, b.local, method);
754
+ }
755
+ }
756
+ }
757
+ const discovered = [...effective].filter((f) => !configuredSet.has(f));
758
+ discovered.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
759
+ return [...uniqConfigured, ...discovered];
760
+ }
761
+
762
+ // src/extractor/shared/jslikeTextRanges.ts
763
+ function skipString(text, start, quote) {
764
+ let i = start + 1;
765
+ while (i < text.length) {
766
+ const ch = text[i];
767
+ if (ch === "\\") {
768
+ i += 2;
769
+ continue;
770
+ }
771
+ if (ch === quote) return i + 1;
772
+ i += 1;
773
+ }
774
+ return text.length;
775
+ }
776
+ function skipTemplate(text, start) {
777
+ let i = start + 1;
778
+ while (i < text.length) {
779
+ const ch = text[i];
780
+ if (ch === "\\") {
781
+ i += 2;
782
+ continue;
783
+ }
784
+ if (ch === "`") return i + 1;
785
+ if (ch === "$" && text[i + 1] === "{") {
786
+ i += 2;
787
+ let depth = 1;
788
+ while (i < text.length && depth > 0) {
789
+ const c2 = text[i];
790
+ if (c2 === "{") depth += 1;
791
+ else if (c2 === "}") depth -= 1;
792
+ i += 1;
793
+ }
794
+ continue;
795
+ }
796
+ i += 1;
797
+ }
798
+ return text.length;
799
+ }
800
+ function mergeRanges(ranges) {
801
+ if (ranges.length === 0) return [];
802
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
803
+ const out = [];
804
+ let cur = sorted[0];
805
+ for (let k = 1; k < sorted.length; k += 1) {
806
+ const n = sorted[k];
807
+ if (n.start <= cur.end) {
808
+ cur = { start: cur.start, end: Math.max(cur.end, n.end) };
809
+ } else {
810
+ out.push(cur);
811
+ cur = n;
812
+ }
813
+ }
814
+ out.push(cur);
815
+ return out;
816
+ }
817
+ function commentRangesForJsLikeText(text) {
818
+ const ranges = [];
819
+ let i = 0;
820
+ const len = text.length;
821
+ while (i < len) {
822
+ const c = text[i];
823
+ if (c === "/" && text[i + 1] === "/") {
824
+ const start = i;
825
+ i += 2;
826
+ while (i < len && text[i] !== "\n") i += 1;
827
+ ranges.push({ start, end: i });
828
+ continue;
829
+ }
830
+ if (c === "/" && text[i + 1] === "*") {
831
+ const start = i;
832
+ i += 2;
833
+ while (i + 1 < len) {
834
+ if (text[i] === "*" && text[i + 1] === "/") {
835
+ i += 2;
836
+ ranges.push({ start, end: i });
837
+ break;
838
+ }
839
+ i += 1;
840
+ }
841
+ if (i >= len && start >= 0 && !ranges.some((r) => r.start === start)) {
842
+ ranges.push({ start, end: len });
843
+ }
844
+ continue;
845
+ }
846
+ if (c === "'" || c === '"') {
847
+ i = skipString(text, i, c);
848
+ continue;
849
+ }
850
+ if (c === "`") {
851
+ i = skipTemplate(text, i);
852
+ continue;
853
+ }
854
+ i += 1;
855
+ }
856
+ return mergeRanges(ranges);
857
+ }
858
+ function literalRangesForJsLikeText(text) {
859
+ const ranges = [];
860
+ let i = 0;
861
+ const len = text.length;
862
+ while (i < len) {
863
+ const c = text[i];
864
+ if (c === "/" && text[i + 1] === "/") {
865
+ i += 2;
866
+ while (i < len && text[i] !== "\n") i += 1;
867
+ continue;
868
+ }
869
+ if (c === "/" && text[i + 1] === "*") {
870
+ i += 2;
871
+ while (i + 1 < len) {
872
+ if (text[i] === "*" && text[i + 1] === "/") {
873
+ i += 2;
874
+ break;
875
+ }
876
+ i += 1;
877
+ }
878
+ if (i >= len) break;
879
+ continue;
880
+ }
881
+ if (c === "'" || c === '"') {
882
+ const start = i;
883
+ i = skipString(text, i, c);
884
+ if (i > start + 1) ranges.push({ start: start + 1, end: i - 1 });
885
+ continue;
886
+ }
887
+ if (c === "`") {
888
+ const start = i;
889
+ i = skipTemplate(text, i);
890
+ if (i > start + 1) ranges.push({ start: start + 1, end: i - 1 });
891
+ continue;
892
+ }
893
+ i += 1;
894
+ }
895
+ return mergeRanges(ranges);
896
+ }
897
+ function importBindingScanBlankRanges(text) {
898
+ return mergeRanges([...commentRangesForJsLikeText(text), ...literalRangesForJsLikeText(text)]);
899
+ }
900
+ function offsetInCommentRanges(offset, ranges) {
901
+ for (const r of ranges) {
902
+ if (offset >= r.start && offset < r.end) return true;
903
+ }
904
+ return false;
905
+ }
906
+
907
+ // src/extractor/bindings/imports.ts
908
+ var U = "gu";
909
+ var IMPORT_TYPE_NAMESPACE_RX = new RegExp(
910
+ String.raw`import\s+type\s*\*\s*as\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
911
+ U
912
+ );
913
+ var IMPORT_TYPE_NAMED_RX = new RegExp(
914
+ String.raw`import\s+type\s+\{([^}]*)\}\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
915
+ U
916
+ );
917
+ var IMPORT_TYPE_DEFAULT_RX = new RegExp(
918
+ String.raw`import\s+type\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
919
+ U
920
+ );
921
+ var TS_IMPORT_EQUALS_RX = new RegExp(
922
+ String.raw`import\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*=\s*require\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
923
+ U
924
+ );
925
+ var NAMESPACE_IMPORT_RX = new RegExp(
926
+ String.raw`import\s*\*\s*as\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
927
+ U
928
+ );
929
+ var DEFAULT_AND_NAMED_IMPORT_RX = new RegExp(
930
+ String.raw`import\s+(?!type\b)(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*,\s*\{([^}]*)\}\s+from\s*(['"])(?:(?!\3).|\\.)*\3`,
931
+ U
932
+ );
933
+ var NAMED_IMPORT_ONLY_RX = new RegExp(
934
+ String.raw`import\s+(?:type\s+)?\{([^}]*)\}\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
935
+ U
936
+ );
937
+ var DEFAULT_IMPORT_ONLY_RX = new RegExp(
938
+ String.raw`import\s+(?!type\b)(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+from\s*(['"])(?:(?!\2).|\\.)*\2`,
939
+ U
940
+ );
941
+ var CJS_REQUIRE_DESTRUCT_RX = new RegExp(
942
+ String.raw`(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
943
+ U
944
+ );
945
+ var CJS_REQUIRE_MODULE_RX = new RegExp(
946
+ String.raw`(?:const|let|var)\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*=\s*require\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
947
+ U
948
+ );
949
+ var DYNAMIC_IMPORT_DESTRUCT_RX = new RegExp(
950
+ String.raw`(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:await\s+)?import\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
951
+ U
952
+ );
953
+ var DYNAMIC_IMPORT_MODULE_RX = new RegExp(
954
+ String.raw`(?:const|let|var)\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*=\s*(?:await\s+)?import\s*\(\s*(['"])(?:(?!\2).|\\.)*\2\s*\)`,
955
+ U
956
+ );
957
+ var RX_AS_PAIR = new RegExp(String.raw`^(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s+as\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
958
+ var RX_IDENT_ONLY = new RegExp(String.raw`^(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
959
+ var RX_TYPE_INLINE = new RegExp(String.raw`^type\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
960
+ var RX_DEFAULT_AS_SEGMENT = new RegExp(String.raw`^default\s+as\s+(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
961
+ var RX_CJS_RENAME = new RegExp(String.raw`^(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)\s*:\s*(` + IMPORT_BINDING_IDENT_PATTERN + String.raw`)$`, "u");
962
+ function isTypeOnlyImportStatement(statement) {
963
+ return /^import\s+type\s+/.test(statement.trimStart());
964
+ }
965
+ function blankScanTextForImportBindings(text) {
966
+ const ranges = importBindingScanBlankRanges(text);
967
+ if (ranges.length === 0) return text;
968
+ const chars = [...text];
969
+ for (const r of ranges) {
970
+ for (let i = r.start; i < r.end; i += 1) chars[i] = " ";
971
+ }
972
+ return chars.join("");
973
+ }
974
+ function splitImportSpecifiers(clause) {
975
+ const out = [];
976
+ let depth = 0;
977
+ let cur = "";
978
+ for (let i = 0; i < clause.length; i += 1) {
979
+ const c = clause[i];
980
+ if (c === "{" || c === "[" || c === "(") depth += 1;
981
+ else if (c === "}" || c === "]" || c === ")") depth -= 1;
982
+ else if (c === "," && depth === 0) {
983
+ const t = cur.trim();
984
+ if (t) out.push(t);
985
+ cur = "";
986
+ continue;
987
+ }
988
+ cur += c;
989
+ }
990
+ const tail = cur.trim();
991
+ if (tail) out.push(tail);
992
+ return out;
993
+ }
994
+ function parseEsmDefaultAsLocals(clause) {
995
+ const out = [];
996
+ for (const raw of splitImportSpecifiers(clause)) {
997
+ const seg = raw.trim();
998
+ if (!seg) continue;
999
+ const m = seg.match(RX_DEFAULT_AS_SEGMENT);
1000
+ if (m) out.push(m[1]);
1001
+ }
1002
+ return out;
1003
+ }
1004
+ function parseEsmNamedImportClause(clause) {
1005
+ const pairs = [];
1006
+ for (const raw of splitImportSpecifiers(clause)) {
1007
+ const seg = raw.trim();
1008
+ if (!seg) continue;
1009
+ if (RX_TYPE_INLINE.test(seg)) continue;
1010
+ if (RX_DEFAULT_AS_SEGMENT.test(seg)) continue;
1011
+ const asMatch = seg.match(RX_AS_PAIR);
1012
+ if (asMatch) {
1013
+ pairs.push({ imported: asMatch[1], local: asMatch[2] });
1014
+ continue;
1015
+ }
1016
+ const ident = seg.match(RX_IDENT_ONLY);
1017
+ if (ident) {
1018
+ const name = ident[1];
1019
+ pairs.push({ imported: name, local: name });
1020
+ }
1021
+ }
1022
+ return pairs;
1023
+ }
1024
+ function parseCjsDestructuringClause(clause) {
1025
+ const pairs = [];
1026
+ for (const raw of splitImportSpecifiers(clause)) {
1027
+ const seg = raw.trim();
1028
+ if (!seg) continue;
1029
+ const rename = seg.match(RX_CJS_RENAME);
1030
+ if (rename) {
1031
+ pairs.push({ imported: rename[1], local: rename[2] });
1032
+ continue;
1033
+ }
1034
+ const ident = seg.match(RX_IDENT_ONLY);
1035
+ if (ident) {
1036
+ const name = ident[1];
1037
+ pairs.push({ imported: name, local: name });
1038
+ }
1039
+ }
1040
+ return pairs;
1041
+ }
1042
+ function pushNamed(out, imported, local, source, typeOnly) {
1043
+ out.push(
1044
+ typeOnly === true ? { kind: "named", imported, local, source, isTypeOnly: true } : { kind: "named", imported, local, source }
1045
+ );
1046
+ }
1047
+ function pushModule(out, local, moduleKind, source, typeOnly) {
1048
+ out.push(
1049
+ typeOnly === true ? { kind: "module", local, moduleKind, source, isTypeOnly: true } : { kind: "module", local, moduleKind, source }
1050
+ );
1051
+ }
1052
+ function scanImportBindings(text) {
1053
+ const scanText = blankScanTextForImportBindings(text);
1054
+ const out = [];
1055
+ let m;
1056
+ const typeNsRe = new RegExp(IMPORT_TYPE_NAMESPACE_RX.source, U);
1057
+ while ((m = typeNsRe.exec(scanText)) !== null) {
1058
+ pushModule(out, m[1], "namespace", "esm", true);
1059
+ }
1060
+ const typeNamedRe = new RegExp(IMPORT_TYPE_NAMED_RX.source, U);
1061
+ while ((m = typeNamedRe.exec(scanText)) !== null) {
1062
+ const clause = m[1];
1063
+ for (const pair of parseEsmNamedImportClause(clause)) {
1064
+ pushNamed(out, pair.imported, pair.local, "esm", true);
1065
+ }
1066
+ for (const local of parseEsmDefaultAsLocals(clause)) {
1067
+ pushModule(out, local, "default", "esm", true);
1068
+ }
1069
+ }
1070
+ const typeDefRe = new RegExp(IMPORT_TYPE_DEFAULT_RX.source, U);
1071
+ while ((m = typeDefRe.exec(scanText)) !== null) {
1072
+ pushModule(out, m[1], "default", "esm", true);
1073
+ }
1074
+ const tsEqRe = new RegExp(TS_IMPORT_EQUALS_RX.source, U);
1075
+ while ((m = tsEqRe.exec(scanText)) !== null) {
1076
+ pushModule(out, m[1], "default", "ts_import_equals");
1077
+ }
1078
+ const nsRe = new RegExp(NAMESPACE_IMPORT_RX.source, U);
1079
+ while ((m = nsRe.exec(scanText)) !== null) {
1080
+ const stmt = m[0];
1081
+ if (isTypeOnlyImportStatement(stmt)) continue;
1082
+ pushModule(out, m[1], "namespace", "esm");
1083
+ }
1084
+ const dnRe = new RegExp(DEFAULT_AND_NAMED_IMPORT_RX.source, U);
1085
+ while ((m = dnRe.exec(scanText)) !== null) {
1086
+ const stmt = m[0];
1087
+ if (isTypeOnlyImportStatement(stmt)) continue;
1088
+ pushModule(out, m[1], "default", "esm");
1089
+ const clause = m[2];
1090
+ for (const pair of parseEsmNamedImportClause(clause)) {
1091
+ pushNamed(out, pair.imported, pair.local, "esm");
1092
+ }
1093
+ for (const local of parseEsmDefaultAsLocals(clause)) {
1094
+ pushModule(out, local, "default", "esm");
1095
+ }
1096
+ }
1097
+ const namedRe = new RegExp(NAMED_IMPORT_ONLY_RX.source, U);
1098
+ while ((m = namedRe.exec(scanText)) !== null) {
1099
+ const stmt = m[0];
1100
+ if (isTypeOnlyImportStatement(stmt)) continue;
1101
+ const clause = m[1];
1102
+ for (const pair of parseEsmNamedImportClause(clause)) {
1103
+ pushNamed(out, pair.imported, pair.local, "esm");
1104
+ }
1105
+ for (const local of parseEsmDefaultAsLocals(clause)) {
1106
+ pushModule(out, local, "default", "esm");
1107
+ }
1108
+ }
1109
+ const defRe = new RegExp(DEFAULT_IMPORT_ONLY_RX.source, U);
1110
+ while ((m = defRe.exec(scanText)) !== null) {
1111
+ const stmt = m[0];
1112
+ if (isTypeOnlyImportStatement(stmt)) continue;
1113
+ pushModule(out, m[1], "default", "esm");
1114
+ }
1115
+ const cjsDestRe = new RegExp(CJS_REQUIRE_DESTRUCT_RX.source, U);
1116
+ while ((m = cjsDestRe.exec(scanText)) !== null) {
1117
+ for (const pair of parseCjsDestructuringClause(m[1])) {
1118
+ pushNamed(out, pair.imported, pair.local, "cjs_require");
1119
+ }
1120
+ }
1121
+ const cjsModRe = new RegExp(CJS_REQUIRE_MODULE_RX.source, U);
1122
+ while ((m = cjsModRe.exec(scanText)) !== null) {
1123
+ const clauseStart = m.index;
1124
+ const pre = scanText.slice(Math.max(0, clauseStart - 32), clauseStart + 1);
1125
+ if (/\{\s*$/u.test(pre)) continue;
1126
+ pushModule(out, m[1], "default", "cjs_require");
1127
+ }
1128
+ const dynDestRe = new RegExp(DYNAMIC_IMPORT_DESTRUCT_RX.source, U);
1129
+ while ((m = dynDestRe.exec(scanText)) !== null) {
1130
+ for (const pair of parseCjsDestructuringClause(m[1])) {
1131
+ pushNamed(out, pair.imported, pair.local, "dynamic_import");
1132
+ }
1133
+ }
1134
+ const dynModRe = new RegExp(DYNAMIC_IMPORT_MODULE_RX.source, U);
1135
+ while ((m = dynModRe.exec(scanText)) !== null) {
1136
+ const clauseStart = m.index;
1137
+ const pre = scanText.slice(Math.max(0, clauseStart - 32), clauseStart + 1);
1138
+ if (/\{\s*$/u.test(pre)) continue;
1139
+ pushModule(out, m[1], "default", "dynamic_import");
1140
+ }
1141
+ return dedupeBindings(out);
1142
+ }
1143
+ function dedupeBindings(bindings) {
1144
+ const seen = /* @__PURE__ */ new Set();
1145
+ const out = [];
1146
+ for (const b of bindings) {
1147
+ 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"}`;
1148
+ if (seen.has(key)) continue;
1149
+ seen.add(key);
1150
+ out.push(b);
1151
+ }
1152
+ return out;
1153
+ }
1154
+
1155
+ // src/shared/errors/internal.ts
1156
+ var I18nPruneError = class extends Error {
1157
+ code;
1158
+ issueCode;
1159
+ constructor(message, code, options) {
1160
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
1161
+ this.name = "I18nPruneError";
1162
+ this.code = code;
1163
+ this.issueCode = options?.issueCode;
1164
+ }
1165
+ };
1166
+ var DOCS_ISSUES_PAGE_PATH = "/issues";
1167
+
1168
+ // src/shared/docs/issueAnchors.ts
1169
+ var DOC_ISSUE_PARENT_SEGMENTS = /* @__PURE__ */ new Set([
1170
+ "cli",
1171
+ "cleanup",
1172
+ "config",
1173
+ "context",
1174
+ "doctor",
1175
+ "generate",
1176
+ "io",
1177
+ "languages",
1178
+ "locale",
1179
+ "locales",
1180
+ "missing",
1181
+ "patching",
1182
+ "paths",
1183
+ "project",
1184
+ "quality",
1185
+ "report",
1186
+ "scan",
1187
+ "share",
1188
+ "sync",
1189
+ "translate",
1190
+ "validate"
1191
+ ]);
1192
+ function issueDocHeadingSlug(raw) {
1193
+ return raw.toLowerCase().replace(/[.\s_]+/g, "-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1194
+ }
1195
+ function splitIssueCode(code) {
1196
+ return code.trim().split(".");
1197
+ }
1198
+ function buildIssueCodeDocLinkParts(code) {
1199
+ const trimmed = code.trim();
1200
+ const parts = splitIssueCode(trimmed);
1201
+ let parent = null;
1202
+ if (parts.length >= 3 && parts[0] === "i18nprune") {
1203
+ const p = parts[1];
1204
+ if (DOC_ISSUE_PARENT_SEGMENTS.has(p)) parent = p;
1205
+ }
1206
+ let anchor;
1207
+ if (parent) {
1208
+ anchor = issueDocHeadingSlug(parts.slice(2).join("."));
1209
+ } else {
1210
+ anchor = issueDocHeadingSlug(trimmed.includes(".") ? trimmed.replace(/\./g, "_") : trimmed);
1211
+ }
1212
+ const sitePagePath = parent ? `${DOCS_ISSUES_PAGE_PATH}/${parent}` : DOCS_ISSUES_PAGE_PATH;
1213
+ const repoDocPath = parent ? `issues/${parent}` : "issues";
1214
+ return { parent, anchor, sitePagePath, repoDocPath };
1215
+ }
1216
+ function issueCodeRepoDocPathForIssueCode(code) {
1217
+ return buildIssueCodeDocLinkParts(code).repoDocPath;
1218
+ }
1219
+
1220
+ // src/runtime/helpers/sync/assert.ts
1221
+ function isThenable(value) {
1222
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
1223
+ }
1224
+ function assertSyncPortResult(value, label, at) {
1225
+ if (isThenable(value)) {
1226
+ throw new I18nPruneError(
1227
+ `Synchronous ${label} requires a plain value (got a Promise at ${at})`,
1228
+ "USAGE"
1229
+ );
1230
+ }
1231
+ return value;
1232
+ }
1233
+
1234
+ // src/runtime/helpers/sync/fs.ts
1235
+ function readRuntimeFsTextSync(filePath, fs) {
1236
+ return assertSyncPortResult(fs.readText(filePath), "fs.readText", filePath);
1237
+ }
1238
+ function existsRuntimeFsSync(filePath, fs) {
1239
+ return assertSyncPortResult(fs.exists(filePath), "fs.exists", filePath);
1240
+ }
1241
+ function listRuntimeFsDirSync(dirPath, fs) {
1242
+ return assertSyncPortResult(fs.listDir(dirPath), "fs.listDir", dirPath);
1243
+ }
1244
+
1245
+ // src/shared/constants/issueCodes.ts
1246
+ var ISSUE_SCAN_DYNAMIC_KEY_SITES = "i18nprune.scan.dynamic_key_sites";
1247
+ var ISSUE_SYNC_LOCALE_FILE_NOT_FOUND = "i18nprune.sync.locale_file_not_found";
1248
+ var ISSUE_SYNC_METADATA_FLAG_CONFLICT = "i18nprune.sync.metadata_flag_conflict";
1249
+ var ISSUE_LOCALE_SOURCE_PLACEHOLDER_LEAVES = "i18nprune.locale.source_placeholder_leaves";
1250
+ var ISSUE_LOCALE_TARGET_PLACEHOLDER_LEAVES = "i18nprune.locale.target_placeholder_leaves";
1251
+ var ISSUE_IO_READ_FAILED = "i18nprune.io.read_failed";
1252
+
1253
+ // src/shared/json/parse.ts
1254
+ var I18nPruneJsonParseError = class extends I18nPruneError {
1255
+ filePath;
1256
+ line;
1257
+ column;
1258
+ offset;
1259
+ constructor(input) {
1260
+ const options = input.issueCode !== void 0 ? { cause: input.cause, issueCode: input.issueCode } : { cause: input.cause };
1261
+ super(input.message, input.code, options);
1262
+ this.name = "I18nPruneJsonParseError";
1263
+ this.filePath = input.filePath;
1264
+ this.line = input.location.line;
1265
+ this.column = input.location.column;
1266
+ this.offset = input.location.offset;
1267
+ }
1268
+ };
1269
+ function locationFromOffset(text, offset) {
1270
+ let line = 1;
1271
+ let column = 1;
1272
+ const capped = Math.max(0, Math.min(offset, text.length));
1273
+ for (let i = 0; i < capped; i += 1) {
1274
+ if (text.charCodeAt(i) === 10) {
1275
+ line += 1;
1276
+ column = 1;
1277
+ } else {
1278
+ column += 1;
1279
+ }
1280
+ }
1281
+ return { line, column, offset };
1282
+ }
1283
+ function getJsonParseLocation(error, text) {
1284
+ const message = error instanceof Error ? error.message : String(error);
1285
+ const positionMatch = /\bposition\s+(\d+)\b/i.exec(message);
1286
+ if (positionMatch?.[1]) {
1287
+ return locationFromOffset(text, Number.parseInt(positionMatch[1], 10));
1288
+ }
1289
+ const lineColumnMatch = /\bline\s+(\d+)\s+column\s+(\d+)\b/i.exec(message);
1290
+ if (lineColumnMatch?.[1] && lineColumnMatch[2]) {
1291
+ return {
1292
+ line: Number.parseInt(lineColumnMatch[1], 10),
1293
+ column: Number.parseInt(lineColumnMatch[2], 10)
1294
+ };
1295
+ }
1296
+ return {};
1297
+ }
1298
+ function formatJsonParseMessage(filePath, location, cause) {
1299
+ const subject = filePath ? `Invalid JSON in ${filePath}` : "Invalid JSON";
1300
+ 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)}` : "";
1301
+ const detail = cause instanceof Error ? cause.message : String(cause);
1302
+ return `${subject}${at}: ${detail}`;
1303
+ }
1304
+ function parseJsonText(text, options = {}) {
1305
+ try {
1306
+ return JSON.parse(text);
1307
+ } catch (cause) {
1308
+ const location = getJsonParseLocation(cause, text);
1309
+ throw new I18nPruneJsonParseError({
1310
+ message: formatJsonParseMessage(options.filePath, location, cause),
1311
+ code: options.code ?? "IO",
1312
+ cause,
1313
+ location,
1314
+ ...options.issueCode !== void 0 ? { issueCode: options.issueCode } : {},
1315
+ ...options.filePath !== void 0 ? { filePath: options.filePath } : {}
1316
+ });
1317
+ }
1318
+ }
1319
+ function tryParseJsonText(text, options = {}) {
1320
+ try {
1321
+ return { ok: true, data: parseJsonText(text, options) };
1322
+ } catch (error) {
1323
+ if (error instanceof I18nPruneJsonParseError) return { ok: false, error };
1324
+ throw error;
1325
+ }
1326
+ }
1327
+
1328
+ // src/shared/options/runOptions.ts
1329
+ var run = {
1330
+ json: false,
1331
+ jsonPretty: true,
1332
+ quiet: false,
1333
+ silent: false,
1334
+ debugScan: false,
1335
+ debugCache: false,
1336
+ onScanDebug: void 0
1337
+ };
1338
+ function getRunOptions() {
1339
+ return run;
1340
+ }
1341
+
1342
+ // src/shared/scanner/presets.ts
1343
+ var SCAN_EXCLUDE_PRESETS = {
1344
+ production: {
1345
+ dirs: ["node_modules", "dist", "build", "compiled", "tests", "bench"],
1346
+ files: ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"],
1347
+ extensions: ["test.ts", "test.tsx", "spec.ts", "spec.tsx", "test.js", "test.jsx", "spec.js", "spec.jsx"]
1348
+ }
1349
+ };
1350
+ function mergeExcludeRules(base, extra) {
1351
+ if (!base && !extra) return void 0;
1352
+ return {
1353
+ useDefaultSkip: base?.useDefaultSkip ?? extra?.useDefaultSkip,
1354
+ dirs: [...extra?.dirs ?? [], ...base?.dirs ?? []],
1355
+ files: [...extra?.files ?? [], ...base?.files ?? []],
1356
+ extensions: [...extra?.extensions ?? [], ...base?.extensions ?? []],
1357
+ patterns: [...extra?.patterns ?? [], ...base?.patterns ?? []]
1358
+ };
1359
+ }
1360
+ function resolveScanExcludeConfig(exclude) {
1361
+ if (!exclude) return void 0;
1362
+ const fromPreset = exclude.preset ? SCAN_EXCLUDE_PRESETS[exclude.preset] : void 0;
1363
+ return mergeExcludeRules(exclude, fromPreset);
1364
+ }
1365
+
1366
+ // src/shared/scanner/files.ts
1367
+ function resolveScanDebugSink(listOpts) {
1368
+ const g = getRunOptions();
1369
+ return listOpts?.onScanDebug ?? g.onScanDebug;
1370
+ }
1371
+ function emitScanDebug(sink, event) {
1372
+ if (sink) sink(event);
1373
+ }
1374
+ var DEFAULT_SCAN_SKIP_DIR_NAMES = [
1375
+ "node_modules",
1376
+ "dist",
1377
+ "build",
1378
+ ".git",
1379
+ "coverage",
1380
+ ".next",
1381
+ "out"
1382
+ ];
1383
+ var DEFAULT_SCAN_SKIP_DIR_SET = new Set(DEFAULT_SCAN_SKIP_DIR_NAMES);
1384
+ var SOURCE_FILE_NAME = /\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/i;
1385
+ var MAX_SOURCE_TREE_WALK_DEPTH = 48;
1386
+ function walkDirectoryKey(dir, pathPort, fs) {
1387
+ if (fs.realpath) {
1388
+ try {
1389
+ return fs.realpath(dir);
1390
+ } catch {
1391
+ return pathPort.resolve(dir);
1392
+ }
1393
+ }
1394
+ return pathPort.resolve(dir);
1395
+ }
1396
+ function normExtToken(s) {
1397
+ const t = s.trim().toLowerCase();
1398
+ return t.startsWith(".") ? t.slice(1) : t;
1399
+ }
1400
+ function basenameExtensionSuffixes(fileBase) {
1401
+ const parts = fileBase.split(".");
1402
+ if (parts.length < 2) return [];
1403
+ const out = [];
1404
+ for (let i = 1; i < parts.length; i++) {
1405
+ out.push(parts.slice(i).join(".").toLowerCase());
1406
+ }
1407
+ out.sort((a, b) => b.length - a.length);
1408
+ return out;
1409
+ }
1410
+ function relPosix(pathPort, rootDir, absPath) {
1411
+ return pathPort.relative(rootDir, absPath).replace(/\\/g, "/");
1412
+ }
1413
+ function partitionRules(rules) {
1414
+ const strings = /* @__PURE__ */ new Set();
1415
+ const regexes = [];
1416
+ if (!rules) return { strings, regexes };
1417
+ for (const r of rules) {
1418
+ if (typeof r === "string") {
1419
+ const t = r.trim();
1420
+ if (t) strings.add(t);
1421
+ } else {
1422
+ regexes.push(r);
1423
+ }
1424
+ }
1425
+ return { strings, regexes };
1426
+ }
1427
+ function partitionExtRules(rules) {
1428
+ const strings = /* @__PURE__ */ new Set();
1429
+ const regexes = [];
1430
+ if (!rules) return { strings, regexes };
1431
+ for (const r of rules) {
1432
+ if (typeof r === "string") {
1433
+ const n = normExtToken(r);
1434
+ if (n) strings.add(n);
1435
+ } else {
1436
+ regexes.push(r);
1437
+ }
1438
+ }
1439
+ return { strings, regexes };
1440
+ }
1441
+ function compileScanExclude(exclude) {
1442
+ const resolved = resolveScanExcludeConfig(exclude);
1443
+ const useDefault = resolved?.useDefaultSkip !== false;
1444
+ const defaultDirs = useDefault ? DEFAULT_SCAN_SKIP_DIR_SET : null;
1445
+ const dirs = partitionRules(resolved?.dirs);
1446
+ const files = partitionRules(resolved?.files);
1447
+ const exts = partitionExtRules(resolved?.extensions);
1448
+ const pathPatterns = resolved?.patterns ?? [];
1449
+ 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;
1450
+ return {
1451
+ defaultDirs,
1452
+ dirStrings: dirs.strings,
1453
+ dirRegexes: dirs.regexes,
1454
+ fileStrings: files.strings,
1455
+ fileRegexes: files.regexes,
1456
+ extStrings: exts.strings,
1457
+ extRegexes: exts.regexes,
1458
+ pathPatterns,
1459
+ userRulesEmpty
1460
+ };
1461
+ }
1462
+ function explainDirSkip(name, c) {
1463
+ if (c.defaultDirs?.has(name)) return `built-in directory skip (${name})`;
1464
+ if (c.userRulesEmpty) return null;
1465
+ if (c.dirStrings.has(name)) return `exclude.dirs (${name})`;
1466
+ for (const re of c.dirRegexes) {
1467
+ if (re.test(name)) return `exclude.dirs regex /${re.source}/`;
1468
+ }
1469
+ return null;
1470
+ }
1471
+ function explainFileSkip(relPosix2, baseName, c) {
1472
+ if (c.userRulesEmpty) return null;
1473
+ if (c.fileStrings.has(baseName)) return `exclude.files (${baseName})`;
1474
+ for (const re of c.fileRegexes) {
1475
+ if (re.test(baseName)) return `exclude.files regex /${re.source}/`;
1476
+ }
1477
+ if (c.extStrings.size > 0 || c.extRegexes.length > 0) {
1478
+ for (const suf of basenameExtensionSuffixes(baseName)) {
1479
+ if (c.extStrings.has(suf)) return `exclude.extensions (${suf})`;
1480
+ for (const re of c.extRegexes) {
1481
+ if (re.test(suf)) return `exclude.extensions regex /${re.source}/`;
1482
+ }
1483
+ }
1484
+ }
1485
+ for (const re of c.pathPatterns) {
1486
+ if (re.test(relPosix2)) return `exclude.patterns /${re.source}/ \u2190 ${relPosix2}`;
1487
+ }
1488
+ return null;
1489
+ }
1490
+ function listSourceFiles(runtime, rootDir, exclude, listOpts) {
1491
+ const { fs, path } = runtime;
1492
+ const compiled = compileScanExclude(exclude);
1493
+ const out = [];
1494
+ const debugSink = resolveScanDebugSink(listOpts);
1495
+ const visitedDirs = /* @__PURE__ */ new Set();
1496
+ function walk(dir, depth) {
1497
+ if (!existsRuntimeFsSync(dir, fs)) return;
1498
+ const dirKey = walkDirectoryKey(dir, path, fs);
1499
+ if (visitedDirs.has(dirKey)) {
1500
+ emitScanDebug(debugSink, {
1501
+ kind: "skip_directory",
1502
+ relativePath: relPosix(path, rootDir, dir),
1503
+ basename: path.basename(dir),
1504
+ reason: "directory already visited (symlink cycle or duplicate path)"
1505
+ });
1506
+ return;
1507
+ }
1508
+ visitedDirs.add(dirKey);
1509
+ if (depth > MAX_SOURCE_TREE_WALK_DEPTH) {
1510
+ emitScanDebug(debugSink, {
1511
+ kind: "skip_directory",
1512
+ relativePath: relPosix(path, rootDir, dir),
1513
+ basename: path.basename(dir),
1514
+ reason: `scan depth limit (${String(MAX_SOURCE_TREE_WALK_DEPTH)})`
1515
+ });
1516
+ return;
1517
+ }
1518
+ const entries = listRuntimeFsDirSync(dir, fs);
1519
+ for (const e of entries) {
1520
+ const p = path.join(dir, e.name);
1521
+ if (e.kind === "directory") {
1522
+ const why = explainDirSkip(e.name, compiled);
1523
+ if (why) {
1524
+ emitScanDebug(debugSink, {
1525
+ kind: "skip_directory",
1526
+ relativePath: relPosix(path, rootDir, p),
1527
+ basename: e.name,
1528
+ reason: why
1529
+ });
1530
+ continue;
1531
+ }
1532
+ walk(p, depth + 1);
1533
+ } else if (e.kind === "file") {
1534
+ const rel = relPosix(path, rootDir, p);
1535
+ if (!SOURCE_FILE_NAME.test(e.name)) {
1536
+ emitScanDebug(debugSink, {
1537
+ kind: "skip_file",
1538
+ relativePath: rel,
1539
+ basename: e.name,
1540
+ reason: `not a scanned source extension (${e.name})`
1541
+ });
1542
+ continue;
1543
+ }
1544
+ const whyF = explainFileSkip(rel, e.name, compiled);
1545
+ if (whyF) {
1546
+ emitScanDebug(debugSink, {
1547
+ kind: "skip_file",
1548
+ relativePath: rel,
1549
+ basename: e.name,
1550
+ reason: whyF
1551
+ });
1552
+ continue;
1553
+ }
1554
+ out.push(p);
1555
+ } else if (e.kind === "other") {
1556
+ emitScanDebug(debugSink, {
1557
+ kind: "skip_file",
1558
+ relativePath: relPosix(path, rootDir, p),
1559
+ basename: e.name,
1560
+ reason: "not a regular file or directory (symlink or special entry)"
1561
+ });
1562
+ }
1563
+ }
1564
+ }
1565
+ walk(rootDir, 0);
1566
+ return out;
1567
+ }
1568
+
1569
+ // src/extractor/shared/projectScan.ts
1570
+ function resolveScanIo(input) {
1571
+ const { readFile, listFiles, runtime, path: pathOverride } = input;
1572
+ const pathPort = runtime?.path ?? pathOverride;
1573
+ if (readFile && listFiles) {
1574
+ if (!pathPort) {
1575
+ throw new I18nPruneError(
1576
+ "scanProjectSourceFiles: with custom `readFile`/`listFiles`, pass `runtime` or `path` for display paths.",
1577
+ "USAGE"
1578
+ );
1579
+ }
1580
+ if (input.cwd !== void 0) {
1581
+ return { cwd: input.cwd, listFiles, readFile, pathPort };
1582
+ }
1583
+ if (runtime) {
1584
+ return { cwd: input.cwd ?? runtime.system.cwd(), listFiles, readFile, pathPort };
1585
+ }
1586
+ throw new I18nPruneError(
1587
+ "scanProjectSourceFiles: when using custom `readFile` and `listFiles`, set `cwd` or pass `runtime` for `system.cwd()`.",
1588
+ "USAGE"
1589
+ );
1590
+ }
1591
+ if (readFile || listFiles) {
1592
+ throw new I18nPruneError(
1593
+ "scanProjectSourceFiles: provide both `readFile` and `listFiles`, or omit both and pass `runtime`.",
1594
+ "USAGE"
1595
+ );
1596
+ }
1597
+ if (!runtime) {
1598
+ throw new I18nPruneError(
1599
+ "scanProjectSourceFiles: pass `runtime` ({ fs, path, system }) or both `readFile` and `listFiles`.",
1600
+ "USAGE"
1601
+ );
1602
+ }
1603
+ const cwd = input.cwd ?? runtime.system.cwd();
1604
+ return {
1605
+ cwd,
1606
+ listFiles: (srcRoot) => listSourceFiles(runtime, srcRoot, input.exclude, input.scanDebug ? { onScanDebug: input.scanDebug } : void 0),
1607
+ readFile: (filePath) => readRuntimeFsTextSync(filePath, runtime.fs),
1608
+ pathPort: runtime.path
1609
+ };
1610
+ }
1611
+ function scanProjectSourceFiles(input) {
1612
+ const { cwd, listFiles, readFile, pathPort } = resolveScanIo(input);
1613
+ const files = listFiles(input.srcRoot);
1614
+ const out = [];
1615
+ for (const filePath of files) {
1616
+ let text;
1617
+ try {
1618
+ text = readFile(filePath);
1619
+ } catch {
1620
+ continue;
1621
+ }
1622
+ const relFromSrc = pathPort.relative(input.srcRoot, filePath).replace(/\\/g, "/");
1623
+ const displayPath = relFromSrc && !relFromSrc.startsWith("..") ? relFromSrc : (() => {
1624
+ const relCwd = pathPort.relative(cwd, filePath);
1625
+ return relCwd && !relCwd.startsWith("..") ? relCwd.replace(/\\/g, "/") : filePath;
1626
+ })();
1627
+ const rows = input.scanFile({ filePath, displayPath, text });
1628
+ out.push(...rows);
1629
+ }
1630
+ return out;
1631
+ }
1632
+
1633
+ // src/extractor/constmap/build.ts
1634
+ function buildConstStringMap(source) {
1635
+ const map = {};
1636
+ const re = /\bconst\s+([A-Za-z_$][\w$]*)(?:\s*:\s*[^=]+)?\s*=\s*(['"`])([^'"`]+)\2\s*;?/g;
1637
+ let m;
1638
+ while ((m = re.exec(source)) !== null) {
1639
+ map[m[1]] = m[3];
1640
+ }
1641
+ return map;
1642
+ }
1643
+
1644
+ // src/extractor/shared/pattern.ts
1645
+ function escapeRegex(s) {
1646
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1647
+ }
1648
+ function buildFunctionsPattern(functions) {
1649
+ const parts = functions.map((f) => {
1650
+ const escaped = escapeRegex(f);
1651
+ return escaped.replace(/\\\./g, "\\.");
1652
+ });
1653
+ return `(?:${parts.join("|")})`;
1654
+ }
1655
+
1656
+ // src/extractor/shared/calls.ts
1657
+ function findTranslationCallSites(text, functions) {
1658
+ const out = [];
1659
+ if (functions.length === 0) return out;
1660
+ const fn = buildFunctionsPattern(functions);
1661
+ const callStart = new RegExp(`\\b(${fn})\\s*\\(`, "g");
1662
+ let m;
1663
+ while ((m = callStart.exec(text)) !== null) {
1664
+ const functionName = m[1];
1665
+ const matchIndex = m.index;
1666
+ const fnEsc = escapeRegex(functionName);
1667
+ const decl = new RegExp(`\\b(?:export\\s+)?function\\s+${fnEsc}\\s*\\(`);
1668
+ if (decl.test(text.slice(Math.max(0, matchIndex - 64), matchIndex + functionName.length + 2))) {
1669
+ continue;
1670
+ }
1671
+ const openParenIndex = matchIndex + m[0].length - 1;
1672
+ const closeParenIndex = findMatchingCloseParen(text, openParenIndex);
1673
+ if (closeParenIndex === -1) continue;
1674
+ const first = parseFirstArgument(text, openParenIndex, closeParenIndex);
1675
+ if (!first.empty && firstArgLooksLikeAsciiLowercaseProse(first.raw)) {
1676
+ continue;
1677
+ }
1678
+ out.push({
1679
+ functionName,
1680
+ matchIndex,
1681
+ openParenIndex,
1682
+ closeParenIndex,
1683
+ firstArgStart: first.start,
1684
+ firstArgEnd: first.end,
1685
+ firstArgRaw: first.raw,
1686
+ isEmptyCall: first.empty,
1687
+ isMultilineCall: text.slice(matchIndex, closeParenIndex + 1).includes("\n")
1688
+ });
1689
+ }
1690
+ return out;
1691
+ }
1692
+ function firstArgLooksLikeAsciiLowercaseProse(raw) {
1693
+ const trimmed = raw.trim();
1694
+ if (trimmed.length === 0) return false;
1695
+ if (!/\s/.test(trimmed)) return false;
1696
+ const parts = trimmed.split(/\s+/).filter(Boolean);
1697
+ if (parts.length < 2) return false;
1698
+ for (const p of parts) {
1699
+ if (!/^[a-z]+$/.test(p)) return false;
1700
+ }
1701
+ return true;
1702
+ }
1703
+ function findMatchingCloseParen(text, openParenIndex) {
1704
+ let i = openParenIndex + 1;
1705
+ let depth = 1;
1706
+ while (i < text.length) {
1707
+ const c = text[i];
1708
+ if (c === "'" || c === '"') {
1709
+ i = skipString2(text, i, c);
1710
+ continue;
1711
+ }
1712
+ if (c === "`") {
1713
+ i = skipTemplate2(text, i);
1714
+ continue;
1715
+ }
1716
+ if (c === "/" && text[i + 1] === "/") {
1717
+ i = skipLineComment(text, i);
1718
+ continue;
1719
+ }
1720
+ if (c === "/" && text[i + 1] === "*") {
1721
+ i = skipBlockComment(text, i);
1722
+ continue;
1723
+ }
1724
+ if (c === "(") depth += 1;
1725
+ else if (c === ")") {
1726
+ depth -= 1;
1727
+ if (depth === 0) return i;
1728
+ }
1729
+ i += 1;
1730
+ }
1731
+ return -1;
1732
+ }
1733
+ function parseFirstArgument(text, openParenIndex, closeParenIndex) {
1734
+ let i = skipTrivia(text, openParenIndex + 1, closeParenIndex);
1735
+ if (i >= closeParenIndex) return { start: i, end: i, raw: "", empty: true };
1736
+ let parenDepth = 0;
1737
+ let bracketDepth = 0;
1738
+ let braceDepth = 0;
1739
+ const start = i;
1740
+ while (i < closeParenIndex) {
1741
+ const c = text[i];
1742
+ if (c === "'" || c === '"') {
1743
+ i = skipString2(text, i, c);
1744
+ continue;
1745
+ }
1746
+ if (c === "`") {
1747
+ i = skipTemplate2(text, i);
1748
+ continue;
1749
+ }
1750
+ if (c === "/" && text[i + 1] === "/") {
1751
+ i = skipLineComment(text, i);
1752
+ continue;
1753
+ }
1754
+ if (c === "/" && text[i + 1] === "*") {
1755
+ i = skipBlockComment(text, i);
1756
+ continue;
1757
+ }
1758
+ if (c === "(") parenDepth += 1;
1759
+ else if (c === ")") {
1760
+ if (parenDepth > 0) parenDepth -= 1;
1761
+ } else if (c === "[") bracketDepth += 1;
1762
+ else if (c === "]") {
1763
+ if (bracketDepth > 0) bracketDepth -= 1;
1764
+ } else if (c === "{") braceDepth += 1;
1765
+ else if (c === "}") {
1766
+ if (braceDepth > 0) braceDepth -= 1;
1767
+ } else if (c === "," && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
1768
+ break;
1769
+ }
1770
+ i += 1;
1771
+ }
1772
+ const end = rtrimIndex(text, start, i);
1773
+ return {
1774
+ start,
1775
+ end,
1776
+ raw: text.slice(start, end),
1777
+ empty: false
1778
+ };
1779
+ }
1780
+ function skipTrivia(text, start, end) {
1781
+ let i = start;
1782
+ while (i < end) {
1783
+ const c = text[i];
1784
+ if (/\s/.test(c)) {
1785
+ i += 1;
1786
+ continue;
1787
+ }
1788
+ if (c === "/" && text[i + 1] === "/") {
1789
+ i = skipLineComment(text, i);
1790
+ continue;
1791
+ }
1792
+ if (c === "/" && text[i + 1] === "*") {
1793
+ i = skipBlockComment(text, i);
1794
+ continue;
1795
+ }
1796
+ break;
1797
+ }
1798
+ return i;
1799
+ }
1800
+ function rtrimIndex(text, start, end) {
1801
+ let i = end;
1802
+ while (i > start && /\s/.test(text[i - 1])) i -= 1;
1803
+ return i;
1804
+ }
1805
+ function skipLineComment(text, start) {
1806
+ let i = start + 2;
1807
+ while (i < text.length && text[i] !== "\n") i += 1;
1808
+ return i;
1809
+ }
1810
+ function skipBlockComment(text, start) {
1811
+ let i = start + 2;
1812
+ while (i + 1 < text.length) {
1813
+ if (text[i] === "*" && text[i + 1] === "/") return i + 2;
1814
+ i += 1;
1815
+ }
1816
+ return text.length;
1817
+ }
1818
+ function skipString2(text, start, quote) {
1819
+ let i = start + 1;
1820
+ while (i < text.length) {
1821
+ const ch = text[i];
1822
+ if (ch === "\\") {
1823
+ i += 2;
1824
+ continue;
1825
+ }
1826
+ if (ch === quote) return i + 1;
1827
+ i += 1;
1828
+ }
1829
+ return text.length;
1830
+ }
1831
+ function skipTemplate2(text, start) {
1832
+ let i = start + 1;
1833
+ while (i < text.length) {
1834
+ const ch = text[i];
1835
+ if (ch === "\\") {
1836
+ i += 2;
1837
+ continue;
1838
+ }
1839
+ if (ch === "`") return i + 1;
1840
+ if (ch === "$" && text[i + 1] === "{") {
1841
+ i += 2;
1842
+ let depth = 1;
1843
+ while (i < text.length && depth > 0) {
1844
+ const c2 = text[i];
1845
+ if (c2 === "'" || c2 === '"') {
1846
+ i = skipString2(text, i, c2);
1847
+ continue;
1848
+ }
1849
+ if (c2 === "`") {
1850
+ i = skipTemplate2(text, i);
1851
+ continue;
1852
+ }
1853
+ if (c2 === "/" && text[i + 1] === "/") {
1854
+ i = skipLineComment(text, i);
1855
+ continue;
1856
+ }
1857
+ if (c2 === "/" && text[i + 1] === "*") {
1858
+ i = skipBlockComment(text, i);
1859
+ continue;
1860
+ }
1861
+ if (c2 === "{") depth += 1;
1862
+ else if (c2 === "}") depth -= 1;
1863
+ i += 1;
1864
+ }
1865
+ continue;
1866
+ }
1867
+ i += 1;
1868
+ }
1869
+ return text.length;
1870
+ }
1871
+
1872
+ // src/extractor/dynamic/helpers.ts
1873
+ function snippetRange(text, start, end, maxLen) {
1874
+ const boundedEnd = Math.max(start, Math.min(end, text.length));
1875
+ const slice = text.slice(start, boundedEnd).replace(/\s+/g, " ").trim();
1876
+ if (slice.length <= maxLen) return slice;
1877
+ return `${slice.slice(0, maxLen - 1)}\u2026`;
1878
+ }
1879
+ function offsetToLineColumn(text, offset) {
1880
+ let line = 1;
1881
+ let col = 1;
1882
+ for (let i = 0; i < offset && i < text.length; i += 1) {
1883
+ const c = text[i];
1884
+ if (c === "\n") {
1885
+ line += 1;
1886
+ col = 1;
1887
+ } else {
1888
+ col += 1;
1889
+ }
1890
+ }
1891
+ return { line, column: col };
1892
+ }
1893
+
1894
+ // src/extractor/constmap/resolve.ts
1895
+ function resolveKeyPlaceholdersWithTrace(fragment, constMap) {
1896
+ let out = fragment;
1897
+ const substitutions = [];
1898
+ const re = /\$\{([A-Za-z_$][\w$]*)\}/;
1899
+ for (let i = 0; i < 64; i += 1) {
1900
+ const m = re.exec(out);
1901
+ if (!m) break;
1902
+ const name = m[1];
1903
+ const val = constMap[name];
1904
+ if (val === void 0) {
1905
+ return { resolved: null, substitutions, remainder: out };
1906
+ }
1907
+ substitutions.push({ identifier: name, value: val });
1908
+ out = out.replace(m[0], val);
1909
+ }
1910
+ if (/\$\{[^}]+\}/.test(out)) {
1911
+ return { resolved: null, substitutions, remainder: out };
1912
+ }
1913
+ return { resolved: out, substitutions, remainder: out };
1914
+ }
1915
+
1916
+ // src/extractor/dynamic/rebuild.ts
1917
+ function tryRebuildTemplateKeyFromConsts(inner, constMap) {
1918
+ return resolveKeyPlaceholdersWithTrace(inner, constMap).resolved;
1919
+ }
1920
+ function tryResolveTemplatePrefixBeforeUnknown(inner, constMap) {
1921
+ const interpRe = /\$\{([^}]+)\}/g;
1922
+ const matches = [...inner.matchAll(interpRe)];
1923
+ if (matches.length === 0) {
1924
+ const t = inner.trim();
1925
+ return t.includes(".") ? t : null;
1926
+ }
1927
+ let cutIndex = -1;
1928
+ for (const m of matches) {
1929
+ const expr = m[1]?.trim();
1930
+ const ident = expr?.match(/^[A-Za-z_$][\w$]*$/)?.[0];
1931
+ if (!ident) {
1932
+ cutIndex = m.index ?? -1;
1933
+ break;
1934
+ }
1935
+ if (!Object.prototype.hasOwnProperty.call(constMap, ident)) {
1936
+ cutIndex = m.index ?? -1;
1937
+ break;
1938
+ }
1939
+ }
1940
+ const prefix = (cutIndex === -1 ? inner : inner.slice(0, cutIndex)).trim();
1941
+ const resolved = prefix.replace(/\$\{([^}]+)\}/g, (whole, innerExpr) => {
1942
+ const ident = String(innerExpr).trim();
1943
+ if (/^[A-Za-z_$][\w$]*$/.test(ident) && Object.prototype.hasOwnProperty.call(constMap, ident)) {
1944
+ return constMap[ident];
1945
+ }
1946
+ return whole;
1947
+ });
1948
+ const trimmed = resolved.replace(/\.$/, "").trim();
1949
+ if (!trimmed.includes(".")) return null;
1950
+ return trimmed;
1951
+ }
1952
+
1953
+ // src/extractor/dynamic/providers/javascript.ts
1954
+ var PREVIEW = 72;
1955
+ var JAVASCRIPT_LIKE_EXTENSIONS = /* @__PURE__ */ new Set([
1956
+ ".ts",
1957
+ ".tsx",
1958
+ ".js",
1959
+ ".jsx",
1960
+ ".mjs",
1961
+ ".cjs",
1962
+ ".vue",
1963
+ ".svelte"
1964
+ ]);
1965
+ function isJavascriptLikePath(filePath) {
1966
+ const lower = filePath.toLowerCase();
1967
+ const dot = lower.lastIndexOf(".");
1968
+ if (dot < 0) return false;
1969
+ return JAVASCRIPT_LIKE_EXTENSIONS.has(lower.slice(dot));
1970
+ }
1971
+ function findDynamicKeySitesInJavascriptFile(text, functions, filePath) {
1972
+ const commentRanges = commentRangesForJsLikeText(text);
1973
+ const constMap = buildConstStringMap(text);
1974
+ const raw = findDynamicKeySitesRaw(text, functions, constMap);
1975
+ const out = [];
1976
+ for (const site of raw) {
1977
+ const at = site.matchIndex;
1978
+ const commented = offsetInCommentRanges(at, commentRanges);
1979
+ const { line, column } = offsetToLineColumn(text, at);
1980
+ const kind = commented ? "commented" : site.kind;
1981
+ out.push({
1982
+ kind,
1983
+ functionName: site.functionName,
1984
+ preview: snippetRange(text, at, site.closeParenIndex + 1, PREVIEW),
1985
+ filePath,
1986
+ line,
1987
+ column,
1988
+ isMultilineCall: site.isMultilineCall,
1989
+ isCommented: commented,
1990
+ isSourceFile: true,
1991
+ ...site.resolvedPrefix !== void 0 ? { resolvedPrefix: site.resolvedPrefix } : {}
1992
+ });
1993
+ }
1994
+ return out;
1995
+ }
1996
+ function findDynamicKeySitesRaw(text, functions, constMap) {
1997
+ const out = [];
1998
+ const calls = findTranslationCallSites(text, functions);
1999
+ for (const call of calls) {
2000
+ const at = call.matchIndex;
2001
+ if (call.isEmptyCall) {
2002
+ out.push({
2003
+ kind: "empty_call",
2004
+ functionName: call.functionName,
2005
+ matchIndex: at,
2006
+ closeParenIndex: call.closeParenIndex,
2007
+ isMultilineCall: call.isMultilineCall
2008
+ });
2009
+ continue;
2010
+ }
2011
+ const arg = call.firstArgRaw.trim();
2012
+ const first = arg[0];
2013
+ if (first === "'" || first === '"') continue;
2014
+ if (first === "`" && arg.endsWith("`")) {
2015
+ const inner = arg.slice(1, -1);
2016
+ if (!/\$\{/.test(inner)) continue;
2017
+ const rebuilt = tryRebuildTemplateKeyFromConsts(inner, constMap);
2018
+ if (rebuilt !== null) continue;
2019
+ const resolvedPrefix = tryResolveTemplatePrefixBeforeUnknown(inner, constMap) ?? void 0;
2020
+ out.push({
2021
+ kind: "template_interpolation",
2022
+ functionName: call.functionName,
2023
+ matchIndex: at,
2024
+ closeParenIndex: call.closeParenIndex,
2025
+ isMultilineCall: call.isMultilineCall,
2026
+ ...resolvedPrefix !== void 0 ? { resolvedPrefix } : {}
2027
+ });
2028
+ continue;
2029
+ }
2030
+ out.push({
2031
+ kind: "non_literal",
2032
+ functionName: call.functionName,
2033
+ matchIndex: at,
2034
+ closeParenIndex: call.closeParenIndex,
2035
+ isMultilineCall: call.isMultilineCall
2036
+ });
2037
+ }
2038
+ return out;
2039
+ }
2040
+
2041
+ // src/extractor/dynamic/providers/index.ts
2042
+ function findDynamicKeySitesForFile(filePath, content, functions) {
2043
+ if (!isJavascriptLikePath(filePath)) {
2044
+ return [];
2045
+ }
2046
+ return findDynamicKeySitesInJavascriptFile(content, functions, filePath);
2047
+ }
2048
+
2049
+ // src/extractor/dynamic/orchestrate.ts
2050
+ function scanProjectDynamicKeySites(input) {
2051
+ return scanProjectSourceFiles({
2052
+ srcRoot: input.srcRoot,
2053
+ cwd: input.cwd,
2054
+ runtime: input.runtime,
2055
+ path: input.path,
2056
+ readFile: input.readFile,
2057
+ listFiles: input.listFiles,
2058
+ exclude: input.exclude,
2059
+ scanFile: ({ filePath, displayPath, text }) => {
2060
+ const functions = expandFunctionsWithBindings(input.functions, scanImportBindings(text));
2061
+ const sites = findDynamicKeySitesForFile(filePath, text, functions);
2062
+ return sites.map((site) => ({ ...site, filePath: displayPath }));
2063
+ }
2064
+ });
2065
+ }
2066
+
2067
+ // src/extractor/keySites/line.ts
2068
+ function lineNumberAtIndex(text, index) {
2069
+ if (index <= 0) return 1;
2070
+ let line = 1;
2071
+ const end = Math.min(index, text.length);
2072
+ for (let i = 0; i < end; i++) {
2073
+ if (text.charCodeAt(i) === 10) line += 1;
2074
+ }
2075
+ return line;
2076
+ }
2077
+
2078
+ // src/extractor/keySites/scan.ts
2079
+ function spanAtOffset(text, offset, functionName, isMultilineCall) {
2080
+ return {
2081
+ line: lineNumberAtIndex(text, offset),
2082
+ functionName,
2083
+ isMultilineCall,
2084
+ charOffset: offset
2085
+ };
2086
+ }
2087
+ function scanKeyObservations(text, functions, constMap, options) {
2088
+ const out = [];
2089
+ const calls = findTranslationCallSites(text, functions);
2090
+ const ranges = options?.commentRanges;
2091
+ for (const call of calls) {
2092
+ if (ranges?.length && offsetInCommentRanges(call.matchIndex, ranges)) continue;
2093
+ if (call.isEmptyCall) continue;
2094
+ const arg = call.firstArgRaw.trim();
2095
+ const stringMatch = arg.match(/^(['"])([\s\S]*)\1$/);
2096
+ if (stringMatch) {
2097
+ const key = stringMatch[2];
2098
+ out.push({
2099
+ kind: "literal",
2100
+ resolvedKey: key,
2101
+ raw: key,
2102
+ span: spanAtOffset(text, call.matchIndex, call.functionName, call.isMultilineCall)
2103
+ });
2104
+ continue;
2105
+ }
2106
+ const tplMatch = arg.match(/^`([\s\S]*)`$/);
2107
+ if (!tplMatch) continue;
2108
+ const templateRaw = tplMatch[1];
2109
+ const trace = resolveKeyPlaceholdersWithTrace(templateRaw, constMap);
2110
+ const span = spanAtOffset(text, call.matchIndex, call.functionName, call.isMultilineCall);
2111
+ if (trace.resolved !== null) {
2112
+ out.push({
2113
+ kind: "template_resolved",
2114
+ resolvedKey: trace.resolved,
2115
+ templateRaw,
2116
+ substitutions: trace.substitutions,
2117
+ span
2118
+ });
2119
+ } else {
2120
+ const unresolved = [];
2121
+ const rem = trace.remainder;
2122
+ const ph = /\$\{([A-Za-z_$][\w$]*)\}/g;
2123
+ let pm;
2124
+ while ((pm = ph.exec(rem)) !== null) {
2125
+ unresolved.push(pm[1]);
2126
+ }
2127
+ const uncertainPrefix = tryResolveTemplatePrefixBeforeUnknown(templateRaw, constMap) ?? void 0;
2128
+ out.push({
2129
+ kind: "template_partial",
2130
+ templateRaw,
2131
+ substitutions: trace.substitutions,
2132
+ unresolvedPlaceholders: unresolved,
2133
+ span,
2134
+ ...uncertainPrefix !== void 0 ? { uncertainPrefix } : {}
2135
+ });
2136
+ }
2137
+ }
2138
+ return out;
2139
+ }
2140
+
2141
+ // src/extractor/keySites/orchestrate.ts
2142
+ function scanProjectKeyObservations(input) {
2143
+ return scanProjectSourceFiles({
2144
+ srcRoot: input.srcRoot,
2145
+ cwd: input.cwd,
2146
+ runtime: input.runtime,
2147
+ path: input.path,
2148
+ readFile: input.readFile,
2149
+ listFiles: input.listFiles,
2150
+ exclude: input.exclude,
2151
+ scanFile: ({ text, displayPath }) => {
2152
+ const functions = expandFunctionsWithBindings(input.functions, scanImportBindings(text));
2153
+ const constMap = buildConstStringMap(text);
2154
+ const commentRanges = commentRangesForJsLikeText(text);
2155
+ const observations = scanKeyObservations(text, functions, constMap, {
2156
+ commentRanges
2157
+ });
2158
+ return observations.map((obs) => ({
2159
+ ...obs,
2160
+ span: { ...obs.span, filePath: displayPath }
2161
+ }));
2162
+ }
2163
+ });
2164
+ }
2165
+
2166
+ // src/extractor/keySites/projectUsage.ts
2167
+ function topPathSegment(path) {
2168
+ const first = path.split(/[.[\]]/).find(Boolean);
2169
+ return first ?? null;
2170
+ }
2171
+ function literalKeyUsageFromObservations(observations) {
2172
+ const resolvedKeys = /* @__PURE__ */ new Set();
2173
+ const uncertainPrefixes = /* @__PURE__ */ new Set();
2174
+ const usedRoots = /* @__PURE__ */ new Set();
2175
+ for (const o of observations) {
2176
+ if (o.kind === "literal" || o.kind === "template_resolved") {
2177
+ resolvedKeys.add(o.resolvedKey);
2178
+ const root = topPathSegment(o.resolvedKey);
2179
+ if (root) usedRoots.add(root);
2180
+ continue;
2181
+ }
2182
+ if (o.kind === "template_partial" && o.uncertainPrefix) {
2183
+ uncertainPrefixes.add(o.uncertainPrefix);
2184
+ const root = topPathSegment(o.uncertainPrefix);
2185
+ if (root) usedRoots.add(root);
2186
+ }
2187
+ }
2188
+ return { resolvedKeys, uncertainPrefixes, usedRoots };
2189
+ }
2190
+
2191
+ // src/shared/reference/context.ts
2192
+ function pushUnique(out, v) {
2193
+ if (!v || out.includes(v)) return;
2194
+ out.push(v);
2195
+ }
2196
+ function includeDynamicSite(site, eff) {
2197
+ if ((site.kind === "commented" || site.isCommented) && !eff.treatCommentedCallSitesAsRuntime) return false;
2198
+ if (site.isSourceFile === false && !eff.treatNonSourceFileSitesAsRuntime) return false;
2199
+ return true;
2200
+ }
2201
+ function collectUncertainPrefixesFromDynamic(sites, out, eff) {
2202
+ for (const site of sites) {
2203
+ if (!includeDynamicSite(site, eff)) continue;
2204
+ if (site.resolvedPrefix) pushUnique(out, site.resolvedPrefix);
2205
+ }
2206
+ }
2207
+ function buildKeyReferenceContextFromLiteralUsageAndDynamicSites(usage, dynamicSites, eff) {
2208
+ const uncertainPrefixes = [];
2209
+ for (const p of usage.uncertainPrefixes) pushUnique(uncertainPrefixes, p);
2210
+ collectUncertainPrefixesFromDynamic([...dynamicSites], uncertainPrefixes, eff);
2211
+ return { provenKeys: usage.resolvedKeys, uncertainPrefixes };
2212
+ }
2213
+ function buildKeyReferenceContextFromReportDetails(observations, dynamicSites, eff) {
2214
+ const usage = literalKeyUsageFromObservations([...observations]);
2215
+ return buildKeyReferenceContextFromLiteralUsageAndDynamicSites(usage, dynamicSites, eff);
2216
+ }
2217
+
2218
+ // src/shared/reference/resolveConfig.ts
2219
+ var BASE = {
2220
+ treatCommentedCallSitesAsRuntime: false,
2221
+ treatNonSourceFileSitesAsRuntime: false,
2222
+ uncertainKeyPolicy: "protect",
2223
+ stringPresence: "guard",
2224
+ stringPresenceMaxHitsPerKey: 5,
2225
+ respectPreserve: true
2226
+ };
2227
+ function resolveReferenceConfig(operation, config) {
2228
+ const ref = config.reference;
2229
+ const d = ref?.defaults ?? {};
2230
+ const cmds = ref?.commands;
2231
+ const cmd = cmds?.[operation] ?? {};
2232
+ const merged = { ...BASE, ...d, ...cmd };
2233
+ return {
2234
+ treatCommentedCallSitesAsRuntime: merged.treatCommentedCallSitesAsRuntime ?? BASE.treatCommentedCallSitesAsRuntime,
2235
+ treatNonSourceFileSitesAsRuntime: merged.treatNonSourceFileSitesAsRuntime ?? BASE.treatNonSourceFileSitesAsRuntime,
2236
+ uncertainKeyPolicy: merged.uncertainKeyPolicy ?? BASE.uncertainKeyPolicy,
2237
+ stringPresence: merged.stringPresence ?? BASE.stringPresence,
2238
+ stringPresenceMaxHitsPerKey: merged.stringPresenceMaxHitsPerKey ?? BASE.stringPresenceMaxHitsPerKey,
2239
+ respectPreserve: merged.respectPreserve ?? BASE.respectPreserve
2240
+ };
2241
+ }
2242
+
2243
+ // src/shared/path/posix.ts
2244
+ function toPosixPath(value) {
2245
+ return value.replace(/\\/g, "/");
2246
+ }
2247
+
2248
+ // src/shared/path/platform.ts
2249
+ function normalizePathKeyForCache(projectRoot) {
2250
+ return toPosixPath(projectRoot).normalize("NFC").toLowerCase();
2251
+ }
2252
+
2253
+ // src/cache/io/hash.ts
2254
+ function fallbackHashText(text) {
2255
+ let hash = 0xcbf29ce484222325n;
2256
+ const prime = 0x100000001b3n;
2257
+ for (let i = 0; i < text.length; i += 1) {
2258
+ hash ^= BigInt(text.charCodeAt(i));
2259
+ hash = BigInt.asUintN(64, hash * prime);
2260
+ }
2261
+ return hash.toString(16).padStart(16, "0");
2262
+ }
2263
+ function normalizeProjectRoot(projectRoot, pathPort) {
2264
+ const resolved = pathPort ? pathPort.resolve(projectRoot) : projectRoot;
2265
+ return normalizePathKeyForCache(resolved);
2266
+ }
2267
+ function computeCacheProjectId(projectRoot, input = {}) {
2268
+ return (input.hashText ?? fallbackHashText)(normalizeProjectRoot(projectRoot, input.path)).slice(0, 16);
2269
+ }
2270
+ function computeCacheContentHash(text, hashText) {
2271
+ return (hashText ?? fallbackHashText)(text);
2272
+ }
2273
+
2274
+ // src/cache/io/helpers.ts
2275
+ function nowIso(runtime) {
2276
+ return new Date(runtime?.system.now() ?? Date.now()).toISOString();
2277
+ }
2278
+ function textByteLength(text, runtime) {
2279
+ return runtime.byteLength ? runtime.byteLength(text) : new TextEncoder().encode(text).length;
2280
+ }
2281
+ function readJsonFileWithLimit(filePath, maxBytes, runtime) {
2282
+ try {
2283
+ if (!assertSyncPortResult(runtime.fs.exists(filePath), "fs.exists", filePath)) return {};
2284
+ const kind = assertSyncPortResult(runtime.fs.statKind(filePath), "fs.statKind", filePath);
2285
+ if (kind !== "file") {
2286
+ return {
2287
+ warning: {
2288
+ code: "cache_io_error",
2289
+ message: "cache path is not a file; skipping",
2290
+ path: filePath
2291
+ }
2292
+ };
2293
+ }
2294
+ const knownSize = runtime.fileSize?.(filePath);
2295
+ if (knownSize !== void 0 && knownSize > maxBytes) {
2296
+ return {
2297
+ warning: {
2298
+ code: "cache_oversize",
2299
+ message: `cache file exceeds size limit (${String(knownSize)} > ${String(maxBytes)} bytes); skipping`,
2300
+ path: filePath
2301
+ }
2302
+ };
2303
+ }
2304
+ const raw = assertSyncPortResult(runtime.fs.readText(filePath), "fs.readText", filePath);
2305
+ const size = knownSize ?? textByteLength(raw, runtime);
2306
+ if (size > maxBytes) {
2307
+ return {
2308
+ warning: {
2309
+ code: "cache_oversize",
2310
+ message: `cache file exceeds size limit (${String(size)} > ${String(maxBytes)} bytes); skipping`,
2311
+ path: filePath
2312
+ }
2313
+ };
2314
+ }
2315
+ const parsed = tryParseJsonText(raw, { filePath, issueCode: "i18nprune.cache.json" });
2316
+ if (!parsed.ok) {
2317
+ return {
2318
+ warning: {
2319
+ code: "cache_malformed",
2320
+ message: parsed.error.message,
2321
+ path: filePath
2322
+ }
2323
+ };
2324
+ }
2325
+ return { data: parsed.data };
2326
+ } catch (err) {
2327
+ return {
2328
+ warning: {
2329
+ code: "cache_io_error",
2330
+ message: `cache read error: ${err instanceof Error ? err.message : String(err)}`,
2331
+ path: filePath
2332
+ }
2333
+ };
2334
+ }
2335
+ }
2336
+ function writeJsonAtomic(filePath, data, runtime) {
2337
+ try {
2338
+ const content = JSON.stringify(data, null, 2);
2339
+ if (runtime.writeTextAtomic) {
2340
+ runtime.writeTextAtomic(filePath, content);
2341
+ return void 0;
2342
+ }
2343
+ assertSyncPortResult(runtime.fs.mkdirp(runtime.path.dirname(filePath)), "fs.mkdirp", runtime.path.dirname(filePath));
2344
+ assertSyncPortResult(runtime.fs.writeText(filePath, content), "fs.writeText", filePath);
2345
+ return void 0;
2346
+ } catch (err) {
2347
+ return {
2348
+ code: "cache_io_error",
2349
+ message: `cache write error: ${err instanceof Error ? err.message : String(err)}`,
2350
+ path: filePath
2351
+ };
2352
+ }
2353
+ }
2354
+
2355
+ // src/shared/constants/cache.ts
2356
+ var CACHE_SCHEMA_VERSION = 1;
2357
+ var DEFAULT_CACHE_PROFILE_ID = "balanced";
2358
+ var CACHE_PROFILE_DEFAULTS = {
2359
+ safe: {
2360
+ rebuild: "full",
2361
+ fullRescanThresholdPercent: 10,
2362
+ mode: "readWrite"
2363
+ },
2364
+ balanced: {
2365
+ rebuild: "partial",
2366
+ fullRescanThresholdPercent: 40,
2367
+ mode: "readWrite"
2368
+ },
2369
+ fast: {
2370
+ rebuild: "partial",
2371
+ fullRescanThresholdPercent: 70,
2372
+ mode: "readWrite"
2373
+ }
2374
+ };
2375
+ var TRANSLATIONS_DIR = "translations";
2376
+ var MAX_PROJECTS_INDEX_BYTES = 2 * 1024 * 1024;
2377
+ var MAX_PROJECT_FILES_BYTES = 32 * 1024 * 1024;
2378
+ var MAX_ANALYSIS_BYTES = 16 * 1024 * 1024;
2379
+ var MAX_TRANSLATIONS_CACHE_BYTES = 32 * 1024 * 1024;
2380
+ var DEFAULT_HEAL_EVERY_RUNS = 20;
2381
+
2382
+ // src/cache/setup/policy.ts
2383
+ function isProjectCacheWritable(state) {
2384
+ return state.enabled && !state.readOnly;
2385
+ }
2386
+ function isFileRecordMap(value) {
2387
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2388
+ }
2389
+ function validateProjectFilesPayload(data, _filePath) {
2390
+ if (!data || typeof data !== "object") {
2391
+ return { ok: false, message: "root is not an object" };
2392
+ }
2393
+ const o = data;
2394
+ if (!isFileRecordMap(o.files)) {
2395
+ return { ok: false, message: "missing or invalid field: files" };
2396
+ }
2397
+ if (o.localeSegments !== void 0 && !isFileRecordMap(o.localeSegments)) {
2398
+ return { ok: false, message: "invalid field: localeSegments" };
2399
+ }
2400
+ if (o.localesLayout !== void 0) {
2401
+ const layout = o.localesLayout;
2402
+ if (!layout || typeof layout !== "object" || typeof layout.mode !== "string" || typeof layout.structure !== "string" || typeof layout.directory !== "string" || typeof layout.source !== "string") {
2403
+ return { ok: false, message: "invalid field: localesLayout" };
2404
+ }
2405
+ }
2406
+ if (o.version !== void 0 && o.version !== CACHE_SCHEMA_VERSION) {
2407
+ return { ok: false, message: `unsupported cache schema version: ${String(o.version)}` };
2408
+ }
2409
+ return { ok: true, files: data };
2410
+ }
2411
+ function validateProjectRunEnvelope(data, _filePath) {
2412
+ if (!data || typeof data !== "object") {
2413
+ return { ok: false, message: "root is not an object" };
2414
+ }
2415
+ const o = data;
2416
+ if (!("data" in o)) {
2417
+ return { ok: false, message: "missing required field: data" };
2418
+ }
2419
+ if (o.inputFilesEpoch !== void 0 && typeof o.inputFilesEpoch !== "string") {
2420
+ return { ok: false, message: "invalid field type: inputFilesEpoch" };
2421
+ }
2422
+ return { ok: true, run: data };
2423
+ }
2424
+ function loadProjectRunEnvelope(state, runtime) {
2425
+ const warnings = [];
2426
+ const filePath = state.analysisPath;
2427
+ const { data, warning } = readJsonFileWithLimit(filePath, MAX_ANALYSIS_BYTES, runtime);
2428
+ if (warning) {
2429
+ warnings.push({ ...warning, path: filePath });
2430
+ return { warnings };
2431
+ }
2432
+ if (data === void 0) return { warnings };
2433
+ const validated = validateProjectRunEnvelope(data);
2434
+ if (!validated.ok) {
2435
+ warnings.push({
2436
+ code: "cache_malformed",
2437
+ message: `cache analysis envelope invalid (${validated.message})`,
2438
+ path: filePath
2439
+ });
2440
+ return { warnings };
2441
+ }
2442
+ return { run: validated.run, warnings };
2443
+ }
2444
+ function tryDeleteCacheFile(runtime, filePath) {
2445
+ try {
2446
+ if (!assertSyncPortResult(runtime.fs.exists(filePath), "fs.exists", filePath)) return;
2447
+ assertSyncPortResult(runtime.fs.deleteFile(filePath), "fs.deleteFile", filePath);
2448
+ } catch {
2449
+ }
2450
+ }
2451
+
2452
+ // src/cache/io/projects.ts
2453
+ function defaultProjectsIndex(runtime) {
2454
+ return {
2455
+ version: CACHE_SCHEMA_VERSION,
2456
+ updatedAt: nowIso(runtime),
2457
+ projects: {},
2458
+ maintenance: { runCount: 0, healEveryRuns: DEFAULT_HEAL_EVERY_RUNS }
2459
+ };
2460
+ }
2461
+ function loadProjectsIndex(state, runtime) {
2462
+ const warnings = [];
2463
+ if (!state.enabled) return { index: defaultProjectsIndex(runtime), warnings };
2464
+ const { data, warning } = readJsonFileWithLimit(state.metaPath, MAX_PROJECTS_INDEX_BYTES, runtime);
2465
+ if (warning) warnings.push(warning);
2466
+ if (!data || typeof data !== "object" || typeof data.projects !== "object" || typeof data.maintenance !== "object" || typeof data.maintenance.runCount !== "number" || typeof data.maintenance.healEveryRuns !== "number") {
2467
+ return { index: defaultProjectsIndex(runtime), warnings };
2468
+ }
2469
+ return { index: data, warnings };
2470
+ }
2471
+ function saveProjectsIndex(state, index, runtime) {
2472
+ if (!state.enabled) return void 0;
2473
+ if (!isProjectCacheWritable(state)) {
2474
+ return {
2475
+ code: "cache_read_only",
2476
+ message: "cache is read-only; skipped persisting meta index",
2477
+ path: state.metaPath
2478
+ };
2479
+ }
2480
+ return writeJsonAtomic(state.metaPath, { ...index, updatedAt: nowIso(runtime), version: CACHE_SCHEMA_VERSION }, runtime);
2481
+ }
2482
+ function normalizeProjectRootKey(projectRoot) {
2483
+ const normalized = toPosixPath(projectRoot).normalize("NFC").replace(/\/+$/g, "");
2484
+ return `${normalized}/`;
2485
+ }
2486
+ function touchProjectIndex(state, index, runtime) {
2487
+ const next = {
2488
+ ...index,
2489
+ projects: { ...index.projects },
2490
+ maintenance: { ...index.maintenance }
2491
+ };
2492
+ const key = normalizeProjectRootKey(state.projectRoot);
2493
+ next.projects[key] = computeCacheProjectId(state.projectRoot, {
2494
+ path: runtime?.path,
2495
+ hashText: runtime?.hashText
2496
+ });
2497
+ next.maintenance.runCount += 1;
2498
+ return next;
2499
+ }
2500
+ function shouldHeal(index) {
2501
+ const every = Math.max(1, Math.trunc(index.maintenance.healEveryRuns || DEFAULT_HEAL_EVERY_RUNS));
2502
+ return index.maintenance.runCount % every === 0;
2503
+ }
2504
+ function maybeHealCacheIndex(state, index, runtime) {
2505
+ const warnings = [];
2506
+ if (!state.enabled || !shouldHeal(index)) {
2507
+ return { index, warnings, healed: false };
2508
+ }
2509
+ const next = {
2510
+ ...index,
2511
+ projects: { ...index.projects },
2512
+ maintenance: { ...index.maintenance, lastHealAt: nowIso(runtime) }
2513
+ };
2514
+ const projectsRoot = runtime.path.join(state.rootDir, "projects");
2515
+ for (const [projectRoot, id] of Object.entries(next.projects)) {
2516
+ const projectDir = runtime.path.join(projectsRoot, id);
2517
+ const kind = assertSyncPortResult(runtime.fs.statKind(projectDir), "fs.statKind", projectDir);
2518
+ if (kind !== "directory") {
2519
+ delete next.projects[projectRoot];
2520
+ }
2521
+ }
2522
+ const referencedIds = new Set(Object.values(next.projects));
2523
+ try {
2524
+ if (assertSyncPortResult(runtime.fs.exists(projectsRoot), "fs.exists", projectsRoot)) {
2525
+ const dirs = assertSyncPortResult(runtime.fs.listDir(projectsRoot), "fs.listDir", projectsRoot);
2526
+ for (const entry of dirs) {
2527
+ if (entry.kind !== "directory") continue;
2528
+ if (referencedIds.has(entry.name)) continue;
2529
+ const orphanDir = runtime.path.join(projectsRoot, entry.name);
2530
+ if (!runtime.deleteDir) continue;
2531
+ runtime.deleteDir(orphanDir);
2532
+ }
2533
+ }
2534
+ } catch (err) {
2535
+ warnings.push({
2536
+ code: "cache_io_error",
2537
+ message: `cache self-heal failed: ${err instanceof Error ? err.message : String(err)}`,
2538
+ path: projectsRoot
2539
+ });
2540
+ }
2541
+ return { index: next, warnings, healed: true };
2542
+ }
2543
+
2544
+ // src/cache/io/state.ts
2545
+ function defaultProjectFilesState(runtime) {
2546
+ return { version: CACHE_SCHEMA_VERSION, updatedAt: nowIso(runtime), files: {} };
2547
+ }
2548
+ function loadProjectFilesState(state, runtime) {
2549
+ const warnings = [];
2550
+ if (!state.enabled) return { files: defaultProjectFilesState(runtime), warnings };
2551
+ const { data, warning } = readJsonFileWithLimit(state.filesPath, MAX_PROJECT_FILES_BYTES, runtime);
2552
+ if (warning) warnings.push(warning);
2553
+ if (data === void 0) {
2554
+ return { files: defaultProjectFilesState(runtime), warnings };
2555
+ }
2556
+ const validated = validateProjectFilesPayload(data, state.filesPath);
2557
+ if (!validated.ok) {
2558
+ warnings.push({
2559
+ code: "cache_malformed",
2560
+ message: `cache files index invalid (${validated.message})`,
2561
+ path: state.filesPath
2562
+ });
2563
+ return { files: defaultProjectFilesState(runtime), warnings };
2564
+ }
2565
+ return { files: validated.files, warnings };
2566
+ }
2567
+ function saveProjectFilesState(state, files, runtime) {
2568
+ if (!state.enabled) return void 0;
2569
+ if (!isProjectCacheWritable(state)) {
2570
+ return {
2571
+ code: "cache_read_only",
2572
+ message: "cache is read-only; skipped persisting files index",
2573
+ path: state.filesPath
2574
+ };
2575
+ }
2576
+ return writeJsonAtomic(state.filesPath, { ...files, updatedAt: nowIso(runtime), version: CACHE_SCHEMA_VERSION }, runtime);
2577
+ }
2578
+ function loadProjectRunState(state, runtime) {
2579
+ if (!state.enabled) return { warnings: [] };
2580
+ return loadProjectRunEnvelope(state, runtime);
2581
+ }
2582
+ function saveProjectRunState(state, runtime, input) {
2583
+ if (!state.enabled) return void 0;
2584
+ if (!isProjectCacheWritable(state)) {
2585
+ return {
2586
+ code: "cache_read_only",
2587
+ message: "cache is read-only; skipped persisting analysis cache",
2588
+ path: state.analysisPath
2589
+ };
2590
+ }
2591
+ const payload = {
2592
+ version: CACHE_SCHEMA_VERSION,
2593
+ updatedAt: nowIso(runtime),
2594
+ projectId: state.projectId,
2595
+ data: input.data,
2596
+ ...input.inputFilesEpoch !== void 0 ? { inputFilesEpoch: input.inputFilesEpoch } : {}
2597
+ };
2598
+ return writeJsonAtomic(state.analysisPath, payload, runtime);
2599
+ }
2600
+
2601
+ // src/translator/cache/l2Io.ts
2602
+ function isTranslationCacheEntry(value) {
2603
+ if (!value || typeof value !== "object") return false;
2604
+ const o = value;
2605
+ return typeof o.text === "string" && typeof o.leafMeta === "object" && o.leafMeta !== null && typeof o.providerId === "string" && typeof o.createdAt === "string";
2606
+ }
2607
+ function isTranslationProviderId(value) {
2608
+ return value === "google" || value === "mymemory" || value === "deepl" || value === "libre" || value === "llm";
2609
+ }
2610
+ function validateTranslationLocaleCacheFile(data, _filePath) {
2611
+ if (!data || typeof data !== "object") {
2612
+ return { ok: false, message: "root is not an object" };
2613
+ }
2614
+ const o = data;
2615
+ if (typeof o.targetLang !== "string") {
2616
+ return { ok: false, message: "missing or invalid field: targetLang" };
2617
+ }
2618
+ if (typeof o.translateConfigEpoch !== "string") {
2619
+ return { ok: false, message: "missing or invalid field: translateConfigEpoch" };
2620
+ }
2621
+ if (typeof o.inputFilesEpoch !== "string") {
2622
+ return { ok: false, message: "missing or invalid field: inputFilesEpoch" };
2623
+ }
2624
+ if (!o.entries || typeof o.entries !== "object" || Array.isArray(o.entries)) {
2625
+ return { ok: false, message: "missing or invalid field: entries" };
2626
+ }
2627
+ for (const entry of Object.values(o.entries)) {
2628
+ if (!isTranslationCacheEntry(entry)) {
2629
+ return { ok: false, message: "invalid translation cache entry" };
2630
+ }
2631
+ if (!isTranslationProviderId(entry.providerId)) {
2632
+ return { ok: false, message: "invalid translation cache entry providerId" };
2633
+ }
2634
+ }
2635
+ if (o.version !== void 0 && o.version !== CACHE_SCHEMA_VERSION) {
2636
+ return { ok: false, message: `unsupported cache schema version: ${String(o.version)}` };
2637
+ }
2638
+ return { ok: true, state: data };
2639
+ }
2640
+ function loadTranslationLocaleCacheFile(filePath, runtime) {
2641
+ const warnings = [];
2642
+ const { data, warning } = readJsonFileWithLimit(filePath, MAX_TRANSLATIONS_CACHE_BYTES, runtime);
2643
+ if (warning) {
2644
+ warnings.push({ ...warning, path: filePath });
2645
+ return { warnings };
2646
+ }
2647
+ if (data === void 0) return { warnings };
2648
+ const validated = validateTranslationLocaleCacheFile(data);
2649
+ if (!validated.ok) {
2650
+ warnings.push({
2651
+ code: "cache_malformed",
2652
+ message: `cache translations store invalid (${validated.message})`,
2653
+ path: filePath
2654
+ });
2655
+ return { warnings };
2656
+ }
2657
+ return { locale: validated.state, warnings };
2658
+ }
2659
+
2660
+ // src/translator/cache/paths.ts
2661
+ function resolveTranslationsDir(state, runtime) {
2662
+ return runtime.path.join(state.projectDir, TRANSLATIONS_DIR);
2663
+ }
2664
+
2665
+ // src/translator/cache/maintenance.ts
2666
+ function prepareTranslationCacheLayout(state, runtime) {
2667
+ const warnings = [];
2668
+ if (!state.enabled) return warnings;
2669
+ const translationsDir = resolveTranslationsDir(state, runtime);
2670
+ try {
2671
+ assertSyncPortResult(runtime.fs.mkdirp(translationsDir), "fs.mkdirp", translationsDir);
2672
+ } catch (err) {
2673
+ warnings.push({
2674
+ code: "cache_dir_unavailable",
2675
+ message: `unable to create translations cache dir: ${err instanceof Error ? err.message : String(err)}`,
2676
+ path: translationsDir
2677
+ });
2678
+ return warnings;
2679
+ }
2680
+ return [...warnings, ...healTranslationCacheFiles(state, runtime)];
2681
+ }
2682
+ function healTranslationCacheFiles(state, runtime) {
2683
+ const warnings = [];
2684
+ if (!state.enabled) return warnings;
2685
+ const translationsDir = resolveTranslationsDir(state, runtime);
2686
+ let entries = [];
2687
+ try {
2688
+ const kind = assertSyncPortResult(runtime.fs.statKind(translationsDir), "fs.statKind", translationsDir);
2689
+ if (kind !== "directory") return warnings;
2690
+ entries = assertSyncPortResult(runtime.fs.listDir(translationsDir), "fs.listDir", translationsDir);
2691
+ } catch {
2692
+ return warnings;
2693
+ }
2694
+ for (const entry of entries) {
2695
+ if (entry.kind !== "file" || !entry.name.endsWith(".json")) continue;
2696
+ const filePath = runtime.path.join(translationsDir, entry.name);
2697
+ const loaded = loadTranslationLocaleCacheFile(filePath, runtime);
2698
+ warnings.push(...loaded.warnings);
2699
+ const invalid = loaded.warnings.some((w) => w.code === "cache_malformed" || w.code === "cache_oversize");
2700
+ if (invalid) {
2701
+ tryDeleteCacheFile(runtime, filePath);
2702
+ }
2703
+ }
2704
+ return warnings;
2705
+ }
2706
+
2707
+ // src/cache/setup/maintenance.ts
2708
+ function prepareCacheForRun(state, runtime) {
2709
+ const warnings = [];
2710
+ const loaded = loadProjectsIndex(state, runtime);
2711
+ warnings.push(...loaded.warnings);
2712
+ const touched = touchProjectIndex(state, loaded.index, runtime);
2713
+ const healed = maybeHealCacheIndex(state, touched, runtime);
2714
+ warnings.push(...healed.warnings);
2715
+ const saveWarn = saveProjectsIndex(state, healed.index, runtime);
2716
+ if (saveWarn) warnings.push(saveWarn);
2717
+ try {
2718
+ assertSyncPortResult(runtime.fs.mkdirp(state.projectDir), "fs.mkdirp", state.projectDir);
2719
+ } catch (err) {
2720
+ warnings.push({
2721
+ code: "cache_dir_unavailable",
2722
+ message: `unable to create cache project dir: ${err instanceof Error ? err.message : String(err)}`,
2723
+ path: state.projectDir
2724
+ });
2725
+ return { index: healed.index, warnings };
2726
+ }
2727
+ const fileState = loadProjectFilesState(state, runtime);
2728
+ warnings.push(...fileState.warnings);
2729
+ const analysisState = loadProjectRunState(state, runtime);
2730
+ warnings.push(...analysisState.warnings);
2731
+ warnings.push(...prepareTranslationCacheLayout(state, runtime));
2732
+ const hasInvalidFiles = fileState.warnings.some((w) => w.code === "cache_malformed" || w.code === "cache_oversize");
2733
+ const hasInvalidAnalysis = analysisState.warnings.some((w) => w.code === "cache_malformed" || w.code === "cache_oversize");
2734
+ if (hasInvalidFiles) {
2735
+ try {
2736
+ assertSyncPortResult(runtime.fs.deleteFile(state.filesPath), "fs.deleteFile", state.filesPath);
2737
+ } catch {
2738
+ }
2739
+ }
2740
+ if (hasInvalidAnalysis) {
2741
+ tryDeleteCacheFile(runtime, state.analysisPath);
2742
+ }
2743
+ try {
2744
+ assertSyncPortResult(runtime.fs.mkdirp(runtime.path.dirname(state.filesPath)), "fs.mkdirp", runtime.path.dirname(state.filesPath));
2745
+ } catch {
2746
+ }
2747
+ return { index: healed.index, warnings };
2748
+ }
2749
+
2750
+ // src/cache/engine.ts
2751
+ function computeInputFilesEpoch(files, hashText) {
2752
+ const keys = Object.keys(files).sort();
2753
+ const canonical = keys.map((k) => {
2754
+ const r = files[k];
2755
+ return `${k} ${r.hash} ${String(r.size)}
2756
+ `;
2757
+ }).join("");
2758
+ return computeCacheContentHash(canonical, hashText);
2759
+ }
2760
+ function diffProjectFiles(previous, current) {
2761
+ const added = [];
2762
+ const changed = [];
2763
+ const deleted = [];
2764
+ const unchanged = [];
2765
+ for (const [k, next] of Object.entries(current)) {
2766
+ const prev = previous[k];
2767
+ if (!prev) {
2768
+ added.push(k);
2769
+ continue;
2770
+ }
2771
+ if (prev.hash !== next.hash || prev.size !== next.size || prev.mtimeMs !== next.mtimeMs) {
2772
+ changed.push(k);
2773
+ } else {
2774
+ unchanged.push(k);
2775
+ }
2776
+ }
2777
+ for (const k of Object.keys(previous)) {
2778
+ if (!(k in current)) deleted.push(k);
2779
+ }
2780
+ return { added, changed, deleted, unchanged };
2781
+ }
2782
+
2783
+ // src/cache/localesLayout.ts
2784
+ function resolveCachedLocalesLayout(locales) {
2785
+ return {
2786
+ mode: locales.mode ?? "flat_file",
2787
+ structure: locales.structure ?? "locale_file",
2788
+ directory: locales.directory,
2789
+ source: locales.source
2790
+ };
2791
+ }
2792
+ function layoutMatches(a, b) {
2793
+ if (a === void 0) return false;
2794
+ return a.mode === b.mode && a.structure === b.structure && a.directory === b.directory && a.source === b.source;
2795
+ }
2796
+
2797
+ // src/shared/locales/diagnostics/structuralParity.ts
2798
+ function localeStructuralSlot(structure, relativePath) {
2799
+ if (structure === "locale_per_dir") {
2800
+ const slash = relativePath.indexOf("/");
2801
+ if (slash < 0) return null;
2802
+ const slot = relativePath.slice(slash + 1);
2803
+ return slot.length > 0 ? slot : null;
2804
+ }
2805
+ if (structure === "feature_bundle") {
2806
+ const slash = relativePath.lastIndexOf("/");
2807
+ if (slash < 0) return null;
2808
+ const slot = relativePath.slice(0, slash);
2809
+ return slot.length > 0 ? slot : null;
2810
+ }
2811
+ return null;
2812
+ }
2813
+ function slotsByLocale(structure, segments) {
2814
+ const byLocale = /* @__PURE__ */ new Map();
2815
+ for (const segment of segments) {
2816
+ const slot = localeStructuralSlot(structure, segment.relativePath);
2817
+ if (slot === null) continue;
2818
+ let set = byLocale.get(segment.locale);
2819
+ if (!set) {
2820
+ set = /* @__PURE__ */ new Set();
2821
+ byLocale.set(segment.locale, set);
2822
+ }
2823
+ set.add(slot);
2824
+ }
2825
+ return byLocale;
2826
+ }
2827
+ function pickReferenceLocale(byLocale, preferred) {
2828
+ if (preferred !== void 0 && byLocale.has(preferred)) return preferred;
2829
+ let best = null;
2830
+ let bestSize = -1;
2831
+ for (const [locale, slots] of byLocale) {
2832
+ if (slots.size > bestSize) {
2833
+ best = locale;
2834
+ bestSize = slots.size;
2835
+ }
2836
+ }
2837
+ return best;
2838
+ }
2839
+ function collectLocaleStructuralParityDiagnostics(input) {
2840
+ const { structure, segments } = input;
2841
+ if (structure !== "locale_per_dir" && structure !== "feature_bundle") {
2842
+ return [];
2843
+ }
2844
+ const byLocale = slotsByLocale(structure, segments);
2845
+ if (byLocale.size < 2) return [];
2846
+ const reference = pickReferenceLocale(byLocale, input.referenceLocale);
2847
+ if (reference === null) return [];
2848
+ const referenceSlots = byLocale.get(reference);
2849
+ if (!referenceSlots || referenceSlots.size === 0) return [];
2850
+ const diagnostics = [];
2851
+ for (const [locale, slots] of byLocale) {
2852
+ if (locale === reference) continue;
2853
+ for (const slot of referenceSlots) {
2854
+ if (!slots.has(slot)) {
2855
+ diagnostics.push({
2856
+ level: "warn",
2857
+ code: "locale_structure_slot_missing",
2858
+ message: `locale ${locale} is missing segment slot ${slot} (present for reference locale ${reference})`
2859
+ });
2860
+ }
2861
+ }
2862
+ for (const slot of slots) {
2863
+ if (!referenceSlots.has(slot)) {
2864
+ diagnostics.push({
2865
+ level: "warn",
2866
+ code: "locale_structure_slot_extra",
2867
+ message: `locale ${locale} has extra segment slot ${slot} (not present for reference locale ${reference})`
2868
+ });
2869
+ }
2870
+ }
2871
+ }
2872
+ diagnostics.sort((a, b) => a.message.localeCompare(b.message));
2873
+ return diagnostics;
2874
+ }
2875
+
2876
+ // src/shared/constants/locales.ts
2877
+ var MAX_LOCALE_SEGMENT_TREE_DEPTH = 16;
2878
+
2879
+ // src/shared/locales/enumerate/walkJsonTree.ts
2880
+ function posixRelative(pathApi, root, absolute) {
2881
+ let rel = pathApi.relative(root, absolute);
2882
+ if (rel.startsWith("..") || pathApi.isAbsolute(rel)) {
2883
+ rel = pathApi.basename(absolute);
2884
+ }
2885
+ return rel.replace(/\\/g, "/");
2886
+ }
2887
+ function walkLocaleJsonSegments(input) {
2888
+ const { fs, path, rootAbsolute, recursive } = input;
2889
+ const maxDepth = input.maxDepth ?? MAX_LOCALE_SEGMENT_TREE_DEPTH;
2890
+ const out = [];
2891
+ function visit(dirAbsolute, depth) {
2892
+ if (!existsRuntimeFsSync(dirAbsolute, fs)) return;
2893
+ const entries = listRuntimeFsDirSync(dirAbsolute, fs);
2894
+ for (const entry of entries) {
2895
+ const childAbsolute = path.join(dirAbsolute, entry.name);
2896
+ if (entry.kind === "file" && entry.name.endsWith(".json")) {
2897
+ out.push({
2898
+ absolutePath: childAbsolute,
2899
+ relativePath: posixRelative(path, rootAbsolute, childAbsolute)
2900
+ });
2901
+ } else if (recursive && entry.kind === "directory" && depth < maxDepth) {
2902
+ visit(childAbsolute, depth + 1);
2903
+ }
2904
+ }
2905
+ }
2906
+ visit(rootAbsolute, 0);
2907
+ return out;
2908
+ }
2909
+
2910
+ // src/shared/locales/enumerate/listLocaleSegments.ts
2911
+ function listLocaleSegments(input) {
2912
+ const diagnostics = [];
2913
+ const { layout, fs, path } = input;
2914
+ const recursive = layout.structure !== "locale_file";
2915
+ const walked = walkLocaleJsonSegments({
2916
+ fs,
2917
+ path,
2918
+ rootAbsolute: layout.directoryAbsolute,
2919
+ recursive
2920
+ });
2921
+ const segments = [];
2922
+ for (const segment of walked) {
2923
+ const locale = localeCodeForSegment(layout.structure, path, segment);
2924
+ if (locale === null) continue;
2925
+ segments.push({
2926
+ locale,
2927
+ relativePath: segment.relativePath,
2928
+ absolutePath: segment.absolutePath
2929
+ });
2930
+ }
2931
+ segments.sort((a, b) => {
2932
+ const byLocale = a.locale.localeCompare(b.locale);
2933
+ if (byLocale !== 0) return byLocale;
2934
+ return a.relativePath.localeCompare(b.relativePath);
2935
+ });
2936
+ diagnostics.push(
2937
+ ...collectLocaleStructuralParityDiagnostics({
2938
+ structure: layout.structure,
2939
+ segments
2940
+ })
2941
+ );
2942
+ return { segments, diagnostics };
2943
+ }
2944
+
2945
+ // src/shared/locales/layout/requireStructure.ts
2946
+ function localesMode(config) {
2947
+ return config.mode ?? "flat_file";
2948
+ }
2949
+ function resolveLocalesStructure(config) {
2950
+ if (config.structure !== void 0) {
2951
+ return config.structure;
2952
+ }
2953
+ if (localesMode(config) === "flat_file") {
2954
+ return "locale_file";
2955
+ }
2956
+ throw new Error("locales.structure is required when locales.mode is locale_directory");
2957
+ }
2958
+
2959
+ // src/shared/locales/layout/resolveLayout.ts
2960
+ function resolveLocalesLayout(config, directoryAbsolute) {
2961
+ const mode = localesMode(config);
2962
+ const structure = resolveLocalesStructure(config);
2963
+ return { mode, structure, directoryAbsolute, config };
2964
+ }
2965
+ function resolveLocalesLayoutFromContext(ctx) {
2966
+ return resolveLocalesLayout(ctx.config.locales, ctx.paths.localesDir);
2967
+ }
2968
+ function isLocalesLayoutReadSupported(layout) {
2969
+ return layout.mode === "flat_file" && layout.structure === "locale_file" || layout.mode === "locale_directory" && (layout.structure === "locale_per_dir" || layout.structure === "feature_bundle");
2970
+ }
2971
+ function isLocalesLayoutWriteSupported(layout) {
2972
+ return layout.mode === "flat_file" && layout.structure === "locale_file" || layout.mode === "locale_directory" && (layout.structure === "locale_per_dir" || layout.structure === "feature_bundle");
2973
+ }
2974
+
2975
+ // src/cache/trackedFiles.ts
2976
+ var SYNTHETIC_SOURCE_LOCALE_KEY = "__source_locale__";
2977
+ function omitSyntheticSourceKey(files) {
2978
+ if (!(SYNTHETIC_SOURCE_LOCALE_KEY in files)) return files;
2979
+ const out = { ...files };
2980
+ delete out[SYNTHETIC_SOURCE_LOCALE_KEY];
2981
+ return out;
2982
+ }
2983
+ function mergeTrackedFileMaps(files, localeSegments) {
2984
+ return { ...files, ...localeSegments };
2985
+ }
2986
+ function hashFileRecord(absPath, runtime, now) {
2987
+ const content = readRuntimeFsTextSync(absPath, runtime.fs);
2988
+ return {
2989
+ hash: computeCacheContentHash(content, runtime.hashText),
2990
+ size: textByteLength(content, runtime),
2991
+ mtimeMs: 0,
2992
+ updatedAt: now
2993
+ };
2994
+ }
2995
+ function buildSrcFileRecords(input) {
2996
+ const paths = listSourceFiles({ fs: input.runtime.fs, path: input.runtime.path }, input.srcRoot, input.exclude);
2997
+ const now = new Date(input.runtime.system.now()).toISOString();
2998
+ const out = {};
2999
+ for (const absPath of paths) {
3000
+ const rel = input.runtime.path.relative(input.srcRoot, absPath).replace(/\\/g, "/");
3001
+ out[rel] = hashFileRecord(absPath, input.runtime, now);
3002
+ }
3003
+ return out;
3004
+ }
3005
+ function buildLocaleSegmentRecords(input) {
3006
+ const layout = resolveLocalesLayout(input.locales, input.localesDir);
3007
+ const { segments } = listLocaleSegments({ layout, fs: input.runtime.fs, path: input.runtime.path });
3008
+ const now = new Date(input.runtime.system.now()).toISOString();
3009
+ const out = {};
3010
+ for (const segment of segments) {
3011
+ out[segment.relativePath] = hashFileRecord(segment.absolutePath, input.runtime, now);
3012
+ }
3013
+ return out;
3014
+ }
3015
+ function buildTrackedProjectFilesCurrent(input) {
3016
+ const localesLayout = resolveCachedLocalesLayout(input.locales);
3017
+ const files = input.scanSrc ? buildSrcFileRecords(input) : omitSyntheticSourceKey(input.reuseSrcFiles ?? {});
3018
+ const localeSegments = buildLocaleSegmentRecords(input);
3019
+ return {
3020
+ files,
3021
+ localeSegments,
3022
+ localesLayout,
3023
+ merged: mergeTrackedFileMaps(files, localeSegments)
3024
+ };
3025
+ }
3026
+
3027
+ // src/types/cache/filesIndex.ts
3028
+ function filesIndexIsUsable(status) {
3029
+ return status.kind === "ok";
3030
+ }
3031
+
3032
+ // src/cache/deltaClassify.ts
3033
+ function normalizeRelPath(path) {
3034
+ return path.replace(/\\/g, "/");
3035
+ }
3036
+ function unionKeys(a, b) {
3037
+ return /* @__PURE__ */ new Set([...a, ...b]);
3038
+ }
3039
+ function classifyCacheFileDelta(input) {
3040
+ const src = { added: [], changed: [], deleted: [] };
3041
+ const sourceLocale = [];
3042
+ const targetLocale = [];
3043
+ const sourceKey = normalizeRelPath(input.sourceLocaleSegmentKey);
3044
+ const knownSrcKeys = unionKeys(input.currentSrcFileKeys, input.baselineSrcFileKeys);
3045
+ const knownLocaleKeys = unionKeys(input.currentLocaleSegmentKeys, input.baselineLocaleSegmentKeys);
3046
+ const classifyPath = (path, kind) => {
3047
+ if (knownSrcKeys.has(path)) {
3048
+ src[kind].push(path);
3049
+ return;
3050
+ }
3051
+ if (knownLocaleKeys.has(path)) {
3052
+ if (path === sourceKey) sourceLocale.push(path);
3053
+ else targetLocale.push(path);
3054
+ }
3055
+ };
3056
+ for (const path of input.delta.added.map(normalizeRelPath)) {
3057
+ classifyPath(path, "added");
3058
+ }
3059
+ for (const path of input.delta.changed.map(normalizeRelPath)) {
3060
+ classifyPath(path, "changed");
3061
+ }
3062
+ for (const path of input.delta.deleted.map(normalizeRelPath)) {
3063
+ classifyPath(path, "deleted");
3064
+ }
3065
+ const layoutChanged = filesIndexIsUsable(input.filesIndexStatus) && !layoutMatches(input.previousLayout, input.currentLayout);
3066
+ return {
3067
+ src,
3068
+ sourceLocale,
3069
+ targetLocale,
3070
+ layoutChanged,
3071
+ filesIndexStatus: input.filesIndexStatus
3072
+ };
3073
+ }
3074
+ function srcDeltaIsEmpty(src) {
3075
+ return src.added.length + src.changed.length + src.deleted.length === 0;
3076
+ }
3077
+ function countSrcDeltaAffected(src) {
3078
+ return src.added.length + src.changed.length + src.deleted.length;
3079
+ }
3080
+
3081
+ // src/cache/filesIndexStatus.ts
3082
+ function isDefaultEmptyIndex(prev) {
3083
+ const noSrc = Object.keys(prev.files).length === 0;
3084
+ const noLocales = !prev.localeSegments || Object.keys(prev.localeSegments).length === 0;
3085
+ const noLayout = prev.localesLayout === void 0;
3086
+ return noSrc && noLocales && noLayout;
3087
+ }
3088
+ function resolveFilesIndexStatus(input) {
3089
+ const indexWarnings = input.warnings.filter((w) => w.path === void 0 || w.path === input.filesPath);
3090
+ if (indexWarnings.some((w) => w.code === "cache_malformed" || w.code === "cache_oversize")) {
3091
+ return { kind: "malformed" };
3092
+ }
3093
+ const exists = input.runtime.fs.exists(input.filesPath);
3094
+ if (!exists) {
3095
+ return { kind: "missing" };
3096
+ }
3097
+ if (isDefaultEmptyIndex(input.prev)) {
3098
+ return { kind: "empty" };
3099
+ }
3100
+ return { kind: "ok" };
3101
+ }
3102
+
3103
+ // src/cache/resolveConfig.ts
3104
+ function clampThresholdPercent(value) {
3105
+ if (value < 0) return 0;
3106
+ if (value > 100) return 100;
3107
+ return value;
3108
+ }
3109
+ function resolveCacheConfig(cache) {
3110
+ const profileId = cache?.profile ?? DEFAULT_CACHE_PROFILE_ID;
3111
+ const profile = CACHE_PROFILE_DEFAULTS[profileId] ?? CACHE_PROFILE_DEFAULTS[DEFAULT_CACHE_PROFILE_ID];
3112
+ const threshold = cache?.fullRescanThresholdPercent !== void 0 ? clampThresholdPercent(cache.fullRescanThresholdPercent) : profile.fullRescanThresholdPercent;
3113
+ return {
3114
+ enabled: cache?.enabled ?? true,
3115
+ profile: profileId,
3116
+ mode: cache?.mode ?? profile.mode,
3117
+ rebuild: cache?.rebuild ?? profile.rebuild,
3118
+ fullRescanThresholdPercent: threshold,
3119
+ ...cache?.dir !== void 0 ? { dir: cache.dir } : {}
3120
+ };
3121
+ }
3122
+ function resolveCacheRebuildConfig(cache) {
3123
+ const resolved = resolveCacheConfig(cache);
3124
+ return {
3125
+ rebuild: resolved.rebuild,
3126
+ fullRescanThresholdPercent: resolved.fullRescanThresholdPercent
3127
+ };
3128
+ }
3129
+
3130
+ // src/cache/rebuildPolicy.ts
3131
+ function filesIndexRebuildReason(status) {
3132
+ switch (status.kind) {
3133
+ case "missing":
3134
+ return "files_index_missing";
3135
+ case "malformed":
3136
+ return "files_index_malformed";
3137
+ case "empty":
3138
+ return "files_index_empty";
3139
+ default:
3140
+ return "files_index_missing";
3141
+ }
3142
+ }
3143
+ function decideAnalysisRebuild(input) {
3144
+ if (input.config.rebuild === "full") {
3145
+ return { strategy: "full", reason: "config_rebuild_full" };
3146
+ }
3147
+ if (!input.hasPrevious) {
3148
+ if (!filesIndexIsUsable(input.classified.filesIndexStatus)) {
3149
+ return { strategy: "full", reason: filesIndexRebuildReason(input.classified.filesIndexStatus) };
3150
+ }
3151
+ return { strategy: "full", reason: "no_previous_cache" };
3152
+ }
3153
+ if (!filesIndexIsUsable(input.classified.filesIndexStatus)) {
3154
+ return { strategy: "full", reason: "files_index_stale" };
3155
+ }
3156
+ if (input.classified.layoutChanged) {
3157
+ return { strategy: "full", reason: "layout_changed" };
3158
+ }
3159
+ if (input.classified.sourceLocale.length > 0) {
3160
+ const sourceOnly = input.classified.targetLocale.length === 0 && srcDeltaIsEmpty(input.classified.src);
3161
+ if (sourceOnly) {
3162
+ return { strategy: "partial", reason: "source_locale_partial" };
3163
+ }
3164
+ return { strategy: "full", reason: "source_locale_changed" };
3165
+ }
3166
+ if (input.classified.targetLocale.length > 0 && srcDeltaIsEmpty(input.classified.src)) {
3167
+ return { strategy: "reuse", reason: "target_locale_only" };
3168
+ }
3169
+ if (srcDeltaIsEmpty(input.classified.src)) {
3170
+ return { strategy: "full", reason: "locale_or_non_src_changed" };
3171
+ }
3172
+ const srcAffected = countSrcDeltaAffected(input.classified.src);
3173
+ const trackedSrcCount = input.trackedSrcCount;
3174
+ const threshold = input.config.fullRescanThresholdPercent;
3175
+ const percent = trackedSrcCount > 0 ? srcAffected / trackedSrcCount * 100 : 100;
3176
+ if (percent >= threshold) {
3177
+ return {
3178
+ strategy: "full",
3179
+ reason: "src_threshold",
3180
+ thresholdPercent: threshold,
3181
+ srcAffected,
3182
+ trackedSrcCount
3183
+ };
3184
+ }
3185
+ return {
3186
+ strategy: "partial",
3187
+ reason: "src_partial",
3188
+ srcAffected,
3189
+ trackedSrcCount
3190
+ };
3191
+ }
3192
+
3193
+ // src/cache/dispatch.ts
3194
+ function assertLocalesInput(input) {
3195
+ if (input.localesDir === void 0 || input.locales === void 0) {
3196
+ throw new Error("cache dispatch requires localesDir and locales");
3197
+ }
3198
+ }
3199
+ function baselineMergedFromDisk(prev) {
3200
+ return mergeTrackedFileMaps(omitSyntheticSourceKey(prev.files), prev.localeSegments ?? {});
3201
+ }
3202
+ function resolveTrackedCurrent(input, prev) {
3203
+ const locales = input.locales;
3204
+ const localesDir = input.localesDir;
3205
+ const hasCachedLayout = prev.localesLayout !== void 0;
3206
+ const layoutMatch = layoutMatches(prev.localesLayout, resolveCachedLocalesLayout(locales));
3207
+ const scanSrc = !hasCachedLayout || layoutMatch;
3208
+ return buildTrackedProjectFilesCurrent({
3209
+ runtime: input.runtime,
3210
+ srcRoot: input.srcRoot,
3211
+ exclude: input.exclude,
3212
+ localesDir,
3213
+ locales,
3214
+ scanSrc,
3215
+ ...scanSrc ? {} : { reuseSrcFiles: omitSyntheticSourceKey(prev.files) }
3216
+ });
3217
+ }
3218
+ function sourceLocaleSegmentKey(input) {
3219
+ return input.runtime.path.relative(input.localesDir, input.sourceLocalePath).replace(/\\/g, "/");
3220
+ }
3221
+ function buildProducerContext(input) {
3222
+ const currentSrcFileKeys = new Set(Object.keys(omitSyntheticSourceKey(input.tracked.files)));
3223
+ const baselineSrcFileKeys = new Set(Object.keys(omitSyntheticSourceKey(input.prev.files)));
3224
+ const currentLocaleSegmentKeys = new Set(Object.keys(input.tracked.localeSegments));
3225
+ const baselineLocaleSegmentKeys = new Set(Object.keys(input.prev.localeSegments ?? {}));
3226
+ const classified = classifyCacheFileDelta({
3227
+ delta: input.delta,
3228
+ currentSrcFileKeys,
3229
+ baselineSrcFileKeys,
3230
+ currentLocaleSegmentKeys,
3231
+ baselineLocaleSegmentKeys,
3232
+ sourceLocaleSegmentKey: sourceLocaleSegmentKey(input.input),
3233
+ previousLayout: input.prev.localesLayout,
3234
+ currentLayout: input.tracked.localesLayout,
3235
+ filesIndexStatus: input.filesIndexStatus
3236
+ });
3237
+ const rebuildConfig = input.input.rebuildConfig ?? resolveCacheRebuildConfig({ profile: "balanced" });
3238
+ const trackedSrcCount = currentSrcFileKeys.size;
3239
+ const analysisRebuild = {
3240
+ ...decideAnalysisRebuild({
3241
+ config: rebuildConfig,
3242
+ classified,
3243
+ hasPrevious: input.previous !== void 0,
3244
+ trackedSrcCount
3245
+ }),
3246
+ srcDelta: classified.src
3247
+ };
3248
+ return {
3249
+ delta: input.delta,
3250
+ classified,
3251
+ previous: input.previous,
3252
+ trackedSrcCount,
3253
+ rebuildConfig,
3254
+ analysisRebuild
3255
+ };
3256
+ }
3257
+ function persistFilesAndRunState(input) {
3258
+ const saveFilesWarn = saveProjectFilesState(input.state, input.nextIndex, input.runtime);
3259
+ if (saveFilesWarn) input.warnings.push(saveFilesWarn);
3260
+ const saveRunWarn = saveProjectRunState(input.state, input.runtime, {
3261
+ data: input.data,
3262
+ inputFilesEpoch: input.inputFilesEpoch
3263
+ });
3264
+ if (saveRunWarn) input.warnings.push(saveRunWarn);
3265
+ }
3266
+ function getOrBuildCachedProjectData(input) {
3267
+ const warnings = [];
3268
+ const state = input.state;
3269
+ const paths = {
3270
+ meta: state.metaPath,
3271
+ files: state.filesPath,
3272
+ analysis: state.analysisPath,
3273
+ projectDir: state.projectDir
3274
+ };
3275
+ const rebuildConfig = input.rebuildConfig ?? resolveCacheRebuildConfig({ profile: "balanced" });
3276
+ if (!state.enabled) {
3277
+ return {
3278
+ data: input.producer(),
3279
+ cache: {
3280
+ status: state.reason === "cli_no_cache" ? "bypass" : "disabled",
3281
+ reason: "no_cache",
3282
+ warnings,
3283
+ paths
3284
+ }
3285
+ };
3286
+ }
3287
+ const prepared = prepareCacheForRun(state, input.runtime);
3288
+ warnings.push(...prepared.warnings);
3289
+ const prevFiles = loadProjectFilesState(state, input.runtime);
3290
+ warnings.push(...prevFiles.warnings);
3291
+ const prev = prevFiles.files;
3292
+ const filesIndexStatus = resolveFilesIndexStatus({
3293
+ prev,
3294
+ warnings: prevFiles.warnings,
3295
+ filesPath: state.filesPath,
3296
+ runtime: input.runtime
3297
+ });
3298
+ assertLocalesInput(input);
3299
+ const tracked = resolveTrackedCurrent(input, prev);
3300
+ const currentFiles = tracked.merged;
3301
+ const baseline = input.baselineFiles ?? baselineMergedFromDisk(prev);
3302
+ const delta = diffProjectFiles(baseline, currentFiles);
3303
+ const hasFileChanges = delta.added.length + delta.changed.length + delta.deleted.length > 0;
3304
+ const inputFilesEpoch = computeInputFilesEpoch(currentFiles, input.runtime.hashText);
3305
+ const prevRun = loadProjectRunState(state, input.runtime);
3306
+ warnings.push(...prevRun.warnings);
3307
+ let previous;
3308
+ if (prevRun.run?.data !== void 0 && input.parseCachedData) {
3309
+ const parsedPrevious = input.parseCachedData(prevRun.run.data);
3310
+ if (parsedPrevious.ok) previous = parsedPrevious.data;
3311
+ }
3312
+ const filesIndexRecoverable = !filesIndexIsUsable(filesIndexStatus) && hasFileChanges && previous !== void 0 && prevRun.run?.inputFilesEpoch === inputFilesEpoch;
3313
+ if (filesIndexRecoverable && previous !== void 0) {
3314
+ const nextIndex2 = {
3315
+ ...prev,
3316
+ files: tracked.files,
3317
+ localeSegments: tracked.localeSegments,
3318
+ localesLayout: tracked.localesLayout
3319
+ };
3320
+ persistFilesAndRunState({
3321
+ state,
3322
+ runtime: input.runtime,
3323
+ nextIndex: nextIndex2,
3324
+ data: previous,
3325
+ inputFilesEpoch,
3326
+ warnings
3327
+ });
3328
+ return {
3329
+ data: previous,
3330
+ cache: {
3331
+ status: "hit",
3332
+ reason: "files_index_recovered",
3333
+ warnings,
3334
+ delta,
3335
+ paths,
3336
+ analysisRebuild: { strategy: "reuse", reason: "files_index_recovered" },
3337
+ filesIndexStatus
3338
+ }
3339
+ };
3340
+ }
3341
+ let missReason = hasFileChanges ? "files_changed" : "run_missing";
3342
+ let inputFilesEpochDebug;
3343
+ if (!hasFileChanges && prevRun.run?.data !== void 0) {
3344
+ if (prevRun.run.inputFilesEpoch !== inputFilesEpoch) {
3345
+ missReason = "run_binding_stale";
3346
+ inputFilesEpochDebug = { cached: prevRun.run.inputFilesEpoch, current: inputFilesEpoch };
3347
+ } else {
3348
+ const parsed = input.parseCachedData ? input.parseCachedData(prevRun.run.data) : { ok: true, data: prevRun.run.data };
3349
+ if (parsed.ok) {
3350
+ return {
3351
+ data: parsed.data,
3352
+ cache: {
3353
+ status: "hit",
3354
+ reason: "cache_hit",
3355
+ warnings,
3356
+ delta,
3357
+ paths
3358
+ }
3359
+ };
3360
+ }
3361
+ missReason = "run_invalid";
3362
+ }
3363
+ }
3364
+ const producerCtx = buildProducerContext({
3365
+ input: { ...input, rebuildConfig },
3366
+ delta,
3367
+ tracked,
3368
+ prev,
3369
+ previous,
3370
+ filesIndexStatus
3371
+ });
3372
+ if (previous !== void 0 && producerCtx.analysisRebuild?.strategy === "reuse" && producerCtx.analysisRebuild.reason === "target_locale_only") {
3373
+ const nextIndex2 = {
3374
+ ...prev,
3375
+ files: tracked.files,
3376
+ localeSegments: tracked.localeSegments,
3377
+ localesLayout: tracked.localesLayout
3378
+ };
3379
+ persistFilesAndRunState({
3380
+ state,
3381
+ runtime: input.runtime,
3382
+ nextIndex: nextIndex2,
3383
+ data: previous,
3384
+ inputFilesEpoch,
3385
+ warnings
3386
+ });
3387
+ return {
3388
+ data: previous,
3389
+ cache: {
3390
+ status: "miss",
3391
+ reason: missReason,
3392
+ warnings,
3393
+ delta,
3394
+ paths,
3395
+ analysisRebuild: producerCtx.analysisRebuild
3396
+ }
3397
+ };
3398
+ }
3399
+ const fresh = input.producer(producerCtx);
3400
+ const nextIndex = {
3401
+ ...prev,
3402
+ files: tracked.files,
3403
+ localeSegments: tracked.localeSegments,
3404
+ localesLayout: tracked.localesLayout
3405
+ };
3406
+ persistFilesAndRunState({
3407
+ state,
3408
+ runtime: input.runtime,
3409
+ nextIndex,
3410
+ data: fresh,
3411
+ inputFilesEpoch,
3412
+ warnings
3413
+ });
3414
+ return {
3415
+ data: fresh,
3416
+ cache: {
3417
+ status: "miss",
3418
+ reason: missReason,
3419
+ warnings,
3420
+ delta,
3421
+ paths,
3422
+ analysisRebuild: producerCtx.analysisRebuild,
3423
+ filesIndexStatus: filesIndexIsUsable(filesIndexStatus) ? void 0 : filesIndexStatus,
3424
+ ...inputFilesEpochDebug !== void 0 ? { inputFilesEpochDebug } : {}
3425
+ }
3426
+ };
3427
+ }
3428
+
3429
+ // src/shared/run/index.ts
3430
+ function emitRunEvent(emit, event) {
3431
+ if (!emit) return;
3432
+ try {
3433
+ emit(event);
3434
+ } catch {
3435
+ }
3436
+ }
3437
+ function emitRunMessage(emit, input) {
3438
+ emitRunEvent(emit, {
3439
+ type: "run.message",
3440
+ op: input.op,
3441
+ runId: input.runId,
3442
+ at: input.at ?? nowMs(),
3443
+ level: input.level,
3444
+ ...input.channel !== void 0 ? { channel: input.channel } : {},
3445
+ message: input.message,
3446
+ ...input.target !== void 0 ? { target: input.target } : {},
3447
+ ...input.path !== void 0 ? { path: input.path } : {},
3448
+ ...input.data !== void 0 ? { data: input.data } : {}
3449
+ });
3450
+ }
3451
+ function nowMs() {
3452
+ return Date.now();
3453
+ }
3454
+
3455
+ // src/cache/events.ts
3456
+ function describeCacheInvalidation(reason) {
3457
+ switch (reason) {
3458
+ case "files_changed":
3459
+ return "source files changed";
3460
+ case "files_index_recovered":
3461
+ return "files index rebuilt; analysis reused (project files unchanged)";
3462
+ case "run_binding_stale":
3463
+ return "stale (source files changed since last cache write)";
3464
+ case "run_invalid":
3465
+ return "cached data failed validation";
3466
+ case "run_missing":
3467
+ return "no cached data";
3468
+ case "cache_unavailable":
3469
+ return "cache unavailable";
3470
+ case "no_cache":
3471
+ return "cache disabled";
3472
+ case "cache_hit":
3473
+ case "producer_succeeded":
3474
+ return void 0;
3475
+ default:
3476
+ return void 0;
3477
+ }
3478
+ }
3479
+ function emitCacheDetail(input) {
3480
+ emitRunMessage(input.emit, {
3481
+ op: input.op,
3482
+ runId: input.runId,
3483
+ channel: "cache",
3484
+ level: "detail",
3485
+ message: input.message
3486
+ });
3487
+ }
3488
+ function emitCacheDeltaFiles(input) {
3489
+ const limit = 12;
3490
+ for (const file of input.files.slice(0, limit)) {
3491
+ emitCacheDetail({ ...input, message: ` ${input.kind}: ${file}` });
3492
+ }
3493
+ if (input.files.length > limit) {
3494
+ emitCacheDetail({ ...input, message: ` ${input.kind}: ... ${String(input.files.length - limit)} more` });
3495
+ }
3496
+ }
3497
+ function emitCacheDispatchMessages(input) {
3498
+ emitRunMessage(input.emit, {
3499
+ op: input.op,
3500
+ runId: input.runId,
3501
+ channel: "cache",
3502
+ level: "info",
3503
+ message: `${input.label} cache ${input.cache.status} (${input.cache.reason})`
3504
+ });
3505
+ const invalidation = describeCacheInvalidation(input.cache.reason);
3506
+ if (invalidation) {
3507
+ emitCacheDetail({ ...input, message: ` invalidated: ${invalidation}` });
3508
+ }
3509
+ if (input.cache.paths) {
3510
+ emitCacheDetail({ ...input, message: ` meta: ${input.cache.paths.meta}` });
3511
+ emitCacheDetail({ ...input, message: ` project: ${input.cache.paths.projectDir}` });
3512
+ emitCacheDetail({ ...input, message: ` files: ${input.cache.paths.files}` });
3513
+ emitCacheDetail({ ...input, message: ` analysis: ${input.cache.paths.analysis}` });
3514
+ }
3515
+ if (input.cache.delta) {
3516
+ const d = input.cache.delta;
3517
+ emitCacheDetail({
3518
+ ...input,
3519
+ message: ` file delta: +${String(d.added.length)} ~${String(d.changed.length)} -${String(d.deleted.length)} =${String(d.unchanged.length)}`
3520
+ });
3521
+ emitCacheDeltaFiles({ ...input, kind: "added", files: d.added });
3522
+ emitCacheDeltaFiles({ ...input, kind: "changed", files: d.changed });
3523
+ emitCacheDeltaFiles({ ...input, kind: "deleted", files: d.deleted });
3524
+ }
3525
+ if (input.cache.analysisRebuild) {
3526
+ const r = input.cache.analysisRebuild;
3527
+ if (r.strategy === "reuse") {
3528
+ const detail = r.reason === "target_locale_only" ? "target locale only (reusing analysis.json)" : "reusing analysis.json";
3529
+ emitCacheDetail({ ...input, message: ` analysis rebuild: skipped (${detail})` });
3530
+ } else if (r.strategy === "partial") {
3531
+ if (r.reason === "source_locale_partial") {
3532
+ emitCacheDetail({ ...input, message: " analysis rebuild: partial (source locale only, missingKeys updated)" });
3533
+ } else {
3534
+ const src = r.srcDelta;
3535
+ const changed = src?.changed.length ?? 0;
3536
+ const added = src?.added.length ?? 0;
3537
+ const deleted = src?.deleted.length ?? 0;
3538
+ emitCacheDetail({
3539
+ ...input,
3540
+ message: ` analysis rebuild: partial (${String(changed)} changed, ${String(added)} added, ${String(deleted)} deleted)`
3541
+ });
3542
+ }
3543
+ } else {
3544
+ let detail = "config rebuild=full";
3545
+ if (r.reason === "src_threshold") {
3546
+ const pct = r.trackedSrcCount && r.trackedSrcCount > 0 ? Math.round((r.srcAffected ?? 0) / r.trackedSrcCount * 100) : 100;
3547
+ detail = `threshold ${String(pct)}% (limit ${String(r.thresholdPercent ?? 40)}%)`;
3548
+ } else if (r.reason === "layout_changed") {
3549
+ detail = "layout changed";
3550
+ } else if (r.reason === "source_locale_changed") {
3551
+ detail = "source locale changed (with other deltas)";
3552
+ } else if (r.reason === "locale_or_non_src_changed") {
3553
+ detail = "locale or non-src delta";
3554
+ } else if (r.reason === "no_previous_cache") {
3555
+ detail = "no previous analysis cache";
3556
+ } else if (r.reason === "files_index_missing") {
3557
+ detail = "files.json missing";
3558
+ } else if (r.reason === "files_index_malformed") {
3559
+ detail = "files.json invalid or oversized";
3560
+ } else if (r.reason === "files_index_empty") {
3561
+ detail = "files.json empty";
3562
+ } else if (r.reason === "files_index_stale") {
3563
+ detail = "files index unusable and project files changed since last analysis";
3564
+ }
3565
+ emitCacheDetail({ ...input, message: ` analysis rebuild: full (${detail})` });
3566
+ }
3567
+ }
3568
+ if (input.cache.filesIndexStatus !== void 0 && input.cache.filesIndexStatus.kind !== "ok") {
3569
+ const kind = input.cache.filesIndexStatus.kind;
3570
+ 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";
3571
+ emitCacheDetail({ ...input, message: ` files index: ${note}` });
3572
+ }
3573
+ for (const warn of input.cache.warnings) {
3574
+ const path = warn.path ? ` (${warn.path})` : "";
3575
+ emitRunMessage(input.emit, {
3576
+ op: input.op,
3577
+ runId: input.runId,
3578
+ channel: "cache",
3579
+ level: "info",
3580
+ message: `warning: ${warn.message}${path}`
3581
+ });
3582
+ }
3583
+ }
3584
+
3585
+ // src/validate/missingLiterals.ts
3586
+ function logicalPathsFromSourceLeaves(leaves) {
3587
+ return new Set(leaves.map((l) => l.path));
3588
+ }
3589
+ function computeMissingLiteralKeysFromLeaves(sourceLeaves, resolvedKeys) {
3590
+ const keySet = logicalPathsFromSourceLeaves(sourceLeaves);
3591
+ return [...resolvedKeys].filter((k) => !keySet.has(k)).sort(compareDottedPathDepth);
3592
+ }
3593
+ function compareDottedPathDepth(a, b) {
3594
+ const da = splitPath(a).length;
3595
+ const db = splitPath(b).length;
3596
+ if (da !== db) return da - db;
3597
+ return a.localeCompare(b);
3598
+ }
3599
+
3600
+ // src/shared/locales/read/flatFileSurface.ts
3601
+ function readFlatLocaleJsonSurface(input) {
3602
+ const diagnostics = [];
3603
+ const emit = (d) => {
3604
+ diagnostics.push(d);
3605
+ };
3606
+ try {
3607
+ const text = readRuntimeFsTextSync(input.absoluteFile, input.fs);
3608
+ let json;
3609
+ try {
3610
+ json = parseJsonText(text, {
3611
+ filePath: input.absoluteFile,
3612
+ code: "IO",
3613
+ issueCode: ISSUE_IO_READ_FAILED
3614
+ });
3615
+ } catch (e) {
3616
+ const message = e instanceof Error ? e.message : String(e);
3617
+ emit({ level: "error", code: "locale_json_parse_failed", message, path: input.absoluteFile });
3618
+ return { ok: false, leaves: [], diagnostics };
3619
+ }
3620
+ const fileOrigin = localeSegmentSourceForFile({
3621
+ path: input.path,
3622
+ absoluteFile: input.absoluteFile,
3623
+ localesDir: input.localesDir,
3624
+ structure: input.structure
3625
+ });
3626
+ const leaves = collectTranslationSurfaceLeaves(json, "", [], fileOrigin ?? void 0);
3627
+ return { ok: true, document: json, leaves, text, diagnostics };
3628
+ } catch (e) {
3629
+ const message = e instanceof Error ? e.message : String(e);
3630
+ emit({ level: "error", code: "locale_fs_read_failed", message, path: input.absoluteFile });
3631
+ return { ok: false, leaves: [], diagnostics };
3632
+ }
3633
+ }
3634
+
3635
+ // src/shared/locales/enumerate/resolveSegmentPath.ts
3636
+ function resolveLocaleSegmentAbsolutePath(input) {
3637
+ const { layout, path, locale } = input;
3638
+ const rel = input.segmentRelativePath ?? `${locale}.json`;
3639
+ return toPosixPath(path.join(layout.directoryAbsolute, rel));
3640
+ }
3641
+ function localeSegmentRefFromAbsolute(input) {
3642
+ const { layout, path, absolutePath } = input;
3643
+ let relativePath = path.relative(layout.directoryAbsolute, absolutePath);
3644
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
3645
+ return null;
3646
+ }
3647
+ relativePath = relativePath.replace(/\\/g, "/");
3648
+ if (!relativePath.endsWith(".json")) {
3649
+ return null;
3650
+ }
3651
+ const locale = localeCodeForSegment(layout.structure, path, { absolutePath, relativePath });
3652
+ if (locale === null) return null;
3653
+ return { locale, relativePath, absolutePath: toPosixPath(absolutePath) };
3654
+ }
3655
+
3656
+ // src/shared/locales/read/bundle.ts
3657
+ function readLocaleBundle(input) {
3658
+ const diagnostics = [];
3659
+ const emit = (d) => {
3660
+ diagnostics.push(d);
3661
+ };
3662
+ if (!isLocalesLayoutReadSupported(input.layout)) {
3663
+ emit({
3664
+ level: "error",
3665
+ code: "locale_layout_unsupported",
3666
+ message: `locale read is not implemented for mode=${input.layout.mode} structure=${input.layout.structure}`,
3667
+ path: input.absoluteFile
3668
+ });
3669
+ return { ok: false, leaves: [], diagnostics };
3670
+ }
3671
+ const segmentRef = localeSegmentRefFromAbsolute({
3672
+ layout: input.layout,
3673
+ path: input.path,
3674
+ absolutePath: input.absoluteFile
3675
+ });
3676
+ if (segmentRef === null) {
3677
+ emit({
3678
+ level: "warn",
3679
+ code: "locale_read_path_layout_mismatch",
3680
+ message: `path does not match configured layout mode=${input.layout.mode} structure=${input.layout.structure}`,
3681
+ path: input.absoluteFile
3682
+ });
3683
+ return { ok: false, leaves: [], diagnostics };
3684
+ }
3685
+ return readFlatLocaleJsonSurface({
3686
+ fs: input.fs,
3687
+ path: input.path,
3688
+ absoluteFile: input.absoluteFile,
3689
+ localesDir: input.layout.directoryAbsolute,
3690
+ structure: input.layout.structure,
3691
+ onDiagnostic: input.onDiagnostic
3692
+ });
3693
+ }
3694
+
3695
+ // src/shared/languages/normalize.ts
3696
+ function normalizeLanguageCode(code) {
3697
+ return code.trim().toLowerCase().replace(/_/g, "-");
3698
+ }
3699
+
3700
+ // src/shared/locales/enumerate/fromContext.ts
3701
+ function listLocaleSegmentsFromContext(ctx) {
3702
+ return listLocaleSegments({
3703
+ layout: resolveLocalesLayoutFromContext(ctx),
3704
+ fs: ctx.adapters.fs,
3705
+ path: ctx.adapters.path
3706
+ });
3707
+ }
3708
+
3709
+ // src/shared/locales/targets/context.ts
3710
+ function normalizeCode(code) {
3711
+ return normalizeLanguageCode(code);
3712
+ }
3713
+ function listLocaleSegmentTargets(ctx) {
3714
+ const { segments } = listLocaleSegmentsFromContext(ctx);
3715
+ return segments.map((segment) => ({
3716
+ ...segment,
3717
+ reportKey: segment.relativePath
3718
+ }));
3719
+ }
3720
+ function sourceLocaleCodeFromContext(ctx) {
3721
+ const layout = resolveLocalesLayoutFromContext(ctx);
3722
+ const ref = localeSegmentRefFromAbsolute({
3723
+ layout,
3724
+ path: ctx.adapters.path,
3725
+ absolutePath: ctx.paths.sourceLocale
3726
+ });
3727
+ if (ref !== null) {
3728
+ return normalizeCode(ref.locale);
3729
+ }
3730
+ return normalizeCode(ctx.adapters.path.basename(ctx.paths.sourceLocale, ".json"));
3731
+ }
3732
+ function segmentsForLocaleCode(ctx, localeCode) {
3733
+ const want = normalizeCode(localeCode);
3734
+ return listLocaleSegmentTargets(ctx).filter((s) => normalizeCode(s.locale) === want);
3735
+ }
3736
+ function primarySegmentForLocale(ctx, localeCode) {
3737
+ const segments = segmentsForLocaleCode(ctx, localeCode);
3738
+ if (segments.length === 0) return void 0;
3739
+ const sourceResolved = ctx.adapters.path.resolve(ctx.paths.sourceLocale);
3740
+ const sourceMatch = segments.find((s) => ctx.adapters.path.resolve(s.absolutePath) === sourceResolved);
3741
+ if (sourceMatch) return sourceMatch;
3742
+ return segments.slice().sort((a, b) => a.relativePath.localeCompare(b.relativePath))[0];
3743
+ }
3744
+ function resolveLocaleSegmentTargets(ctx, input) {
3745
+ const source = sourceLocaleCodeFromContext(ctx);
3746
+ const all = listLocaleSegmentTargets(ctx).filter((s) => normalizeCode(s.locale) !== source);
3747
+ if (input.selection.mode === "all") {
3748
+ return { segments: all, missingLocaleCodes: [] };
3749
+ }
3750
+ const missingLocaleCodes = [];
3751
+ const segments = [];
3752
+ for (const code of input.selection.codes) {
3753
+ const norm = normalizeCode(code);
3754
+ const forCode = all.filter((s) => normalizeCode(s.locale) === norm);
3755
+ if (forCode.length === 0) {
3756
+ missingLocaleCodes.push(norm);
3757
+ continue;
3758
+ }
3759
+ segments.push(...forCode);
3760
+ }
3761
+ segments.sort((a, b) => {
3762
+ const byLocale = a.locale.localeCompare(b.locale);
3763
+ if (byLocale !== 0) return byLocale;
3764
+ return a.relativePath.localeCompare(b.relativePath);
3765
+ });
3766
+ return { segments, missingLocaleCodes };
3767
+ }
3768
+
3769
+ // src/shared/locales/read/cache.ts
3770
+ function invalidateLocaleReadCacheForAbsolutePath(ctx, absolutePath) {
3771
+ ctx.localeRead.segments.delete(absolutePath);
3772
+ const layout = resolveLocalesLayoutFromContext(ctx);
3773
+ const ref = localeSegmentRefFromAbsolute({
3774
+ layout,
3775
+ path: ctx.adapters.path,
3776
+ absolutePath
3777
+ });
3778
+ if (ref !== null) {
3779
+ ctx.localeRead.localeCodes.delete(normalizeLanguageCode(ref.locale));
3780
+ }
3781
+ }
3782
+ function dropLocaleCodeReadCache(ctx, localeCode) {
3783
+ ctx.localeRead.localeCodes.delete(normalizeLanguageCode(localeCode));
3784
+ }
3785
+ function invalidateLocaleReadCacheForLocaleCode(ctx, localeCode) {
3786
+ const normalized = normalizeLanguageCode(localeCode);
3787
+ dropLocaleCodeReadCache(ctx, normalized);
3788
+ for (const segment of segmentsForLocaleCode(ctx, normalized)) {
3789
+ ctx.localeRead.segments.delete(segment.absolutePath);
3790
+ }
3791
+ }
3792
+
3793
+ // src/shared/locales/read/fromContext.ts
3794
+ function storeSegmentSnapshot(ctx, absoluteFile, snapshot) {
3795
+ ctx.localeRead.segments.set(absoluteFile, snapshot);
3796
+ if (snapshot.ok) {
3797
+ const layout = resolveLocalesLayoutFromContext(ctx);
3798
+ const ref = localeSegmentRefFromAbsolute({
3799
+ layout,
3800
+ path: ctx.adapters.path,
3801
+ absolutePath: absoluteFile
3802
+ });
3803
+ if (ref !== null) {
3804
+ dropLocaleCodeReadCache(ctx, ref.locale);
3805
+ }
3806
+ }
3807
+ }
3808
+ function segmentSnapshotToResult(snapshot) {
3809
+ if (snapshot.ok) {
3810
+ return {
3811
+ ok: true,
3812
+ document: snapshot.document,
3813
+ leaves: snapshot.leaves,
3814
+ text: snapshot.text,
3815
+ diagnostics: []
3816
+ };
3817
+ }
3818
+ return { ok: false, leaves: [], diagnostics: snapshot.diagnostics };
3819
+ }
3820
+ function readLocaleSegmentFromContext(ctx, absoluteFile, onDiagnostic) {
3821
+ const cached = ctx.localeRead.segments.get(absoluteFile);
3822
+ if (cached !== void 0) {
3823
+ return segmentSnapshotToResult(cached);
3824
+ }
3825
+ const result = readLocaleBundle({
3826
+ layout: resolveLocalesLayoutFromContext(ctx),
3827
+ fs: ctx.adapters.fs,
3828
+ path: ctx.adapters.path,
3829
+ absoluteFile,
3830
+ onDiagnostic
3831
+ });
3832
+ if (result.ok) {
3833
+ storeSegmentSnapshot(ctx, absoluteFile, {
3834
+ ok: true,
3835
+ absolutePath: absoluteFile,
3836
+ document: result.document,
3837
+ leaves: result.leaves,
3838
+ text: result.text
3839
+ });
3840
+ } else {
3841
+ storeSegmentSnapshot(ctx, absoluteFile, {
3842
+ ok: false,
3843
+ absolutePath: absoluteFile,
3844
+ diagnostics: result.diagnostics
3845
+ });
3846
+ }
3847
+ return result;
3848
+ }
3849
+ function storeLocaleCodeSnapshot(ctx, snapshot) {
3850
+ ctx.localeRead.localeCodes.set(normalizeLanguageCode(snapshot.localeCode), snapshot);
3851
+ }
3852
+ function readLocaleCodeSurfaceFromContext(ctx, localeCode, onDiagnostic) {
3853
+ const normalized = normalizeLanguageCode(localeCode);
3854
+ const cached = ctx.localeRead.localeCodes.get(normalized);
3855
+ if (cached !== void 0) {
3856
+ return {
3857
+ ok: true,
3858
+ document: cached.document,
3859
+ leaves: cached.leaves,
3860
+ text: "",
3861
+ diagnostics: []
3862
+ };
3863
+ }
3864
+ const layout = resolveLocalesLayoutFromContext(ctx);
3865
+ if (layout.mode === "flat_file") {
3866
+ const segment = primarySegmentForLocale(ctx, normalized);
3867
+ const absoluteFile = segment?.absolutePath ?? ctx.paths.sourceLocale;
3868
+ const read = readLocaleSegmentFromContext(ctx, absoluteFile, onDiagnostic);
3869
+ if (!read.ok) return read;
3870
+ storeLocaleCodeSnapshot(ctx, {
3871
+ localeCode: normalized,
3872
+ document: read.document,
3873
+ leaves: read.leaves
3874
+ });
3875
+ return read;
3876
+ }
3877
+ const diagnostics = [];
3878
+ const { segments, diagnostics: listDiagnostics } = listLocaleSegments({
3879
+ layout,
3880
+ fs: ctx.adapters.fs,
3881
+ path: ctx.adapters.path
3882
+ });
3883
+ diagnostics.push(...listDiagnostics);
3884
+ const forLocale = segments.filter((s) => normalizeLanguageCode(s.locale) === normalized);
3885
+ if (forLocale.length === 0) {
3886
+ const empty = { localeCode: normalized, document: {}, leaves: [] };
3887
+ storeLocaleCodeSnapshot(ctx, empty);
3888
+ return { ok: true, document: {}, leaves: [], text: "{}", diagnostics };
3889
+ }
3890
+ const allLeaves = [];
3891
+ const documents = [];
3892
+ let combinedText = "";
3893
+ for (const segment of forLocale) {
3894
+ const read = readLocaleSegmentFromContext(ctx, segment.absolutePath, onDiagnostic);
3895
+ diagnostics.push(...read.diagnostics);
3896
+ if (!read.ok) return { ok: false, leaves: [], diagnostics };
3897
+ allLeaves.push(...read.leaves);
3898
+ documents.push(read.document);
3899
+ combinedText = read.text;
3900
+ }
3901
+ const document = documents.length === 1 ? documents[0] : documents;
3902
+ storeLocaleCodeSnapshot(ctx, {
3903
+ localeCode: normalized,
3904
+ document,
3905
+ leaves: allLeaves
3906
+ });
3907
+ return {
3908
+ ok: true,
3909
+ document,
3910
+ leaves: allLeaves,
3911
+ text: combinedText,
3912
+ diagnostics
3913
+ };
3914
+ }
3915
+ function readLocaleJsonFromContextSync(ctx, absoluteFile) {
3916
+ const read = readLocaleSegmentFromContext(ctx, absoluteFile);
3917
+ if (!read.ok) {
3918
+ const message = read.diagnostics.map((d) => d.message).join(" \xB7 ") || "failed to read locale JSON";
3919
+ throw new I18nPruneError(message, "IO", { issueCode: ISSUE_IO_READ_FAILED });
3920
+ }
3921
+ return read.document;
3922
+ }
3923
+
3924
+ // src/shared/locales/surface/localeSurface.ts
3925
+ function readLocaleLeavesForCode(ctx, localeCode) {
3926
+ const read = readLocaleCodeSurfaceFromContext(ctx, localeCode);
3927
+ if (!read.ok) return [];
3928
+ return read.leaves;
3929
+ }
3930
+ function readSourceLocaleLeaves(ctx) {
3931
+ return readLocaleLeavesForCode(ctx, sourceLocaleCodeFromContext(ctx));
3932
+ }
3933
+
3934
+ // src/analysis/rebuild.ts
3935
+ function normalizeRelPath2(path) {
3936
+ return path.replace(/\\/g, "/");
3937
+ }
3938
+ function pathSet(paths) {
3939
+ return new Set(paths.map(normalizeRelPath2));
3940
+ }
3941
+ function observationPath(obs) {
3942
+ return obs.span.filePath !== void 0 ? normalizeRelPath2(obs.span.filePath) : void 0;
3943
+ }
3944
+ function sitePath(site) {
3945
+ return site.filePath !== void 0 ? normalizeRelPath2(site.filePath) : void 0;
3946
+ }
3947
+ function filterByPaths(rows, pathOf, remove) {
3948
+ return rows.filter((row) => {
3949
+ const filePath = pathOf(row);
3950
+ return filePath === void 0 || !remove.has(filePath);
3951
+ });
3952
+ }
3953
+ function scanInputFromContext(ctx) {
3954
+ return {
3955
+ srcRoot: ctx.paths.srcRoot,
3956
+ functions: ctx.config.functions,
3957
+ runtime: ctx.adapters,
3958
+ exclude: ctx.config.exclude
3959
+ };
3960
+ }
3961
+ function absolutePathsForRel(ctx, relPaths) {
3962
+ const { path, srcRoot } = { path: ctx.adapters.path, srcRoot: ctx.paths.srcRoot };
3963
+ return relPaths.map((rel) => path.join(srcRoot, rel));
3964
+ }
3965
+ function partialScanIo(ctx, relPaths) {
3966
+ const absPaths = absolutePathsForRel(ctx, relPaths);
3967
+ return {
3968
+ listFiles: () => absPaths,
3969
+ readFile: (filePath) => readRuntimeFsTextSync(filePath, ctx.adapters.fs)
3970
+ };
3971
+ }
3972
+ function scanKeyObservationsForPaths(ctx, relPaths) {
3973
+ if (relPaths.length === 0) return [];
3974
+ return scanProjectKeyObservations({
3975
+ ...scanInputFromContext(ctx),
3976
+ ...partialScanIo(ctx, relPaths)
3977
+ });
3978
+ }
3979
+ function scanDynamicSitesForPaths(ctx, relPaths) {
3980
+ if (relPaths.length === 0) return [];
3981
+ return scanProjectDynamicKeySites({
3982
+ ...scanInputFromContext(ctx),
3983
+ ...partialScanIo(ctx, relPaths)
3984
+ });
3985
+ }
3986
+ function patchProjectAnalysisFromSrcDelta(ctx, previous, srcDelta) {
3987
+ const remove = pathSet([...srcDelta.deleted, ...srcDelta.changed]);
3988
+ const rescan = [...srcDelta.added, ...srcDelta.changed];
3989
+ const keyObservations = [
3990
+ ...filterByPaths(previous.keyObservations, observationPath, remove),
3991
+ ...scanKeyObservationsForPaths(ctx, rescan)
3992
+ ];
3993
+ const dynamicSites = [
3994
+ ...filterByPaths(previous.dynamicSites, sitePath, remove),
3995
+ ...scanDynamicSitesForPaths(ctx, rescan)
3996
+ ];
3997
+ const usage = literalKeyUsageFromObservations(keyObservations);
3998
+ const sourceLeaves = readSourceLocaleLeaves(ctx);
3999
+ const missingKeys = computeMissingLiteralKeysFromLeaves(sourceLeaves, usage.resolvedKeys);
4000
+ const sourceFilesScanned = previous.counts.sourceFilesScanned + srcDelta.added.length - srcDelta.deleted.length;
4001
+ return {
4002
+ version: 1,
4003
+ keyObservations,
4004
+ dynamicSites,
4005
+ missingKeys,
4006
+ counts: {
4007
+ keyObservations: keyObservations.length,
4008
+ dynamicSites: dynamicSites.length,
4009
+ sourceFilesScanned: Math.max(0, sourceFilesScanned),
4010
+ missingKeys: missingKeys.length
4011
+ }
4012
+ };
4013
+ }
4014
+ function patchProjectAnalysisFromSourceLocaleDelta(ctx, previous) {
4015
+ invalidateLocaleReadCacheForLocaleCode(ctx, sourceLocaleCodeFromContext(ctx));
4016
+ const usage = literalKeyUsageFromObservations(previous.keyObservations);
4017
+ const sourceLeaves = readSourceLocaleLeaves(ctx);
4018
+ const missingKeys = computeMissingLiteralKeysFromLeaves(sourceLeaves, usage.resolvedKeys);
4019
+ return {
4020
+ ...previous,
4021
+ missingKeys,
4022
+ counts: {
4023
+ ...previous.counts,
4024
+ missingKeys: missingKeys.length
4025
+ }
4026
+ };
4027
+ }
4028
+
4029
+ // src/analysis/project.ts
4030
+ function isRecord(v) {
4031
+ return typeof v === "object" && v !== null;
4032
+ }
4033
+ function isAnalysisCounts(v) {
4034
+ if (!isRecord(v)) return false;
4035
+ return typeof v.keyObservations === "number" && typeof v.dynamicSites === "number" && typeof v.sourceFilesScanned === "number" && typeof v.missingKeys === "number";
4036
+ }
4037
+ function parseProjectAnalysisCacheData(data) {
4038
+ if (!isRecord(data)) return { ok: false };
4039
+ if (data.version !== 1) return { ok: false };
4040
+ if (!Array.isArray(data.keyObservations) || !Array.isArray(data.dynamicSites)) return { ok: false };
4041
+ if (!Array.isArray(data.missingKeys)) return { ok: false };
4042
+ if (!isAnalysisCounts(data.counts)) return { ok: false };
4043
+ return {
4044
+ ok: true,
4045
+ data: {
4046
+ version: 1,
4047
+ keyObservations: data.keyObservations,
4048
+ dynamicSites: data.dynamicSites,
4049
+ missingKeys: data.missingKeys,
4050
+ counts: data.counts
4051
+ }
4052
+ };
4053
+ }
4054
+ function scanProjectAnalysis(ctx) {
4055
+ invalidateLocaleReadCacheForLocaleCode(ctx, sourceLocaleCodeFromContext(ctx));
4056
+ const scanInput = {
4057
+ srcRoot: ctx.paths.srcRoot,
4058
+ functions: ctx.config.functions,
4059
+ runtime: ctx.adapters,
4060
+ exclude: ctx.config.exclude
4061
+ };
4062
+ const keyObservations = scanProjectKeyObservations(scanInput);
4063
+ const dynamicSites = scanProjectDynamicKeySites(scanInput);
4064
+ const usage = literalKeyUsageFromObservations(keyObservations);
4065
+ const sourceLeaves = readSourceLocaleLeaves(ctx);
4066
+ const missingKeys = computeMissingLiteralKeysFromLeaves(sourceLeaves, usage.resolvedKeys);
4067
+ const projectFs = { fs: ctx.adapters.fs, path: ctx.adapters.path };
4068
+ const sourceFilesScanned = listSourceFiles(projectFs, ctx.paths.srcRoot, ctx.config.exclude).length;
4069
+ return {
4070
+ version: 1,
4071
+ keyObservations,
4072
+ dynamicSites,
4073
+ missingKeys,
4074
+ counts: {
4075
+ keyObservations: keyObservations.length,
4076
+ dynamicSites: dynamicSites.length,
4077
+ sourceFilesScanned,
4078
+ missingKeys: missingKeys.length
4079
+ }
4080
+ };
4081
+ }
4082
+ function produceProjectAnalysis(ctx, rebuild) {
4083
+ if (rebuild?.previous !== void 0 && rebuild.analysisRebuild?.strategy === "reuse") {
4084
+ return rebuild.previous;
4085
+ }
4086
+ if (rebuild?.previous !== void 0 && rebuild.analysisRebuild?.strategy === "partial") {
4087
+ if (rebuild.analysisRebuild.reason === "source_locale_partial") {
4088
+ return patchProjectAnalysisFromSourceLocaleDelta(ctx, rebuild.previous);
4089
+ }
4090
+ return patchProjectAnalysisFromSrcDelta(ctx, rebuild.previous, rebuild.classified.src);
4091
+ }
4092
+ return scanProjectAnalysis(ctx);
4093
+ }
4094
+ function withDerivedUsage(data, cache) {
4095
+ return {
4096
+ ...data,
4097
+ usage: literalKeyUsageFromObservations(data.keyObservations),
4098
+ ...cache !== void 0 ? { cache } : {}
4099
+ };
4100
+ }
4101
+ function resolveProjectAnalysis(ctx, opts = {}) {
4102
+ const cacheCtx = ctx.cache;
4103
+ if (cacheCtx === void 0) {
4104
+ return withDerivedUsage(scanProjectAnalysis(ctx));
4105
+ }
4106
+ const rebuildConfig = resolveCacheRebuildConfig(ctx.config.cache);
4107
+ const result = getOrBuildCachedProjectData({
4108
+ state: cacheCtx.state,
4109
+ runtime: cacheCtx.runtime,
4110
+ sourceLocalePath: ctx.paths.sourceLocale,
4111
+ srcRoot: ctx.paths.srcRoot,
4112
+ localesDir: ctx.paths.localesDir,
4113
+ locales: ctx.config.locales,
4114
+ exclude: ctx.config.exclude,
4115
+ rebuildConfig,
4116
+ producer: (rebuild) => produceProjectAnalysis(ctx, rebuild),
4117
+ parseCachedData: parseProjectAnalysisCacheData,
4118
+ baselineFiles: cacheCtx.baselineFiles
4119
+ });
4120
+ if (opts.emit !== void 0 && opts.op !== void 0) {
4121
+ emitCacheDispatchMessages({
4122
+ emit: opts.emit,
4123
+ op: opts.op,
4124
+ runId: opts.runId,
4125
+ label: "project analysis",
4126
+ cache: result.cache
4127
+ });
4128
+ }
4129
+ return withDerivedUsage(result.data, result.cache);
4130
+ }
4131
+
4132
+ // src/locales/source.ts
4133
+ function getSourceLocaleSlug(path, sourceLocalePath) {
4134
+ return normalizeLanguageCode(path.basename(sourceLocalePath, ".json"));
4135
+ }
4136
+ function getDisplaySourceLocaleCode(ctx) {
4137
+ return getSourceLocaleSlug(ctx.path, ctx.paths.sourceLocale);
4138
+ }
4139
+ function buildSourceLocaleTruthLabel(displaySlug) {
4140
+ return `(${displaySlug} - source of truth)`;
4141
+ }
4142
+ function isSourceLocaleSlug(path, candidate, sourceLocalePath) {
4143
+ return normalizeLanguageCode(candidate) === getSourceLocaleSlug(path, sourceLocalePath);
4144
+ }
4145
+ function assertNotSourceTargetLocale(command, lang, sourceLocalePath, ctx) {
4146
+ if (!isSourceLocaleSlug(ctx.path, lang, sourceLocalePath)) return;
4147
+ const display = getDisplaySourceLocaleCode(ctx);
4148
+ throw new I18nPruneError(
4149
+ `${command} does not apply to the source locale ${buildSourceLocaleTruthLabel(display)}. Pass a target language code.`,
4150
+ "USAGE"
4151
+ );
4152
+ }
4153
+
4154
+ // src/locales/targets.ts
4155
+ var ALL_LOCALES_TOKEN = "all";
4156
+ function parseLocaleCodesList(raw) {
4157
+ return raw.split(",").map((s) => s.trim()).filter(Boolean).map((c) => normalizeLanguageCode(c));
4158
+ }
4159
+ function isAllLocaleToken(raw) {
4160
+ return raw.trim().toLowerCase() === ALL_LOCALES_TOKEN;
4161
+ }
4162
+ function parseSyncLangSelection(lang) {
4163
+ const primary = lang?.trim();
4164
+ if (primary) {
4165
+ if (isAllLocaleToken(primary)) return { mode: "all" };
4166
+ return { mode: "codes", codes: parseLocaleCodesList(primary) };
4167
+ }
4168
+ return { mode: "all" };
4169
+ }
4170
+
4171
+ // src/shared/locales/targets/segmentWritePlan.ts
4172
+ function swapLocaleInSegmentRelativePath(input) {
4173
+ const target = normalizeLanguageCode(input.targetLocale);
4174
+ const rel = input.relativePath.replace(/\\/g, "/");
4175
+ if (input.structure === "locale_file") {
4176
+ if (rel.includes("/")) return null;
4177
+ return `${target}.json`;
4178
+ }
4179
+ if (input.structure === "locale_per_dir") {
4180
+ const slash = rel.indexOf("/");
4181
+ if (slash < 0) return null;
4182
+ const rest = rel.slice(slash + 1);
4183
+ if (!rest) return null;
4184
+ return `${target}/${rest}`;
4185
+ }
4186
+ if (input.structure === "feature_bundle") {
4187
+ const slash = rel.lastIndexOf("/");
4188
+ if (slash < 0) return null;
4189
+ const feature = rel.slice(0, slash);
4190
+ if (!feature) return null;
4191
+ return `${feature}/${target}.json`;
4192
+ }
4193
+ return null;
4194
+ }
4195
+
4196
+ // src/shared/locales/write/flatFileLocaleJson.ts
4197
+ function writeFlatLocaleJsonDocument(input) {
4198
+ const diagnostics = [];
4199
+ const emit = (d) => {
4200
+ diagnostics.push(d);
4201
+ input.onDiagnostic?.(d);
4202
+ };
4203
+ const indent = input.indent ?? 2;
4204
+ let body;
4205
+ try {
4206
+ body = `${JSON.stringify(sortJsonObjectKeysAsc(input.data), null, indent)}
4207
+ `;
4208
+ } catch (e) {
4209
+ const message = e instanceof Error ? e.message : String(e);
4210
+ emit({ level: "error", code: "locale_json_serialize_failed", message, path: input.absoluteFile });
4211
+ return { ok: false, diagnostics };
4212
+ }
4213
+ try {
4214
+ input.fs.mkdirp(input.path.dirname(input.absoluteFile));
4215
+ input.fs.writeText(input.absoluteFile, body);
4216
+ return { ok: true, diagnostics };
4217
+ } catch (e) {
4218
+ const message = e instanceof Error ? e.message : String(e);
4219
+ emit({ level: "error", code: "locale_fs_write_failed", message, path: input.absoluteFile });
4220
+ return { ok: false, diagnostics };
4221
+ }
4222
+ }
4223
+
4224
+ // src/shared/locales/write/bundle.ts
4225
+ function writeLocaleBundle(input) {
4226
+ const diagnostics = [];
4227
+ const emit = (d) => {
4228
+ diagnostics.push(d);
4229
+ input.onDiagnostic?.(d);
4230
+ };
4231
+ if (!isLocalesLayoutWriteSupported(input.layout)) {
4232
+ emit({
4233
+ level: "error",
4234
+ code: "locale_layout_unsupported",
4235
+ message: `locale write is not implemented for mode=${input.layout.mode} structure=${input.layout.structure}`,
4236
+ path: input.absoluteFile
4237
+ });
4238
+ return { ok: false, diagnostics };
4239
+ }
4240
+ const segmentRef = localeSegmentRefFromAbsolute({
4241
+ layout: input.layout,
4242
+ path: input.path,
4243
+ absolutePath: input.absoluteFile
4244
+ });
4245
+ if (segmentRef === null) {
4246
+ emit({
4247
+ level: "warn",
4248
+ code: "locale_write_path_layout_mismatch",
4249
+ message: `path does not match configured layout mode=${input.layout.mode} structure=${input.layout.structure}`,
4250
+ path: input.absoluteFile
4251
+ });
4252
+ return { ok: false, diagnostics };
4253
+ }
4254
+ return writeFlatLocaleJsonDocument({
4255
+ fs: input.fs,
4256
+ path: input.path,
4257
+ absoluteFile: input.absoluteFile,
4258
+ data: input.data,
4259
+ indent: input.indent,
4260
+ onDiagnostic: input.onDiagnostic
4261
+ });
4262
+ }
4263
+ function writeLocaleJsonFromContextSync(ctx, absoluteFile, data) {
4264
+ const result = writeLocaleBundle({
4265
+ layout: resolveLocalesLayoutFromContext(ctx),
4266
+ fs: ctx.adapters.fs,
4267
+ path: ctx.adapters.path,
4268
+ absoluteFile,
4269
+ data
4270
+ });
4271
+ if (!result.ok) {
4272
+ const message = result.diagnostics.map((d) => d.message).join(" \xB7 ") || "failed to write locale JSON";
4273
+ throw new I18nPruneError(message, "IO", { issueCode: ISSUE_IO_READ_FAILED });
4274
+ }
4275
+ invalidateLocaleReadCacheForAbsolutePath(ctx, absoluteFile);
4276
+ }
4277
+
4278
+ // src/shared/locales/surface/segmentPairing.ts
4279
+ function pairedSourceSegmentRelativePath(ctx, targetSegmentRelativePath, _targetLocaleCode) {
4280
+ const layout = resolveLocalesLayoutFromContext(ctx);
4281
+ const sourceCode = sourceLocaleCodeFromContext(ctx);
4282
+ return swapLocaleInSegmentRelativePath({
4283
+ structure: layout.structure,
4284
+ relativePath: targetSegmentRelativePath,
4285
+ targetLocale: sourceCode
4286
+ });
4287
+ }
4288
+ function resolvePairedSourceSegmentAbsolutePath(ctx, targetSegmentRelativePath, targetLocaleCode) {
4289
+ const layout = resolveLocalesLayoutFromContext(ctx);
4290
+ const sourceCode = sourceLocaleCodeFromContext(ctx);
4291
+ const sourceRel = pairedSourceSegmentRelativePath(ctx, targetSegmentRelativePath) ?? targetSegmentRelativePath;
4292
+ return resolveLocaleSegmentAbsolutePath({
4293
+ layout,
4294
+ path: ctx.adapters.path,
4295
+ locale: sourceCode,
4296
+ segmentRelativePath: sourceRel
4297
+ });
4298
+ }
4299
+
4300
+ // src/shared/locales/surface/syncSegment.ts
4301
+ function buildSegmentTemplateFromSource(_sourceRaw, leaves) {
4302
+ let template = {};
4303
+ for (const leaf of leaves) {
4304
+ template = setAtPath(template, leaf.path, leaf.value);
4305
+ }
4306
+ return template;
4307
+ }
4308
+ function resolveSyncSegmentSourcePlan(ctx, input) {
4309
+ const sourceRelativePath = pairedSourceSegmentRelativePath(ctx, input.targetSegmentRelativePath) ?? input.targetSegmentRelativePath;
4310
+ const sourceAbsolutePath = resolvePairedSourceSegmentAbsolutePath(
4311
+ ctx,
4312
+ input.targetSegmentRelativePath);
4313
+ let sourceRaw = {};
4314
+ let allSourceLeaves = [];
4315
+ if (existsRuntimeFsSync(sourceAbsolutePath, ctx.adapters.fs)) {
4316
+ const read = readLocaleSegmentFromContext(ctx, sourceAbsolutePath);
4317
+ if (read.ok) {
4318
+ sourceRaw = read.document;
4319
+ allSourceLeaves = read.leaves;
4320
+ }
4321
+ }
4322
+ const effectiveSchemaPaths = input.schemaPaths.size > 0 ? input.schemaPaths : new Set(allSourceLeaves.map((l) => l.path));
4323
+ const effectiveSourceLeaves = allSourceLeaves.filter((l) => effectiveSchemaPaths.has(l.path));
4324
+ const template = buildSegmentTemplateFromSource(sourceRaw, effectiveSourceLeaves);
4325
+ return {
4326
+ sourceRelativePath,
4327
+ sourceAbsolutePath,
4328
+ sourceRaw,
4329
+ effectiveSourceLeaves,
4330
+ template,
4331
+ sourceMap: new Map(effectiveSourceLeaves.map((l) => [l.path, l.value]))
4332
+ };
4333
+ }
4334
+ function resolveGlobalSyncSchemaPaths(ctx, schemaPaths) {
4335
+ if (schemaPaths.size > 0) return schemaPaths;
4336
+ return new Set(readSourceLocaleLeaves(ctx).map((l) => l.path));
4337
+ }
4338
+
4339
+ // src/shared/constants/missing.ts
4340
+ var DEFAULT_MISSING_LEAF_PLACEHOLDER = "__I18NPRUNE_MISSING__";
4341
+
4342
+ // src/missing/placeholder.ts
4343
+ var MISSING_LEAF_PLACEHOLDER_MAX_LEN = 256;
4344
+ function resolveMissingLeafPlaceholder(raw) {
4345
+ const warnings = [];
4346
+ const def = DEFAULT_MISSING_LEAF_PLACEHOLDER;
4347
+ if (raw === void 0) return { placeholder: def, warnings };
4348
+ if (typeof raw !== "string") {
4349
+ warnings.push(
4350
+ `missing.placeholder must be a string; got ${typeof raw}. Using default ${JSON.stringify(def)} for reliable detection.`
4351
+ );
4352
+ return { placeholder: def, warnings };
4353
+ }
4354
+ const t = raw.trim();
4355
+ if (t.length === 0) {
4356
+ warnings.push(
4357
+ `missing.placeholder is empty or whitespace-only; using default ${JSON.stringify(def)} so missing tooling can detect scaffolded paths.`
4358
+ );
4359
+ return { placeholder: def, warnings };
4360
+ }
4361
+ if (t.length > MISSING_LEAF_PLACEHOLDER_MAX_LEN) {
4362
+ warnings.push(
4363
+ `missing.placeholder exceeds ${String(MISSING_LEAF_PLACEHOLDER_MAX_LEN)} characters; using default ${JSON.stringify(def)}.`
4364
+ );
4365
+ return { placeholder: def, warnings };
4366
+ }
4367
+ return { placeholder: t, warnings };
4368
+ }
4369
+
4370
+ // src/shared/sourcePlaceholders/index.ts
4371
+ function sourcePlaceholderValues(configuredPlaceholder) {
4372
+ const resolved = resolveMissingLeafPlaceholder(configuredPlaceholder).placeholder;
4373
+ return [...new Set([DEFAULT_MISSING_LEAF_PLACEHOLDER, resolved].filter((value) => value.trim().length > 0))];
4374
+ }
4375
+ function detectSourcePlaceholderLeaves(leaves, placeholderValues) {
4376
+ const sentinels = new Set(placeholderValues);
4377
+ if (sentinels.size === 0) return [];
4378
+ return leaves.filter((leaf) => sentinels.has(leaf.value)).map((leaf) => ({ path: leaf.path, value: leaf.value }));
4379
+ }
4380
+ function detectLocalePlaceholderLeaves(input) {
4381
+ return detectSourcePlaceholderLeaves(input.leaves, input.placeholderValues).map((leaf) => ({
4382
+ ...leaf,
4383
+ localeRole: input.localeRole,
4384
+ localeCode: input.localeCode,
4385
+ ...input.localePath !== void 0 ? { localePath: input.localePath } : {}
4386
+ }));
4387
+ }
4388
+ function formatSourcePlaceholderMessage(input) {
4389
+ const sample = input.samplePaths.join(", ");
4390
+ return `Source locale has ${String(input.count)} missing placeholder value(s). Run \`i18nprune missing --full\` to list placeholder paths. Replace them with real source copy, then run \`i18nprune sync\` and \`i18nprune generate --resume\` for target locales. Sample paths: ${sample}`;
4391
+ }
4392
+ function formatSyncSourcePlaceholderMessage(input) {
4393
+ const sample = input.samplePaths.join(", ");
4394
+ return `Source locale has ${String(input.count)} missing placeholder value(s); sync skipped those path(s) so placeholders are not copied to target locales. Run \`i18nprune missing --full\` to list placeholder paths. Replace them with real source copy, then run \`i18nprune sync\` and \`i18nprune generate --resume\`. Sample paths: ${sample}`;
4395
+ }
4396
+ function formatTargetPlaceholderMessage(input) {
4397
+ const sample = input.samplePaths.join(", ");
4398
+ const target = input.targetLabel === void 0 ? "Target locale" : `Target locale ${input.targetLabel}`;
4399
+ const listCommand = input.targetLabel === void 0 ? "i18nprune missing --target all --full" : `i18nprune missing --target ${input.targetLabel} --full`;
4400
+ return `${target} has ${String(input.count)} missing placeholder value(s). Run \`${listCommand}\` to list placeholder paths, then \`i18nprune sync\` and \`i18nprune generate --resume\` to refill translations. Sample paths: ${sample}`;
4401
+ }
4402
+ function issuesFromSourcePlaceholderLeaves(leaves) {
4403
+ if (leaves.length === 0) return [];
4404
+ return [
4405
+ {
4406
+ severity: "warning",
4407
+ code: ISSUE_LOCALE_SOURCE_PLACEHOLDER_LEAVES,
4408
+ message: formatSourcePlaceholderMessage({
4409
+ count: leaves.length,
4410
+ samplePaths: leaves.slice(0, 5).map((leaf) => leaf.path)
4411
+ }),
4412
+ docPath: issueCodeRepoDocPathForIssueCode(ISSUE_LOCALE_SOURCE_PLACEHOLDER_LEAVES)
4413
+ }
4414
+ ];
4415
+ }
4416
+ function issuesFromTargetPlaceholderLeaves(leaves) {
4417
+ const targetLeaves = leaves.filter((leaf) => leaf.localeRole === "target");
4418
+ if (targetLeaves.length === 0) return [];
4419
+ return [
4420
+ {
4421
+ severity: "warning",
4422
+ code: ISSUE_LOCALE_TARGET_PLACEHOLDER_LEAVES,
4423
+ message: formatTargetPlaceholderMessage({
4424
+ count: targetLeaves.length,
4425
+ samplePaths: targetLeaves.slice(0, 5).map((leaf) => `${leaf.localeCode}:${leaf.path}`),
4426
+ ...new Set(targetLeaves.map((leaf) => leaf.localeCode)).size === 1 ? { targetLabel: targetLeaves[0].localeCode } : {}
4427
+ }),
4428
+ docPath: issueCodeRepoDocPathForIssueCode(ISSUE_LOCALE_TARGET_PLACEHOLDER_LEAVES)
4429
+ }
4430
+ ];
4431
+ }
4432
+
4433
+ // src/sync/humanLeafSummary.ts
4434
+ function isPlainObject8(x) {
4435
+ return typeof x === "object" && x !== null && !Array.isArray(x);
4436
+ }
4437
+ function readLeafDisplayString(root, pathStr) {
4438
+ const v = getLocaleLeafAtPath(root, pathStr);
4439
+ if (typeof v === "string") return v;
4440
+ if (isPlainObject8(v) && typeof v.value === "string") return v.value;
4441
+ return void 0;
4442
+ }
4443
+ function canonicalTemplatePathForCollectedLeaf(leafPath, templatePaths) {
4444
+ if (templatePaths.has(leafPath)) return leafPath;
4445
+ if (leafPath.endsWith(".value")) {
4446
+ const base = leafPath.slice(0, -".value".length);
4447
+ if (templatePaths.has(base)) return base;
4448
+ }
4449
+ return null;
4450
+ }
4451
+ function summarizeSyncLeavesForHumanLog(sourceLeaves, cur, mergedLocaleJson) {
4452
+ const templatePaths = new Set(sourceLeaves.map((l) => l.path));
4453
+ let hydratedFromSource = 0;
4454
+ let preservedExistingLeaves = 0;
4455
+ for (const pathStr of templatePaths) {
4456
+ const beforeStr = readLeafDisplayString(cur, pathStr);
4457
+ if (beforeStr === void 0) hydratedFromSource++;
4458
+ else preservedExistingLeaves++;
4459
+ }
4460
+ const beforeRawPaths = new Set(collectTranslationSurfaceLeaves(cur).map((leaf) => leaf.path));
4461
+ const afterRawPaths = new Set(collectTranslationSurfaceLeaves(mergedLocaleJson).map((leaf) => leaf.path));
4462
+ let prunedExtraLeaves = 0;
4463
+ for (const p of beforeRawPaths) {
4464
+ if (canonicalTemplatePathForCollectedLeaf(p, templatePaths) !== null) continue;
4465
+ if (!afterRawPaths.has(p)) prunedExtraLeaves += 1;
4466
+ }
4467
+ return { hydratedFromSource, preservedExistingLeaves, prunedExtraLeaves };
4468
+ }
4469
+
4470
+ // src/shared/constants/listDisplay.ts
4471
+ var LIST_MORE_HINT = "(use --full or --top <n>)";
4472
+ function formatListOmittedSuffix(omitted) {
4473
+ return `\u2026 ${String(omitted)} more ${LIST_MORE_HINT}`;
4474
+ }
4475
+ function formatListShownOmitted(prefix, omitted) {
4476
+ if (omitted <= 0) return prefix;
4477
+ return `${prefix} + ${formatListOmittedSuffix(omitted)}`;
4478
+ }
4479
+
4480
+ // src/sync/humanEmit.ts
4481
+ function parseSyncReportKey(reportKey) {
4482
+ const slash = reportKey.indexOf("/");
4483
+ if (slash === -1) {
4484
+ const base = reportKey.endsWith(".json") ? reportKey.slice(0, -5) : reportKey;
4485
+ return { localeCode: base, segmentName: null };
4486
+ }
4487
+ return { localeCode: reportKey.slice(0, slash), segmentName: reportKey.slice(slash + 1) };
4488
+ }
4489
+ function buildSyncLocaleDisplayGroups(result) {
4490
+ const groups = /* @__PURE__ */ new Map();
4491
+ for (let i = 0; i < result.targets.length; i++) {
4492
+ const reportKey = result.targets[i];
4493
+ const fileLine = result.fileLines[i];
4494
+ if (!fileLine) continue;
4495
+ const { localeCode } = parseSyncReportKey(reportKey);
4496
+ let group = groups.get(localeCode);
4497
+ if (!group) {
4498
+ group = { localeCode, reportKeys: [], fileLines: [], changedCount: 0, summaries: [] };
4499
+ groups.set(localeCode, group);
4500
+ }
4501
+ group.reportKeys.push(reportKey);
4502
+ group.fileLines.push(fileLine);
4503
+ if (fileLine.changed) group.changedCount += 1;
4504
+ const summary = result.humanLeafSummaryByLocaleFile[reportKey];
4505
+ if (summary) group.summaries.push(summary);
4506
+ }
4507
+ return [...groups.values()].sort((a, b) => a.localeCode.localeCompare(b.localeCode));
4508
+ }
4509
+ function isSegmentedSyncLayout(groups) {
4510
+ return groups.some((group) => group.reportKeys.length > 1);
4511
+ }
4512
+ function mergeSyncHumanLeafSummaries(summaries) {
4513
+ return summaries.reduce(
4514
+ (acc, summary) => ({
4515
+ hydratedFromSource: acc.hydratedFromSource + summary.hydratedFromSource,
4516
+ preservedExistingLeaves: acc.preservedExistingLeaves + summary.preservedExistingLeaves,
4517
+ prunedExtraLeaves: acc.prunedExtraLeaves + summary.prunedExtraLeaves
4518
+ }),
4519
+ { hydratedFromSource: 0, preservedExistingLeaves: 0, prunedExtraLeaves: 0 }
4520
+ );
4521
+ }
4522
+ function formatSyncLocaleFileDetailLine(group, segmented, dryRun) {
4523
+ const total = group.fileLines.length;
4524
+ if (!segmented || total === 1) {
4525
+ const reportKey = group.reportKeys[0] ?? group.localeCode;
4526
+ const fileLine = group.fileLines[0];
4527
+ const mark = fileLine.changed ? "\u2713" : "\xB7";
4528
+ const tail = fileLine.changed ? dryRun ? " (would write)" : " (written)" : " (unchanged)";
4529
+ return ` ${mark} ${reportKey}${tail}`;
4530
+ }
4531
+ const unchanged = total - group.changedCount;
4532
+ if (group.changedCount === 0) {
4533
+ return ` \xB7 ${group.localeCode} \xB7 unchanged (${String(total)} segment files)`;
4534
+ }
4535
+ if (group.changedCount === total) {
4536
+ const verb = dryRun ? "would change" : "updated";
4537
+ return ` \u2713 ${group.localeCode} \xB7 ${verb} (${String(total)} segment files)`;
4538
+ }
4539
+ const updatedVerb = dryRun ? "would change" : "updated";
4540
+ return ` \xB7 ${group.localeCode} \xB7 ${String(group.changedCount)} ${updatedVerb} \xB7 ${String(unchanged)} unchanged (${String(total)} segment files)`;
4541
+ }
4542
+ function formatSyncFileListOmittedLine(shownLocaleCount, omittedLocaleCount) {
4543
+ if (omittedLocaleCount <= 0) return "";
4544
+ const prefix = ` \xB7 ${String(shownLocaleCount)} locale(s) shown`;
4545
+ return formatListShownOmitted(prefix, omittedLocaleCount);
4546
+ }
4547
+ function formatSyncLeafSummaryLabel(group, segmented) {
4548
+ if (!segmented || group.reportKeys.length === 1) {
4549
+ return group.reportKeys[0] ?? group.localeCode;
4550
+ }
4551
+ return group.localeCode;
4552
+ }
4553
+
4554
+ // src/sync/run.ts
4555
+ function zeroByReason() {
4556
+ return {
4557
+ legacy_string_promoted: 0,
4558
+ non_object_replaced: 0,
4559
+ missing_value: 0,
4560
+ invalid_status: 0,
4561
+ invalid_confidence: 0,
4562
+ invalid_needs_review: 0,
4563
+ invalid_needs_translation_again: 0,
4564
+ invalid_source: 0,
4565
+ canonical_metadata_materialized: 0
4566
+ };
4567
+ }
4568
+ function idleLocaleMetadataReportForSkippedSync(totalSourceLeafPaths) {
4569
+ return {
4570
+ mode: "legacy_string",
4571
+ totalSourceLeafPaths,
4572
+ unchangedLeaves: totalSourceLeafPaths,
4573
+ structuredLeavesWritten: 0,
4574
+ promotedLegacyLeaves: 0,
4575
+ repairedCorruptLeaves: 0,
4576
+ strippedStructuredLeaves: 0,
4577
+ missingPathsHydratedFromSource: 0,
4578
+ byReason: zeroByReason(),
4579
+ changedPathsSample: [],
4580
+ leafDecisions: []
4581
+ };
4582
+ }
4583
+ function issuesFromDynamicScanCount(count) {
4584
+ if (count <= 0) return [];
4585
+ return [
4586
+ {
4587
+ severity: "warning",
4588
+ code: ISSUE_SCAN_DYNAMIC_KEY_SITES,
4589
+ message: `${String(count)} translation call(s) use a non-literal key \u2014 static analysis cannot enumerate computed keys as fixed paths.`,
4590
+ docPath: "dynamic/README"
4591
+ }
4592
+ ];
4593
+ }
4594
+ function issuesFromSyncMissingLocaleFiles(localeCodes) {
4595
+ if (localeCodes.length === 0) return [];
4596
+ return [
4597
+ {
4598
+ severity: "warning",
4599
+ code: ISSUE_SYNC_LOCALE_FILE_NOT_FOUND,
4600
+ message: `Locale file(s) not found under locales dir (skipped): ${localeCodes.map((c) => `${c}.json`).join(", ")}`,
4601
+ docPath: "commands/sync/README"
4602
+ }
4603
+ ];
4604
+ }
4605
+ function issueFromMetadataFlagConflict(conflict) {
4606
+ if (!conflict) return [];
4607
+ return [
4608
+ {
4609
+ severity: "warning",
4610
+ code: ISSUE_SYNC_METADATA_FLAG_CONFLICT,
4611
+ message: "Both metadata-enable and strip-metadata were requested; strip-metadata takes precedence and locale leaves are written as plain strings.",
4612
+ docPath: "locales/metadata/README"
4613
+ }
4614
+ ];
4615
+ }
4616
+ function syncLocaleMetadataDetailLine(report, explicitStrip, explicitMetadata) {
4617
+ if (!explicitStrip && !explicitMetadata || !report) return void 0;
4618
+ if (explicitStrip) {
4619
+ return report.strippedStructuredLeaves > 0 ? `metadata \xB7 stripped structured fields at ${String(report.strippedStructuredLeaves)} template leaf path(s) (plain strings)` : `metadata \xB7 no structured leaves stripped (already plain strings at template paths)`;
4620
+ }
4621
+ const parts = [];
4622
+ const mat = report.byReason.canonical_metadata_materialized ?? 0;
4623
+ if (report.promotedLegacyLeaves > 0) parts.push(`${String(report.promotedLegacyLeaves)} plain \u2192 structured`);
4624
+ if (mat > 0) parts.push(`${String(mat)} thin leaf(es) gained full canonical metadata keys`);
4625
+ if (report.repairedCorruptLeaves > 0) parts.push(`${String(report.repairedCorruptLeaves)} corrupt repaired`);
4626
+ if (report.missingPathsHydratedFromSource > 0) {
4627
+ parts.push(`${String(report.missingPathsHydratedFromSource)} missing hydrated from source`);
4628
+ }
4629
+ if (parts.length > 0) return `metadata \xB7 ${parts.join(" \xB7 ")}`;
4630
+ return `metadata \xB7 structured terminals unchanged (validated in place)`;
4631
+ }
4632
+ function emitSyncHumanMessages(host, input) {
4633
+ const { result } = input;
4634
+ if (result.missingLocaleCodes.length > 0) {
4635
+ emitRunMessage(host.emit, {
4636
+ op: "sync",
4637
+ runId: host.runId,
4638
+ level: "warn",
4639
+ message: `locale file(s) not found (skipped): ${result.missingLocaleCodes.map((m) => `${m}.json`).join(", ")}`
4640
+ });
4641
+ }
4642
+ if (result.dynamicSites.length > 0) {
4643
+ emitRunMessage(host.emit, {
4644
+ op: "sync",
4645
+ runId: host.runId,
4646
+ level: "warn",
4647
+ message: `${String(result.dynamicSites.length)} translation call(s) use a non-literal key \u2014 sync only aligns locale JSON shapes; computed keys from code are not merged or enumerated.`,
4648
+ data: { dynamicSites: result.dynamicSites.length }
4649
+ });
4650
+ }
4651
+ if (result.sourcePlaceholderLeaves.length > 0) {
4652
+ emitRunMessage(host.emit, {
4653
+ op: "sync",
4654
+ runId: host.runId,
4655
+ level: "warn",
4656
+ message: formatSyncSourcePlaceholderMessage({
4657
+ count: result.sourcePlaceholderLeaves.length,
4658
+ samplePaths: result.sourcePlaceholderLeaves.slice(0, 5).map((leaf) => leaf.path)
4659
+ }),
4660
+ data: { sourcePlaceholderLeaves: result.sourcePlaceholderLeaves.length }
4661
+ });
4662
+ }
4663
+ if (result.targetPlaceholderLeaves.length > 0) {
4664
+ const byLocale = /* @__PURE__ */ new Map();
4665
+ for (const leaf of result.targetPlaceholderLeaves) {
4666
+ byLocale.set(leaf.localeCode, [...byLocale.get(leaf.localeCode) ?? [], leaf]);
4667
+ }
4668
+ for (const [localeCode, leaves] of byLocale) {
4669
+ emitRunMessage(host.emit, {
4670
+ op: "sync",
4671
+ runId: host.runId,
4672
+ level: "warn",
4673
+ message: formatTargetPlaceholderMessage({
4674
+ count: leaves.length,
4675
+ samplePaths: leaves.slice(0, 5).map((leaf) => leaf.path),
4676
+ targetLabel: localeCode
4677
+ }),
4678
+ target: localeCode,
4679
+ data: { targetPlaceholderLeaves: leaves.length }
4680
+ });
4681
+ }
4682
+ }
4683
+ emitRunMessage(host.emit, {
4684
+ op: "sync",
4685
+ runId: host.runId,
4686
+ level: "info",
4687
+ message: `${String(result.fileLines.length)} target file(s) \xB7 ${String(result.dynamicSites.length)} dynamic key site(s)`
4688
+ });
4689
+ const changed = result.fileLines.filter((f) => f.changed).length;
4690
+ const verb = input.dryRun ? "Would change" : "Updated";
4691
+ emitRunMessage(host.emit, {
4692
+ op: "sync",
4693
+ runId: host.runId,
4694
+ level: "info",
4695
+ message: `${verb}: ${String(changed)} \xB7 Unchanged: ${String(result.fileLines.length - changed)}`
4696
+ });
4697
+ const localeGroups = buildSyncLocaleDisplayGroups(result);
4698
+ const segmentedLayout = isSegmentedSyncLayout(localeGroups);
4699
+ const shownLocaleGroups = localeGroups.slice(0, input.listLimit);
4700
+ for (const group of shownLocaleGroups) {
4701
+ emitRunMessage(host.emit, {
4702
+ op: "sync",
4703
+ runId: host.runId,
4704
+ level: "detail",
4705
+ message: formatSyncLocaleFileDetailLine(group, segmentedLayout, input.dryRun),
4706
+ target: group.localeCode,
4707
+ data: { changed: group.changedCount > 0, segmentCount: group.reportKeys.length }
4708
+ });
4709
+ }
4710
+ const omittedLocales = localeGroups.length - shownLocaleGroups.length;
4711
+ const omittedLine = formatSyncFileListOmittedLine(shownLocaleGroups.length, omittedLocales);
4712
+ if (omittedLine) {
4713
+ emitRunMessage(host.emit, {
4714
+ op: "sync",
4715
+ runId: host.runId,
4716
+ level: "detail",
4717
+ message: omittedLine
4718
+ });
4719
+ }
4720
+ if (result.dynamicSites.length > 0) {
4721
+ emitRunMessage(host.emit, {
4722
+ op: "sync",
4723
+ runId: host.runId,
4724
+ level: "info",
4725
+ message: "Dynamic keys are not merged by sync \u2014 see `i18nprune validate` and `i18nprune locales dynamic`."
4726
+ });
4727
+ }
4728
+ const reports = result.payload.localeMetadataReports;
4729
+ const leafGroups = localeGroups.slice(0, input.listLimit);
4730
+ for (const group of leafGroups) {
4731
+ if (group.summaries.length === 0) continue;
4732
+ const merged = mergeSyncHumanLeafSummaries(group.summaries);
4733
+ const label = formatSyncLeafSummaryLabel(group, segmentedLayout);
4734
+ emitRunMessage(host.emit, {
4735
+ op: "sync",
4736
+ runId: host.runId,
4737
+ level: "info",
4738
+ message: `${label}: ${String(merged.hydratedFromSource)} leaf path(s) filled from source \xB7 ${String(merged.preservedExistingLeaves)} kept \xB7 ${String(merged.prunedExtraLeaves)} extra path(s) removed (not in source)`,
4739
+ target: group.localeCode
4740
+ });
4741
+ if (segmentedLayout && group.reportKeys.length > 1) {
4742
+ for (const reportKey of group.reportKeys) {
4743
+ const metaLine = syncLocaleMetadataDetailLine(
4744
+ reports?.[reportKey],
4745
+ input.explicitStripMetadata,
4746
+ input.explicitMetadata
4747
+ );
4748
+ if (metaLine === void 0) continue;
4749
+ emitRunMessage(host.emit, {
4750
+ op: "sync",
4751
+ runId: host.runId,
4752
+ level: "detail",
4753
+ message: ` \u25CF ${reportKey} \xB7 ${metaLine}`,
4754
+ target: group.localeCode
4755
+ });
4756
+ }
4757
+ } else {
4758
+ const reportKey = group.reportKeys[0];
4759
+ const metaLine = reportKey !== void 0 ? syncLocaleMetadataDetailLine(reports?.[reportKey], input.explicitStripMetadata, input.explicitMetadata) : void 0;
4760
+ if (metaLine !== void 0) {
4761
+ emitRunMessage(host.emit, {
4762
+ op: "sync",
4763
+ runId: host.runId,
4764
+ level: "detail",
4765
+ message: ` \u25CF ${metaLine}`,
4766
+ target: group.localeCode
4767
+ });
4768
+ }
4769
+ }
4770
+ }
4771
+ }
4772
+ function runSync(ctx, opts, host) {
4773
+ host.emitProgress({ type: "run.progress.sync", phase: "scan_dynamic_sites" });
4774
+ const analysis = resolveProjectAnalysis(ctx, { emit: host.emit, op: "sync", runId: host.runId });
4775
+ const observations = analysis.keyObservations;
4776
+ const dynamicSites = analysis.dynamicSites;
4777
+ const schemaPaths = analysis.usage.resolvedKeys;
4778
+ const eff = resolveReferenceConfig("sync", ctx.config);
4779
+ const refCtx = buildKeyReferenceContextFromReportDetails(observations, dynamicSites, eff);
4780
+ host.emitProgress({
4781
+ type: "run.progress.sync",
4782
+ phase: "scan_dynamic_sites",
4783
+ current: dynamicSites.length,
4784
+ total: dynamicSites.length,
4785
+ label: `${String(dynamicSites.length)} dynamic key site(s)`
4786
+ });
4787
+ const sourcePath = ctx.paths.sourceLocale;
4788
+ host.emitProgress({ type: "run.progress.sync", phase: "read_source", label: sourcePath });
4789
+ const dir = ctx.paths.localesDir;
4790
+ const sel = parseSyncLangSelection(opts.target);
4791
+ if (sel.mode === "codes") {
4792
+ for (const code of sel.codes) {
4793
+ assertNotSourceTargetLocale("sync", code, sourcePath, {
4794
+ paths: ctx.paths,
4795
+ path: ctx.adapters.path
4796
+ });
4797
+ }
4798
+ }
4799
+ const { segments: targets, missingLocaleCodes } = resolveLocaleSegmentTargets(ctx, { selection: sel });
4800
+ host.emitProgress({
4801
+ type: "run.progress.sync",
4802
+ phase: "resolve_targets",
4803
+ total: targets.length
4804
+ });
4805
+ let updated = 0;
4806
+ const fileLines = [];
4807
+ const humanLeafSummaryByLocaleFile = {};
4808
+ const explicitStripMetadata = opts.stripMetadata === true;
4809
+ const explicitMetadata = opts.metadata === true;
4810
+ const modeDecision = resolveLocaleLeafMode({
4811
+ metadataFlag: explicitMetadata,
4812
+ stripMetadataFlag: explicitStripMetadata
4813
+ });
4814
+ const effectiveSchemaPaths = resolveGlobalSyncSchemaPaths(ctx, schemaPaths);
4815
+ const mergedSourceLeaves = readSourceLocaleLeaves(ctx);
4816
+ const sourcePlaceholderLeaves = detectSourcePlaceholderLeaves(
4817
+ mergedSourceLeaves,
4818
+ sourcePlaceholderValues(ctx.config.missing?.placeholder)
4819
+ );
4820
+ const sourcePlaceholderPaths = new Set(sourcePlaceholderLeaves.map((leaf) => leaf.path));
4821
+ const localeMetadataReports = {};
4822
+ const targetPlaceholderLeaves = [];
4823
+ for (let i = 0; i < targets.length; i++) {
4824
+ const segment = targets[i];
4825
+ const file = segment.reportKey;
4826
+ const full = segment.absolutePath;
4827
+ const segmentSource = resolveSyncSegmentSourcePlan(ctx, {
4828
+ targetSegmentRelativePath: segment.relativePath,
4829
+ targetLocaleCode: segment.locale,
4830
+ schemaPaths: effectiveSchemaPaths
4831
+ });
4832
+ const segmentFillLeaves = segmentSource.effectiveSourceLeaves.filter(
4833
+ (leaf) => !sourcePlaceholderPaths.has(leaf.path)
4834
+ );
4835
+ let segmentTemplate = buildSegmentTemplateFromSource(segmentSource.sourceRaw, segmentFillLeaves);
4836
+ const segmentSourceMap = new Map(segmentFillLeaves.map((leaf) => [leaf.path, leaf.value]));
4837
+ host.emitProgress({
4838
+ type: "run.progress.sync",
4839
+ phase: "build_target",
4840
+ target: file,
4841
+ current: i + 1,
4842
+ total: targets.length
4843
+ });
4844
+ const segmentLeafPaths = segmentFillLeaves.map((leaf) => leaf.path);
4845
+ const curRaw = readLocaleJsonFromContextSync(ctx, full);
4846
+ const cur = normalizeLocaleDocumentToNestedCanonical(curRaw, segmentLeafPaths);
4847
+ const targetCode = segment.locale;
4848
+ const targetPlaceholdersForFile = detectLocalePlaceholderLeaves({
4849
+ leaves: collectTranslationSurfaceLeaves(cur),
4850
+ placeholderValues: sourcePlaceholderValues(ctx.config.missing?.placeholder),
4851
+ localeRole: "target",
4852
+ localeCode: targetCode,
4853
+ localePath: full
4854
+ });
4855
+ targetPlaceholderLeaves.push(...targetPlaceholdersForFile);
4856
+ const targetPlaceholderPaths = targetPlaceholdersForFile.map((leaf) => leaf.path);
4857
+ const mergeOpts = eff.uncertainKeyPolicy === "protect" || eff.uncertainKeyPolicy === "warn_only" ? {
4858
+ uncertainKeepPrefixes: refCtx.uncertainPrefixes,
4859
+ skipFillPaths: [...sourcePlaceholderPaths],
4860
+ forceFillPaths: targetPlaceholderPaths
4861
+ } : { skipFillPaths: [...sourcePlaceholderPaths], forceFillPaths: targetPlaceholderPaths };
4862
+ host.emitProgress({
4863
+ type: "run.progress.sync",
4864
+ phase: "merge",
4865
+ target: file,
4866
+ current: i + 1,
4867
+ total: targets.length
4868
+ });
4869
+ const { next } = computeSyncedLocaleJson(
4870
+ segmentTemplate,
4871
+ cur,
4872
+ ctx.config.policies?.preserve,
4873
+ mergeOpts
4874
+ );
4875
+ host.emitProgress({
4876
+ type: "run.progress.sync",
4877
+ phase: "prune",
4878
+ target: file,
4879
+ current: i + 1,
4880
+ total: targets.length
4881
+ });
4882
+ humanLeafSummaryByLocaleFile[file] = summarizeSyncLeavesForHumanLog(segmentFillLeaves, cur, next);
4883
+ let finalNext = next;
4884
+ if (explicitStripMetadata || explicitMetadata) {
4885
+ let metadataInput = next;
4886
+ if (explicitMetadata && targetPlaceholderPaths.length > 0) {
4887
+ for (const placeholderPath of targetPlaceholderPaths) {
4888
+ metadataInput = setAtPath(metadataInput, placeholderPath, null);
4889
+ }
4890
+ }
4891
+ const normalized = applyLocaleLeafMode({
4892
+ localeJson: metadataInput,
4893
+ sourceMap: segmentSourceMap,
4894
+ mode: modeDecision.mode
4895
+ });
4896
+ finalNext = normalized.next;
4897
+ localeMetadataReports[file] = normalized.report;
4898
+ } else {
4899
+ localeMetadataReports[file] = idleLocaleMetadataReportForSkippedSync(segmentSourceMap.size);
4900
+ }
4901
+ finalNext = normalizeLocaleDocumentToNestedCanonical(finalNext, segmentLeafPaths);
4902
+ const finalWouldChange = !localeJsonContentEquals(curRaw, finalNext);
4903
+ if (opts.dryRun) {
4904
+ fileLines.push({ path: full, changed: finalWouldChange });
4905
+ continue;
4906
+ }
4907
+ if (finalWouldChange) {
4908
+ host.emitProgress({
4909
+ type: "run.progress.sync",
4910
+ phase: "write_files",
4911
+ target: file,
4912
+ label: full
4913
+ });
4914
+ writeLocaleJsonFromContextSync(ctx, full, finalNext);
4915
+ updated += 1;
4916
+ }
4917
+ fileLines.push({ path: full, changed: finalWouldChange });
4918
+ }
4919
+ host.emitProgress({
4920
+ type: "run.progress.sync",
4921
+ phase: "done",
4922
+ current: targets.length,
4923
+ total: targets.length
4924
+ });
4925
+ const payload = {
4926
+ kind: "sync",
4927
+ sourcePath,
4928
+ localesDir: dir,
4929
+ targetFiles: targets.length,
4930
+ writtenFiles: updated,
4931
+ dynamicKeySites: dynamicSites.length,
4932
+ dryRun: Boolean(opts.dryRun),
4933
+ files: fileLines,
4934
+ localeMetadataReports
4935
+ };
4936
+ const issues = [
4937
+ ...issuesFromDynamicScanCount(dynamicSites.length),
4938
+ ...issuesFromSyncMissingLocaleFiles(missingLocaleCodes),
4939
+ ...issueFromMetadataFlagConflict(modeDecision.conflict),
4940
+ ...issuesFromSourcePlaceholderLeaves(sourcePlaceholderLeaves),
4941
+ ...issuesFromTargetPlaceholderLeaves(targetPlaceholderLeaves)
4942
+ ];
4943
+ return {
4944
+ payload,
4945
+ issues,
4946
+ fileLines,
4947
+ targets: targets.map((s) => s.reportKey),
4948
+ updated,
4949
+ dynamicSites,
4950
+ keyObservationsCount: observations.length,
4951
+ missingLocaleCodes,
4952
+ humanLeafSummaryByLocaleFile,
4953
+ sourcePlaceholderLeaves,
4954
+ targetPlaceholderLeaves
4955
+ };
4956
+ }
4957
+
4958
+ // src/sync/resolveTargets.ts
4959
+ function resolveSyncTargetFiles(input) {
4960
+ const { localeJsonBasenames, sourceJsonBasename, selection } = input;
4961
+ const allNonSource = localeJsonBasenames.filter((f) => f !== sourceJsonBasename);
4962
+ if (selection.mode === "all") {
4963
+ return { targetFiles: [...allNonSource], missingLocaleCodes: [] };
4964
+ }
4965
+ const codes = selection.codes;
4966
+ const missingLocaleCodes = codes.filter((c) => !allNonSource.includes(`${c}.json`));
4967
+ const targetFiles = codes.map((c) => `${c}.json`).filter((f) => allNonSource.includes(f));
4968
+ return { targetFiles, missingLocaleCodes };
4969
+ }
4970
+
4971
+ export { canonicalTemplatePathForCollectedLeaf, computeSyncedLocaleJson, emitSyncHumanMessages, idleLocaleMetadataReportForSkippedSync, mergeToTemplateShape, pruneToTemplateShape, readLeafDisplayString, resolveSyncTargetFiles, runSync, stripStructuredLeafMetadata, summarizeSyncLeavesForHumanLog };