@i18nprune/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/dist/adapters-gp1lXp0T.d.ts +12 -0
  4. package/dist/capabilities-x74cD2Hu.d.ts +48 -0
  5. package/dist/cleanup.d.ts +64 -0
  6. package/dist/cleanup.js +3999 -0
  7. package/dist/config.d.ts +201 -0
  8. package/dist/config.js +2865 -0
  9. package/dist/coreContext-DMaWLvmB.d.ts +388 -0
  10. package/dist/fs-BUYD8ZhA.d.ts +20 -0
  11. package/dist/generate.d.ts +487 -0
  12. package/dist/generate.js +9389 -0
  13. package/dist/humanEmit-ygNlYX-S.d.ts +79 -0
  14. package/dist/index-BQuLEQ9b.d.ts +7 -0
  15. package/dist/index-B_ow_Xvr.d.ts +97 -0
  16. package/dist/index-BgG01AKL.d.ts +287 -0
  17. package/dist/index-CIzZl4W8.d.ts +124 -0
  18. package/dist/index-Csm1w7XD.d.ts +58 -0
  19. package/dist/index-DLwTogCo.d.ts +43 -0
  20. package/dist/index-DVT26v11.d.ts +61 -0
  21. package/dist/index-DdjljwMj.d.ts +39 -0
  22. package/dist/index-DeIw-cZd.d.ts +52 -0
  23. package/dist/index-X50E1FIX.d.ts +50 -0
  24. package/dist/index.d.ts +9180 -0
  25. package/dist/index.js +21888 -0
  26. package/dist/init.d.ts +86 -0
  27. package/dist/init.js +848 -0
  28. package/dist/listWindow-XEFxQZi1.d.ts +30 -0
  29. package/dist/localeTargetCodes-BBIQjauw.d.ts +11 -0
  30. package/dist/locales.d.ts +39 -0
  31. package/dist/locales.js +2288 -0
  32. package/dist/missing-BVCvgUC8.d.ts +10 -0
  33. package/dist/missing.d.ts +85 -0
  34. package/dist/missing.js +5892 -0
  35. package/dist/modeResolve-cGVaY5Hh.d.ts +25 -0
  36. package/dist/path-Bfn3SAts.d.ts +11 -0
  37. package/dist/profile-BwOP9WKh.d.ts +9 -0
  38. package/dist/providers-0uMEfT6q.d.ts +82 -0
  39. package/dist/prune-c6hKZCv_.d.ts +33 -0
  40. package/dist/quality.d.ts +36 -0
  41. package/dist/quality.js +3868 -0
  42. package/dist/report-D5-6bVFj.d.ts +8 -0
  43. package/dist/report-schema.d.ts +102 -0
  44. package/dist/report-schema.js +42 -0
  45. package/dist/resumeCandidates-xR13eEwt.d.ts +200 -0
  46. package/dist/root-2-kCaBvQ.d.ts +1110 -0
  47. package/dist/runtime/edge.d.ts +21 -0
  48. package/dist/runtime/edge.js +87 -0
  49. package/dist/runtime/helpers/sync.d.ts +16 -0
  50. package/dist/runtime/helpers/sync.js +117 -0
  51. package/dist/runtime/node.d.ts +24 -0
  52. package/dist/runtime/node.js +204 -0
  53. package/dist/runtime/web.d.ts +21 -0
  54. package/dist/runtime/web.js +84 -0
  55. package/dist/shared.d.ts +1177 -0
  56. package/dist/shared.js +4897 -0
  57. package/dist/sourceContext-1LQg3HiQ.d.ts +36 -0
  58. package/dist/sourceSurface-mDtwGo1E.d.ts +122 -0
  59. package/dist/sync.d.ts +86 -0
  60. package/dist/sync.js +4971 -0
  61. package/dist/syncSegment-Bx6He2Mu.d.ts +149 -0
  62. package/dist/targets-EmtKyr6F.d.ts +23 -0
  63. package/dist/template-CGM-_WLT.d.ts +139 -0
  64. package/dist/translate-CIHYp7wi.d.ts +77 -0
  65. package/dist/types/shared.d.ts +21 -0
  66. package/dist/types/shared.js +1 -0
  67. package/dist/types.d.ts +1345 -0
  68. package/dist/types.js +1 -0
  69. package/dist/validate.d.ts +126 -0
  70. package/dist/validate.js +3717 -0
  71. package/package.json +128 -0
package/dist/init.js ADDED
@@ -0,0 +1,848 @@
1
+ // src/shared/translator/utils/orchestration.ts
2
+ var DEFAULT_PROVIDER_RATE_LIMITS = {
3
+ // Google can sustain higher throughput; default to safe-high (32) while allowing user/config up to 64.
4
+ google: { maxConcurrency: 32, rpm: 1920, rps: 32, intervalMs: 32 },
5
+ mymemory: { maxConcurrency: 2, rpm: 60, rps: 1, intervalMs: 1e3 },
6
+ libre: { maxConcurrency: 6, rpm: 120, rps: 2, intervalMs: 500 },
7
+ deepl: { maxConcurrency: 4, rpm: 90, rps: 1.5, intervalMs: 600 },
8
+ llm: { maxConcurrency: 2, rpm: 30, rps: 0.5, intervalMs: 1500 }
9
+ };
10
+ var DOCS_SITE_BASE = "https://docs.i18nprune.dev";
11
+
12
+ // src/shared/docs/urls.ts
13
+ function parseDocsLinkInput(input) {
14
+ const hashIdx = input.indexOf("#");
15
+ const hash = hashIdx >= 0 ? input.slice(hashIdx) : "";
16
+ let p = (hashIdx >= 0 ? input.slice(0, hashIdx) : input).trim().replace(/^\/+/, "");
17
+ p = p.replace(/\/+$/g, "");
18
+ if (p.endsWith(".md")) p = p.slice(0, -3);
19
+ if (p.endsWith(".mdx")) p = p.slice(0, -4);
20
+ if (p === "README" || p.endsWith("/README")) {
21
+ p = p === "README" ? "" : p.slice(0, -"/README".length);
22
+ }
23
+ p = p.replace(/\/+$/g, "");
24
+ p = p.replace(/\/+/g, "/");
25
+ return { core: p, hash };
26
+ }
27
+ function getDocsUrl(path = "") {
28
+ const { core, hash } = parseDocsLinkInput(path);
29
+ if (!core) {
30
+ return `${DOCS_SITE_BASE}${hash}`;
31
+ }
32
+ return `${DOCS_SITE_BASE}/${core}${hash}`;
33
+ }
34
+
35
+ // src/init/presets/fields.ts
36
+ var INIT_PRESET_IDS = [
37
+ "generic",
38
+ "i18next",
39
+ "lingui",
40
+ "next-i18next",
41
+ "next-intl",
42
+ "react-intl"
43
+ ];
44
+ function isInitPresetId(value) {
45
+ return INIT_PRESET_IDS.includes(value);
46
+ }
47
+ function formatInitPresetIdList() {
48
+ return INIT_PRESET_IDS.join(", ");
49
+ }
50
+ var PRESET_FIELDS = {
51
+ generic: {
52
+ locales: { source: "en", directory: "locales" },
53
+ src: "src",
54
+ functions: ["t"]
55
+ },
56
+ "next-intl": {
57
+ locales: { source: "en", directory: "messages" },
58
+ src: "src",
59
+ functions: ["useTranslations", "t"]
60
+ },
61
+ "next-i18next": {
62
+ locales: { source: "en", directory: "public/locales" },
63
+ src: "src",
64
+ functions: ["useTranslation", "t"]
65
+ },
66
+ i18next: {
67
+ locales: { source: "en", directory: "locales" },
68
+ src: "src",
69
+ functions: ["t", "i18n.t"]
70
+ },
71
+ lingui: {
72
+ locales: { source: "en", directory: "locales" },
73
+ src: "src",
74
+ functions: ["t", "Trans"]
75
+ },
76
+ "react-intl": {
77
+ locales: { source: "en", directory: "locales" },
78
+ src: "src",
79
+ functions: ["useIntl", "FormattedMessage"]
80
+ }
81
+ };
82
+ function getInitPresetConfigFields(preset) {
83
+ return PRESET_FIELDS[preset];
84
+ }
85
+
86
+ // src/init/template.ts
87
+ var CACHE_PROFILE_DOCS_URL = getDocsUrl("cli/cache");
88
+ var REFERENCE_DOCS_URL = getDocsUrl("reference");
89
+ var DEFAULT_INIT_CONFIG_IMPORT_SPECIFIER = "i18nprune/core/config";
90
+ function rateLimitLiteral(providerId) {
91
+ const d = DEFAULT_PROVIDER_RATE_LIMITS[providerId];
92
+ return `{ maxConcurrency: ${String(d.maxConcurrency)}, rpm: ${String(d.rpm)}, rps: ${String(d.rps)}, intervalMs: ${String(d.intervalMs)} }`;
93
+ }
94
+ var GOOGLE_RATE_LIMIT_LITERAL = rateLimitLiteral("google");
95
+ var MYMEMORY_RATE_LIMIT_LITERAL = rateLimitLiteral("mymemory");
96
+ var LIBRE_RATE_LIMIT_LITERAL = rateLimitLiteral("libre");
97
+ var DEEPL_RATE_LIMIT_LITERAL = rateLimitLiteral("deepl");
98
+ var LLM_RATE_LIMIT_LITERAL = rateLimitLiteral("llm");
99
+ var TRANSLATE_PROVIDER_COMMENT_ROWS = `
100
+ // The order of \`providers[]\` IS the auto-routing chain (\`policy.routing: 'auto'\` walks
101
+ // top-to-bottom on retryable failures). \`--provider\` / \`I18NPRUNE_TRANSLATE_PROVIDER\`
102
+ // pins an id to the FRONT of the chain without disabling fallback. Set \`enabled: false\`
103
+ // (or comment the row) to skip a provider.
104
+ // Uncomment one row at a time \xB7 set \`translate.primary\` \xB7 use env vars where noted (run \`i18nprune providers\`).
105
+ // { id: 'mymemory', enabled: true, contactEmail: 'you@example.com', rateLimit: ${MYMEMORY_RATE_LIMIT_LITERAL} },
106
+ // { id: 'libre', enabled: true, baseUrl: 'https://libretranslate.com', rateLimit: ${LIBRE_RATE_LIMIT_LITERAL} },
107
+ // { id: 'deepl', enabled: true, apiKey: process.env.I18NPRUNE_TRANSLATE_DEEPL_API_KEY, rateLimit: ${DEEPL_RATE_LIMIT_LITERAL} },
108
+ // {
109
+ // id: 'llm',
110
+ // enabled: true,
111
+ // apiKey: process.env.I18NPRUNE_TRANSLATE_LLM_API_KEY,
112
+ // baseUrl: process.env.I18NPRUNE_TRANSLATE_LLM_BASE_URL ?? 'https://api.openai.com/v1',
113
+ // model: process.env.I18NPRUNE_TRANSLATE_LLM_MODEL ?? 'gpt-4o-mini',
114
+ // rateLimit: ${LLM_RATE_LIMIT_LITERAL},
115
+ // },`;
116
+ var TRANSLATE_POLICY_BLOCK = `
117
+ // Per-outcome verbs consumed by the translate-policy resolver. All keys optional \u2014 these are the safe defaults.
118
+ policy: {
119
+ routing: 'single',
120
+ onRateLimit: 'backoff',
121
+ onTransientFailure: 'retry',
122
+ onQuotaExceeded: 'fallback',
123
+ onAuthFailure: 'abort',
124
+ onProviderUnavailable: 'fallback',
125
+ onIdentityOutput: 'flag',
126
+ onIncompleteRun: 'confirm',
127
+ handoff: 'auto',
128
+ // maxAttempts: providers.length, // omit to use one shot per provider in chain
129
+ },`;
130
+ function formatFunctionsArray(functions) {
131
+ return JSON.stringify(functions);
132
+ }
133
+ function formatLocaleLayoutLines(layout) {
134
+ if (!layout) return "";
135
+ return `
136
+ mode: '${layout.mode}',
137
+ structure: '${layout.structure}',`;
138
+ }
139
+ function formatPresetBody(preset, localeLayout) {
140
+ const p = getInitPresetConfigFields(preset);
141
+ const sf = p.locales.source.replace(/'/g, "\\'");
142
+ const dir = p.locales.directory.replace(/'/g, "\\'");
143
+ const layoutLines = formatLocaleLayoutLines(localeLayout);
144
+ return ` locales: {
145
+ source: '${sf}',
146
+ directory: '${dir}',${layoutLines}
147
+ },
148
+ src: '${p.src.replace(/'/g, "\\'")}',
149
+ functions: ${formatFunctionsArray(p.functions)},`;
150
+ }
151
+ function buildMinimalInitConfigTemplate(importSpecifier, preset, localeLayout) {
152
+ const body = formatPresetBody(preset, localeLayout);
153
+ return `import { defineConfig, type I18nPruneConfig } from '${importSpecifier}';
154
+
155
+ export default defineConfig({
156
+ ${body}
157
+ translate: {
158
+ primary: 'google',
159
+ workers: ${String(DEFAULT_PROVIDER_RATE_LIMITS.google.maxConcurrency)},
160
+ providers: [{ id: 'google', rateLimit: ${GOOGLE_RATE_LIMIT_LITERAL} },${TRANSLATE_PROVIDER_COMMENT_ROWS}
161
+ ],${TRANSLATE_POLICY_BLOCK}
162
+ },
163
+ policies: {
164
+ preserve: {
165
+ // copyKeys: ['brand.tagline'],
166
+ // copyPrefixes: ['legal.'],
167
+ },
168
+ parity: {
169
+ // excludeKeys: ['debug.flag'],
170
+ // excludePrefixes: ['experimental.'],
171
+ // excludeValues: ['TODO'],
172
+ },
173
+ },
174
+ exclude: {
175
+ preset: 'production',
176
+ useDefaultSkip: true,
177
+ // dirs: ['fixtures'],
178
+ // extensions: ['d.ts'],
179
+ // patterns: [/^src\\/generated\\//],
180
+ },
181
+ } satisfies Partial<I18nPruneConfig>);
182
+ `;
183
+ }
184
+ function buildRichInitConfigTemplate(importSpecifier, preset, localeLayout) {
185
+ const body = formatPresetBody(preset, localeLayout);
186
+ return `import { defineConfig, type I18nPruneConfig } from '${importSpecifier}';
187
+
188
+ export default defineConfig({
189
+ ${body}
190
+ translate: {
191
+ primary: 'google',
192
+ workers: ${String(DEFAULT_PROVIDER_RATE_LIMITS.google.maxConcurrency)},
193
+ providers: [
194
+ { id: 'google', rateLimit: ${GOOGLE_RATE_LIMIT_LITERAL} },${TRANSLATE_PROVIDER_COMMENT_ROWS}
195
+ ],
196
+ // Per-outcome verbs consumed by the translate-policy resolver. All keys optional.
197
+ policy: {
198
+ routing: 'single',
199
+ onRateLimit: 'backoff',
200
+ onTransientFailure: 'retry',
201
+ onQuotaExceeded: 'fallback',
202
+ onAuthFailure: 'abort',
203
+ onProviderUnavailable: 'fallback',
204
+ onIdentityOutput: 'flag',
205
+ onIncompleteRun: 'confirm',
206
+ handoff: 'auto',
207
+ // maxAttempts: providers.length, // omit to use one shot per provider in chain
208
+ },
209
+ },
210
+
211
+ policies: {
212
+ preserve: {
213
+ // copyKeys: ['brand.tagline'],
214
+ // copyPrefixes: ['legal.'],
215
+ },
216
+ parity: {
217
+ // excludeKeys: ['debug.flag'],
218
+ // excludePrefixes: ['experimental.'],
219
+ // excludeValues: ['TODO'],
220
+ },
221
+ },
222
+
223
+ exclude: {
224
+ preset: 'production',
225
+ useDefaultSkip: true,
226
+ // dirs: ['fixtures', 'vendor'],
227
+ // files: ['ignored-keys.ts'],
228
+ // extensions: ['d.ts'],
229
+ // patterns: [/^src\\/generated\\//],
230
+ },
231
+
232
+ reference: {
233
+ defaults: {
234
+ treatCommentedCallSitesAsRuntime: false,
235
+ treatNonSourceFileSitesAsRuntime: false,
236
+ uncertainKeyPolicy: 'protect',
237
+ stringPresence: 'guard',
238
+ stringPresenceMaxHitsPerKey: 5,
239
+ respectPreserve: true,
240
+ },
241
+ // Per-command overrides: add \`commands: { cleanup?: {\u2026}, sync?: {\u2026}, generate?: {\u2026} }\` with the same keys as \`defaults\`.
242
+ // Each block shallow-merges over \`defaults\` when that command runs. Docs: ${REFERENCE_DOCS_URL}
243
+ },
244
+
245
+ localeLeaves: {
246
+ // \`mode\`: **\`legacy_string\`** (plain string leaves) or **\`structured\`** (\`{ value, \u2026 }\` terminals) \u2014 only these two values are valid.
247
+ mode: 'legacy_string',
248
+ sync: {
249
+ // When **\`true\`**, sync can strip structured metadata back to plain strings (see \`sync --strip-metadata\`).
250
+ stripMetadata: false,
251
+ },
252
+ },
253
+
254
+ missing: {
255
+ // Omit \`placeholder\` to use the SDK default sentinel; set any string merged at new paths (grep-friendly recommended).
256
+ placeholder: '__I18NPRUNE_MISSING__',
257
+ },
258
+ output: {
259
+ list: {
260
+ top: 10,
261
+ full: false,
262
+ // maxCap: 100000,
263
+ },
264
+ },
265
+
266
+ scanner: {
267
+ // \`auto\` picks serial vs concurrent; tune for huge repos.
268
+ mode: 'auto',
269
+ // concurrency: 8,
270
+ // hardCap: 256,
271
+ },
272
+
273
+ cache: {
274
+ // Preset: safe | balanced | fast (default when omitted: balanced). ${CACHE_PROFILE_DOCS_URL}
275
+ profile: 'balanced',
276
+ // On-disk scan cache for faster repeat runs. Omit \`dir\` for the CLI default (~/.i18nprune/cache).
277
+ enabled: true,
278
+ // --- Optional overrides (uncomment to replace the profile value for that field only) ---
279
+ // dir: '.i18nprune/cache',
280
+ // mode: 'readWrite', // or 'readOnly' (CI: read cache, never write)
281
+ // rebuild: 'partial', // or 'full' \u2014 always full scan on analysis miss
282
+ // fullRescanThresholdPercent: 40, // partial only: full scan when this % of src files change
283
+ },
284
+
285
+ patching: {
286
+ // Opt-in: may create/refresh loader wiring under \`src\` \u2014 keep \`enabled: false\` until you deliberately adopt the patching recipe.
287
+ enabled: false,
288
+ recipe: 'loader_generated',
289
+ mode: 'warn_skip',
290
+ // loaderPath: 'src/i18n/loaders.generated.ts',
291
+ // configPath: 'src/i18n/config.json',
292
+ // localeJsonImportBase: 'locales',
293
+ // sizeLimitBytes: 524288,
294
+ },
295
+
296
+ } satisfies Partial<I18nPruneConfig>);
297
+ `;
298
+ }
299
+ function buildInitConfigTemplate(importSpecifierOrOpts) {
300
+ if (typeof importSpecifierOrOpts === "string") {
301
+ return buildMinimalInitConfigTemplate(importSpecifierOrOpts, "generic");
302
+ }
303
+ const opts = importSpecifierOrOpts ?? {};
304
+ const importSpecifier = opts.importSpecifier ?? DEFAULT_INIT_CONFIG_IMPORT_SPECIFIER;
305
+ const preset = opts.preset ?? "generic";
306
+ const layout = opts.localeLayout;
307
+ return opts.rich ? buildRichInitConfigTemplate(importSpecifier, preset, layout) : buildMinimalInitConfigTemplate(importSpecifier, preset, layout);
308
+ }
309
+ function configFileNameForFormat(baseName, format) {
310
+ return `${baseName}.${format}`;
311
+ }
312
+ function defaultInitConfigFileName(baseName) {
313
+ return configFileNameForFormat(baseName, "ts");
314
+ }
315
+
316
+ // src/shared/errors/internal.ts
317
+ var I18nPruneError = class extends Error {
318
+ code;
319
+ issueCode;
320
+ constructor(message, code, options) {
321
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
322
+ this.name = "I18nPruneError";
323
+ this.code = code;
324
+ this.issueCode = options?.issueCode;
325
+ }
326
+ };
327
+
328
+ // src/runtime/helpers/sync/assert.ts
329
+ function isThenable(value) {
330
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
331
+ }
332
+ function assertSyncPortResult(value, label, at) {
333
+ if (isThenable(value)) {
334
+ throw new I18nPruneError(
335
+ `Synchronous ${label} requires a plain value (got a Promise at ${at})`,
336
+ "USAGE"
337
+ );
338
+ }
339
+ return value;
340
+ }
341
+
342
+ // src/runtime/helpers/sync/fs.ts
343
+ function readRuntimeFsTextSync(filePath, fs) {
344
+ return assertSyncPortResult(fs.readText(filePath), "fs.readText", filePath);
345
+ }
346
+ function existsRuntimeFsSync(filePath, fs) {
347
+ return assertSyncPortResult(fs.exists(filePath), "fs.exists", filePath);
348
+ }
349
+ function listRuntimeFsDirSync(dirPath, fs) {
350
+ return assertSyncPortResult(fs.listDir(dirPath), "fs.listDir", dirPath);
351
+ }
352
+
353
+ // src/init/detect/packageJson.ts
354
+ function readInitPackageJson(host, projectRoot) {
355
+ const pkgPath = host.path.join(projectRoot, "package.json");
356
+ if (!existsRuntimeFsSync(pkgPath, host.fs)) return null;
357
+ try {
358
+ const raw = readRuntimeFsTextSync(pkgPath, host.fs);
359
+ const parsed = JSON.parse(raw);
360
+ const dependencies = parsed.dependencies && typeof parsed.dependencies === "object" && parsed.dependencies !== null ? parsed.dependencies : {};
361
+ const devDependencies = parsed.devDependencies && typeof parsed.devDependencies === "object" && parsed.devDependencies !== null ? parsed.devDependencies : {};
362
+ return { dependencies, devDependencies };
363
+ } catch {
364
+ return null;
365
+ }
366
+ }
367
+ function hasDep(pkg, name) {
368
+ if (!pkg) return false;
369
+ return Object.hasOwn(pkg.dependencies, name) || Object.hasOwn(pkg.devDependencies, name);
370
+ }
371
+ function initPackageDeclares(pkg, name) {
372
+ return hasDep(pkg, name);
373
+ }
374
+
375
+ // src/init/detect/localeTopology.ts
376
+ var NEXT_CONFIG_NAMES = ["next.config.js", "next.config.mjs", "next.config.ts"];
377
+ var LOCALE_ROOT_CANDIDATES = [
378
+ "messages",
379
+ "locales",
380
+ "translations",
381
+ "i18n",
382
+ "lang",
383
+ "public/locales"
384
+ ];
385
+ function readInitTopologySignals(host, projectRoot) {
386
+ const localeRoots = [];
387
+ for (const rel of LOCALE_ROOT_CANDIDATES) {
388
+ const abs = host.path.join(projectRoot, rel);
389
+ if (!existsRuntimeFsSync(abs, host.fs)) continue;
390
+ const kind = assertSyncPortResult(host.fs.statKind(abs), "fs.statKind", abs);
391
+ if (kind === "directory" || kind === "file") {
392
+ localeRoots.push(rel);
393
+ }
394
+ }
395
+ let nextConfigPresent = false;
396
+ for (const name of NEXT_CONFIG_NAMES) {
397
+ const p = host.path.join(projectRoot, name);
398
+ if (existsRuntimeFsSync(p, host.fs)) {
399
+ nextConfigPresent = true;
400
+ break;
401
+ }
402
+ }
403
+ return { localeRoots, nextConfigPresent };
404
+ }
405
+
406
+ // src/shared/locales/enumerate/parseSegmentLocale.ts
407
+ function localeCodeForSegment(structure, path, segment) {
408
+ if (structure === "locale_file") {
409
+ if (segment.relativePath.includes("/")) return null;
410
+ return path.basename(segment.absolutePath, ".json");
411
+ }
412
+ if (structure === "locale_per_dir") {
413
+ const slash = segment.relativePath.indexOf("/");
414
+ if (slash < 0) return null;
415
+ const locale = segment.relativePath.slice(0, slash);
416
+ return locale.length > 0 ? locale : null;
417
+ }
418
+ if (structure === "feature_bundle") {
419
+ return path.basename(segment.absolutePath, ".json");
420
+ }
421
+ return null;
422
+ }
423
+
424
+ // src/shared/constants/locales.ts
425
+ var MAX_LOCALE_SEGMENT_TREE_DEPTH = 16;
426
+
427
+ // src/shared/locales/enumerate/walkJsonTree.ts
428
+ function posixRelative(pathApi, root, absolute) {
429
+ let rel = pathApi.relative(root, absolute);
430
+ if (rel.startsWith("..") || pathApi.isAbsolute(rel)) {
431
+ rel = pathApi.basename(absolute);
432
+ }
433
+ return rel.replace(/\\/g, "/");
434
+ }
435
+ function walkLocaleJsonSegments(input) {
436
+ const { fs, path, rootAbsolute} = input;
437
+ const maxDepth = input.maxDepth ?? MAX_LOCALE_SEGMENT_TREE_DEPTH;
438
+ const out = [];
439
+ function visit(dirAbsolute, depth) {
440
+ if (!existsRuntimeFsSync(dirAbsolute, fs)) return;
441
+ const entries = listRuntimeFsDirSync(dirAbsolute, fs);
442
+ for (const entry of entries) {
443
+ const childAbsolute = path.join(dirAbsolute, entry.name);
444
+ if (entry.kind === "file" && entry.name.endsWith(".json")) {
445
+ out.push({
446
+ absolutePath: childAbsolute,
447
+ relativePath: posixRelative(path, rootAbsolute, childAbsolute)
448
+ });
449
+ } else if (entry.kind === "directory" && depth < maxDepth) {
450
+ visit(childAbsolute, depth + 1);
451
+ }
452
+ }
453
+ }
454
+ visit(rootAbsolute, 0);
455
+ return out;
456
+ }
457
+
458
+ // src/init/detect/localeFilesystemLayout.ts
459
+ var STRUCTURES = ["locale_file", "locale_per_dir", "feature_bundle"];
460
+ var LIKELY_LOCALE_CODE = /^[a-z]{2}(-[A-Z]{2})?$/i;
461
+ function isLikelyLocaleCode(code) {
462
+ return LIKELY_LOCALE_CODE.test(code);
463
+ }
464
+ function classifySegment(path, segment) {
465
+ if (!segment.relativePath.includes("/")) {
466
+ return localeCodeForSegment("locale_file", path, segment) !== null ? "locale_file" : "unknown";
467
+ }
468
+ const slash = segment.relativePath.indexOf("/");
469
+ const firstSegment = segment.relativePath.slice(0, slash);
470
+ const basenameLocale = path.basename(segment.absolutePath, ".json");
471
+ const perDir = localeCodeForSegment("locale_per_dir", path, segment);
472
+ const bundle = localeCodeForSegment("feature_bundle", path, segment);
473
+ if (perDir !== null && bundle !== null) {
474
+ const firstLooksLocale = isLikelyLocaleCode(firstSegment);
475
+ const baseLooksLocale = isLikelyLocaleCode(basenameLocale);
476
+ if (baseLooksLocale && !firstLooksLocale) return "feature_bundle";
477
+ if (firstLooksLocale && !baseLooksLocale) return "locale_per_dir";
478
+ return "unknown";
479
+ }
480
+ if (perDir !== null) return "locale_per_dir";
481
+ if (bundle !== null) return "feature_bundle";
482
+ return "unknown";
483
+ }
484
+ function detectLocaleFilesystemLayout(host, projectRoot, localesDirectory) {
485
+ const rootAbsolute = host.path.join(projectRoot, localesDirectory);
486
+ if (!existsRuntimeFsSync(rootAbsolute, host.fs)) {
487
+ return null;
488
+ }
489
+ const segments = walkLocaleJsonSegments({
490
+ fs: host.fs,
491
+ path: host.path,
492
+ rootAbsolute});
493
+ if (segments.length === 0) {
494
+ return null;
495
+ }
496
+ const counts = {
497
+ locale_file: 0,
498
+ locale_per_dir: 0,
499
+ feature_bundle: 0,
500
+ unknown: 0
501
+ };
502
+ for (const segment of segments) {
503
+ const kind = classifySegment(host.path, segment);
504
+ counts[kind] += 1;
505
+ }
506
+ if (counts.unknown > 0) {
507
+ return null;
508
+ }
509
+ let winner = null;
510
+ let winnerCount = 0;
511
+ for (const structure of STRUCTURES) {
512
+ const n = counts[structure];
513
+ if (n > winnerCount) {
514
+ winner = structure;
515
+ winnerCount = n;
516
+ }
517
+ }
518
+ if (winner === null || winnerCount !== segments.length) {
519
+ return null;
520
+ }
521
+ const mode = winner === "locale_file" ? "flat_file" : "locale_directory";
522
+ return {
523
+ mode,
524
+ structure: winner,
525
+ confidence: 1,
526
+ segmentCount: segments.length
527
+ };
528
+ }
529
+
530
+ // src/config/locales/sourceValidate.ts
531
+ var LANGUAGE_CODE_SHAPE = /^[a-z]{2}(-[a-z0-9]{2,8})*$/i;
532
+ function classifyLocalesSourceInput(raw) {
533
+ const trimmed = raw.trim();
534
+ if (trimmed.length === 0) return "invalid_shape";
535
+ if (trimmed.includes("/") || trimmed.includes("\\")) return "path";
536
+ if (trimmed.toLowerCase().endsWith(".json")) return "json_filename";
537
+ if (!LANGUAGE_CODE_SHAPE.test(trimmed)) return "invalid_shape";
538
+ return "language_code";
539
+ }
540
+
541
+ // src/init/detect/inferLayoutFromConfigPaths.ts
542
+ var LIKELY_LOCALE_CODE2 = /^[a-z]{2}(-[A-Z]{2})?$/i;
543
+ function isLikelyLocaleCode2(code) {
544
+ return LIKELY_LOCALE_CODE2.test(code);
545
+ }
546
+ function relativeSourcePath(directory, source) {
547
+ const dir = directory.replace(/\\/g, "/").replace(/\/$/, "");
548
+ const src = source.replace(/\\/g, "/");
549
+ if (src.startsWith(`${dir}/`)) {
550
+ return src.slice(dir.length + 1);
551
+ }
552
+ return src;
553
+ }
554
+ function inferLocaleLayoutFromConfigPaths(directory, source) {
555
+ if (classifyLocalesSourceInput(source) === "language_code") {
556
+ return null;
557
+ }
558
+ const rel = relativeSourcePath(directory, source);
559
+ if (!rel.endsWith(".json")) {
560
+ return null;
561
+ }
562
+ if (!rel.includes("/")) {
563
+ return {
564
+ mode: "flat_file",
565
+ structure: "locale_file",
566
+ confidence: 0.85,
567
+ segmentCount: 1
568
+ };
569
+ }
570
+ const slash = rel.indexOf("/");
571
+ const first = rel.slice(0, slash);
572
+ const basenameLocale = rel.slice(slash + 1).replace(/\.json$/, "");
573
+ if (isLikelyLocaleCode2(basenameLocale) && !isLikelyLocaleCode2(first)) {
574
+ return {
575
+ mode: "locale_directory",
576
+ structure: "feature_bundle",
577
+ confidence: 0.85,
578
+ segmentCount: 1
579
+ };
580
+ }
581
+ if (isLikelyLocaleCode2(first) && !isLikelyLocaleCode2(basenameLocale)) {
582
+ return {
583
+ mode: "locale_directory",
584
+ structure: "locale_per_dir",
585
+ confidence: 0.85,
586
+ segmentCount: 1
587
+ };
588
+ }
589
+ return null;
590
+ }
591
+
592
+ // src/init/detect/scorePresets.ts
593
+ function sumContributions(factors) {
594
+ let s = 0;
595
+ for (const f of factors) s += f.contribution;
596
+ return s;
597
+ }
598
+ function normalizeConfidence(score) {
599
+ if (!Number.isFinite(score) || score <= 0) return 0;
600
+ return Math.min(1, score);
601
+ }
602
+ function buildFactors(preset, signals) {
603
+ const pkg = signals.packageJson;
604
+ const { topology } = signals;
605
+ const factors = [];
606
+ if (preset === "generic") {
607
+ factors.push({
608
+ id: "baseline.generic",
609
+ contribution: 0.12,
610
+ detail: "Always-available neutral starter"
611
+ });
612
+ }
613
+ if (preset === "next-intl") {
614
+ if (initPackageDeclares(pkg, "next-intl")) {
615
+ factors.push({
616
+ id: "npm.next-intl",
617
+ contribution: 0.48,
618
+ detail: "`next-intl` declared in package.json"
619
+ });
620
+ }
621
+ if (topology.localeRoots.includes("messages")) {
622
+ factors.push({
623
+ id: "dir.messages",
624
+ contribution: 0.28,
625
+ detail: "`messages/` directory present"
626
+ });
627
+ }
628
+ if (topology.nextConfigPresent && initPackageDeclares(pkg, "next")) {
629
+ factors.push({
630
+ id: "conv.next_app",
631
+ contribution: 0.12,
632
+ detail: "`next` dependency and Next config file present"
633
+ });
634
+ }
635
+ }
636
+ if (preset === "next-i18next") {
637
+ if (initPackageDeclares(pkg, "next-i18next")) {
638
+ factors.push({
639
+ id: "npm.next-i18next",
640
+ contribution: 0.46,
641
+ detail: "`next-i18next` declared in package.json"
642
+ });
643
+ }
644
+ if (topology.localeRoots.includes("public/locales")) {
645
+ factors.push({
646
+ id: "dir.public_locales",
647
+ contribution: 0.26,
648
+ detail: "`public/locales/` directory present"
649
+ });
650
+ }
651
+ if (topology.nextConfigPresent && initPackageDeclares(pkg, "next")) {
652
+ factors.push({
653
+ id: "conv.next_app",
654
+ contribution: 0.1,
655
+ detail: "`next` dependency and Next config file present"
656
+ });
657
+ }
658
+ }
659
+ if (preset === "i18next") {
660
+ if (initPackageDeclares(pkg, "i18next")) {
661
+ factors.push({
662
+ id: "npm.i18next",
663
+ contribution: 0.42,
664
+ detail: "`i18next` declared in package.json"
665
+ });
666
+ }
667
+ if (initPackageDeclares(pkg, "react-i18next")) {
668
+ factors.push({
669
+ id: "npm.react-i18next",
670
+ contribution: 0.22,
671
+ detail: "`react-i18next` declared in package.json"
672
+ });
673
+ }
674
+ if (topology.localeRoots.includes("locales") || topology.localeRoots.includes("public/locales")) {
675
+ factors.push({
676
+ id: "dir.locales_family",
677
+ contribution: 0.2,
678
+ detail: "`locales/` or `public/locales/` directory present"
679
+ });
680
+ }
681
+ }
682
+ if (preset === "react-intl") {
683
+ if (initPackageDeclares(pkg, "react-intl")) {
684
+ factors.push({
685
+ id: "npm.react-intl",
686
+ contribution: 0.44,
687
+ detail: "`react-intl` declared in package.json"
688
+ });
689
+ }
690
+ if (initPackageDeclares(pkg, "@formatjs/intl")) {
691
+ factors.push({
692
+ id: "npm.formatjs_intl",
693
+ contribution: 0.18,
694
+ detail: "`@formatjs/intl` declared in package.json"
695
+ });
696
+ }
697
+ if (topology.localeRoots.includes("lang") || topology.localeRoots.includes("locales")) {
698
+ factors.push({
699
+ id: "dir.lang_or_locales",
700
+ contribution: 0.12,
701
+ detail: "`lang/` or `locales/` directory present"
702
+ });
703
+ }
704
+ }
705
+ if (preset === "lingui") {
706
+ if (initPackageDeclares(pkg, "@lingui/core") || initPackageDeclares(pkg, "@lingui/react")) {
707
+ factors.push({
708
+ id: "npm.lingui",
709
+ contribution: 0.46,
710
+ detail: "`@lingui/core` or `@lingui/react` declared in package.json"
711
+ });
712
+ }
713
+ if (topology.localeRoots.includes("locales")) {
714
+ factors.push({
715
+ id: "dir.locales",
716
+ contribution: 0.14,
717
+ detail: "`locales/` directory present"
718
+ });
719
+ }
720
+ }
721
+ return factors;
722
+ }
723
+ var FRAMEWORK_PRESETS = ["next-intl", "next-i18next", "i18next"];
724
+ function applyConflictDampingInPlace(rows) {
725
+ for (const r of rows) {
726
+ r.score = r.rawScore;
727
+ }
728
+ const strong = FRAMEWORK_PRESETS.map((id) => rows.find((x) => x.preset === id)).filter(
729
+ (x) => Boolean(x && x.rawScore >= 0.32)
730
+ );
731
+ if (strong.length >= 2) {
732
+ const factor = 0.72;
733
+ for (const r of strong) {
734
+ r.score = r.rawScore * factor;
735
+ }
736
+ }
737
+ }
738
+ function scoreInitPresets(signals) {
739
+ const rows = INIT_PRESET_IDS.map((preset) => {
740
+ const factors = buildFactors(preset, signals);
741
+ const rawScore = sumContributions(factors);
742
+ return { preset, rawScore, score: 0, confidence: 0, factors };
743
+ });
744
+ applyConflictDampingInPlace(rows);
745
+ for (const r of rows) {
746
+ r.confidence = normalizeConfidence(r.score);
747
+ }
748
+ rows.sort((a, b) => b.score - a.score);
749
+ return rows;
750
+ }
751
+ function pickTopInitPreset(scores) {
752
+ return scores[0]?.preset ?? "generic";
753
+ }
754
+ function isInitAutoAmbiguous(scores) {
755
+ if (scores.length < 2) return false;
756
+ const [a, b] = scores;
757
+ if (a.preset === "generic" && a.score <= 0.2 && b.score < 0.08) {
758
+ return false;
759
+ }
760
+ if (a.score < 0.28) return true;
761
+ if (a.score - b.score < 0.1) return true;
762
+ return false;
763
+ }
764
+
765
+ // src/init/detect/project.ts
766
+ function detectInitProject(host, projectRoot) {
767
+ const packageJson = readInitPackageJson(host, projectRoot);
768
+ const topology = readInitTopologySignals(host, projectRoot);
769
+ const signals = { packageJson, topology };
770
+ const scores = scoreInitPresets(signals);
771
+ return { signals, scores };
772
+ }
773
+
774
+ // src/init/run.ts
775
+ function runInit(host, opts = {}) {
776
+ if (host.skippedExistingConfig) {
777
+ const payload2 = {
778
+ kind: "init",
779
+ schemaVersion: 1,
780
+ skippedExistingConfig: true,
781
+ preset: "generic",
782
+ proposedConfigSource: "",
783
+ proposedConfigFileName: defaultInitConfigFileName("i18nprune.config")
784
+ };
785
+ return { payload: payload2, issues: [], exitCode: 0 };
786
+ }
787
+ const presetArg = opts.preset;
788
+ if (presetArg !== void 0 && !isInitPresetId(presetArg)) {
789
+ const issue = {
790
+ severity: "error",
791
+ code: "i18nprune.init.unknown_preset",
792
+ message: `Unknown init preset "${String(presetArg)}" (expected one of: ${formatInitPresetIdList()}).`
793
+ };
794
+ const payload2 = {
795
+ kind: "init",
796
+ schemaVersion: 1,
797
+ skippedExistingConfig: false,
798
+ preset: "generic",
799
+ proposedConfigSource: "",
800
+ proposedConfigFileName: defaultInitConfigFileName("i18nprune.config")
801
+ };
802
+ return { payload: payload2, issues: [issue], exitCode: 1 };
803
+ }
804
+ const { signals, scores } = detectInitProject(host, host.projectRoot);
805
+ const ambiguous = Boolean(opts.auto) && presetArg === void 0 && isInitAutoAmbiguous(scores);
806
+ let preset = "generic";
807
+ if (presetArg !== void 0) {
808
+ preset = presetArg;
809
+ } else if (opts.auto) {
810
+ preset = pickTopInitPreset(scores);
811
+ }
812
+ const presetFields = getInitPresetConfigFields(preset);
813
+ const localeLayout = detectLocaleFilesystemLayout(host, host.projectRoot, presetFields.locales.directory) ?? inferLocaleLayoutFromConfigPaths(presetFields.locales.directory, presetFields.locales.source);
814
+ const detection = {
815
+ signals,
816
+ scores,
817
+ ambiguous,
818
+ localeLayout
819
+ };
820
+ const issues = [];
821
+ let exitCode = 0;
822
+ if (ambiguous) {
823
+ issues.push({
824
+ severity: "error",
825
+ code: "i18nprune.init.ambiguous_auto",
826
+ message: `Could not pick a unique preset from project signals \u2014 pass \`--preset <${formatInitPresetIdList()}>\` or retry without \`--auto\`.`
827
+ });
828
+ exitCode = 1;
829
+ }
830
+ const proposedConfigSource = exitCode === 1 ? "" : buildInitConfigTemplate({
831
+ importSpecifier: opts.importSpecifier,
832
+ rich: opts.rich,
833
+ preset,
834
+ localeLayout
835
+ });
836
+ const payload = {
837
+ kind: "init",
838
+ schemaVersion: 1,
839
+ skippedExistingConfig: false,
840
+ preset,
841
+ proposedConfigSource,
842
+ proposedConfigFileName: defaultInitConfigFileName("i18nprune.config"),
843
+ detection
844
+ };
845
+ return { payload, issues, exitCode };
846
+ }
847
+
848
+ export { DEFAULT_INIT_CONFIG_IMPORT_SPECIFIER, INIT_PRESET_IDS, buildInitConfigTemplate, configFileNameForFormat, defaultInitConfigFileName, detectInitProject, detectLocaleFilesystemLayout, formatInitPresetIdList, getInitPresetConfigFields, initPackageDeclares, isInitAutoAmbiguous, isInitPresetId, pickTopInitPreset, readInitPackageJson, readInitTopologySignals, runInit, scoreInitPresets };