@i18nprune/core 0.1.0

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