@fluenti/cli 0.2.0 → 0.3.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 (79) hide show
  1. package/dist/ai-provider.d.ts +14 -0
  2. package/dist/ai-provider.d.ts.map +1 -0
  3. package/dist/catalog.d.ts +1 -1
  4. package/dist/catalog.d.ts.map +1 -1
  5. package/dist/check.d.ts +47 -0
  6. package/dist/check.d.ts.map +1 -0
  7. package/dist/cli.cjs +12 -9
  8. package/dist/cli.cjs.map +1 -1
  9. package/dist/cli.js +655 -198
  10. package/dist/cli.js.map +1 -1
  11. package/dist/compile-BZGNcCK1.js +192 -0
  12. package/dist/compile-BZGNcCK1.js.map +1 -0
  13. package/dist/compile-DoeVtT7S.cjs +8 -0
  14. package/dist/compile-DoeVtT7S.cjs.map +1 -0
  15. package/dist/compile-cache.d.ts +26 -0
  16. package/dist/compile-cache.d.ts.map +1 -0
  17. package/dist/compile-runner.d.ts +4 -1
  18. package/dist/compile-runner.d.ts.map +1 -1
  19. package/dist/compile-worker.cjs +2 -0
  20. package/dist/compile-worker.cjs.map +1 -0
  21. package/dist/compile-worker.d.ts +18 -0
  22. package/dist/compile-worker.d.ts.map +1 -0
  23. package/dist/compile-worker.js +14 -0
  24. package/dist/compile-worker.js.map +1 -0
  25. package/dist/compile.d.ts.map +1 -1
  26. package/dist/config-loader.d.ts +1 -6
  27. package/dist/config-loader.d.ts.map +1 -1
  28. package/dist/config.d.ts +2 -2
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/extract-cache-1XVokN31.js +304 -0
  31. package/dist/extract-cache-1XVokN31.js.map +1 -0
  32. package/dist/extract-cache-BVyzqc__.cjs +10 -0
  33. package/dist/extract-cache-BVyzqc__.cjs.map +1 -0
  34. package/dist/extract-cache.d.ts +33 -0
  35. package/dist/extract-cache.d.ts.map +1 -0
  36. package/dist/extract-runner.d.ts +12 -0
  37. package/dist/extract-runner.d.ts.map +1 -0
  38. package/dist/glossary.d.ts +5 -0
  39. package/dist/glossary.d.ts.map +1 -0
  40. package/dist/index.cjs +1 -1
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.ts +5 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +117 -26
  45. package/dist/index.js.map +1 -1
  46. package/dist/lint.d.ts +36 -0
  47. package/dist/lint.d.ts.map +1 -0
  48. package/dist/parallel-compile.d.ts +26 -0
  49. package/dist/parallel-compile.d.ts.map +1 -0
  50. package/dist/translate.d.ts +2 -1
  51. package/dist/translate.d.ts.map +1 -1
  52. package/dist/{tsx-extractor-DZrY1LMS.js → tsx-extractor-B9fnGNTG.js} +61 -53
  53. package/dist/tsx-extractor-B9fnGNTG.js.map +1 -0
  54. package/dist/tsx-extractor-Bi9AXMhH.cjs +2 -0
  55. package/dist/tsx-extractor-Bi9AXMhH.cjs.map +1 -0
  56. package/dist/tsx-extractor.d.ts +2 -2
  57. package/dist/tsx-extractor.d.ts.map +1 -1
  58. package/dist/validation.d.ts +16 -0
  59. package/dist/validation.d.ts.map +1 -0
  60. package/dist/vue-extractor.cjs +3 -0
  61. package/dist/vue-extractor.cjs.map +1 -0
  62. package/dist/vue-extractor.d.ts +2 -2
  63. package/dist/vue-extractor.d.ts.map +1 -1
  64. package/dist/vue-extractor.js +203 -0
  65. package/dist/vue-extractor.js.map +1 -0
  66. package/llms-full.txt +297 -0
  67. package/llms.txt +86 -0
  68. package/package.json +29 -3
  69. package/dist/config-loader-CcqRnMzw.js +0 -387
  70. package/dist/config-loader-CcqRnMzw.js.map +0 -1
  71. package/dist/config-loader-DV5Yqrg5.cjs +0 -16
  72. package/dist/config-loader-DV5Yqrg5.cjs.map +0 -1
  73. package/dist/tsx-extractor-DNg_iUSd.cjs +0 -2
  74. package/dist/tsx-extractor-DNg_iUSd.cjs.map +0 -1
  75. package/dist/tsx-extractor-DZrY1LMS.js.map +0 -1
  76. package/dist/vue-extractor-DWETY0BN.cjs +0 -3
  77. package/dist/vue-extractor-DWETY0BN.cjs.map +0 -1
  78. package/dist/vue-extractor-iUl6SUkv.js +0 -210
  79. package/dist/vue-extractor-iUl6SUkv.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,37 +1,268 @@
1
1
  #!/usr/bin/env node
2
- import { t as e } from "./tsx-extractor-DZrY1LMS.js";
3
- import { a as t, c as n, i as r, l as i, n as a, o, r as s, s as c, t as l, u } from "./config-loader-CcqRnMzw.js";
4
- import { appendFileSync as d, existsSync as f, mkdirSync as p, readFileSync as m, writeFileSync as h } from "node:fs";
5
- import { dirname as g, extname as _, join as v, resolve as y } from "node:path";
6
- import { defineCommand as b, runMain as x } from "citty";
7
- import S from "consola";
2
+ import { i as e, n as t, r as n, t as r } from "./compile-BZGNcCK1.js";
3
+ import { t as i } from "./tsx-extractor-B9fnGNTG.js";
4
+ import { a, c as o, i as s, n as c, o as l, r as u, s as d, t as f } from "./extract-cache-1XVokN31.js";
5
+ import { resolveLocaleCodes as p } from "@fluenti/core/internal";
6
+ import { appendFileSync as m, existsSync as h, mkdirSync as g, readFileSync as _, writeFileSync as v } from "node:fs";
7
+ import { dirname as y, extname as b, join as x, resolve as S } from "node:path";
8
8
  import C from "fast-glob";
9
- import { execFile as w } from "node:child_process";
10
- import { promisify as T } from "node:util";
9
+ import { createHash as w } from "node:crypto";
10
+ import { defineCommand as T, runMain as E } from "citty";
11
+ import D from "consola";
12
+ import { execFile as O } from "node:child_process";
13
+ import { promisify as k } from "node:util";
11
14
  //#region src/stats-format.ts
12
- var ee = "█", E = "░";
13
- function D(e, t = 20) {
15
+ var A = "█", ee = "░";
16
+ function j(e, t = 20) {
14
17
  let n = Math.max(0, Math.min(100, e)), r = Math.round(n / 100 * t);
15
- return ee.repeat(r) + E.repeat(t - r);
18
+ return A.repeat(r) + ee.repeat(t - r);
16
19
  }
17
- function O(e) {
20
+ function te(e) {
18
21
  let t = e.toFixed(1) + "%";
19
22
  return e >= 90 ? `\x1b[32m${t}\x1b[0m` : e >= 70 ? `\x1b[33m${t}\x1b[0m` : `\x1b[31m${t}\x1b[0m`;
20
23
  }
21
- function te(e, t, n) {
22
- let r = t > 0 ? n / t * 100 : 0, i = t > 0 ? O(r) : "—", a = t > 0 ? D(r) : "";
24
+ function ne(e, t, n) {
25
+ let r = t > 0 ? n / t * 100 : 0, i = t > 0 ? te(r) : "—", a = t > 0 ? j(r) : "";
23
26
  return ` ${e.padEnd(8)}│ ${String(t).padStart(5)} │ ${String(n).padStart(10)} │ ${a} ${i}`;
24
27
  }
25
28
  //#endregion
29
+ //#region src/lint.ts
30
+ function M(e, t) {
31
+ let n = [], { sourceLocale: r } = t, i = t.locales ?? Object.keys(e), a = e[r];
32
+ if (!a) return n.push({
33
+ rule: "missing-source",
34
+ severity: "error",
35
+ message: `Source locale catalog "${r}" not found`
36
+ }), n;
37
+ let o = Object.entries(a).filter(([, e]) => !e.obsolete).map(([e]) => e);
38
+ for (let t of i) {
39
+ if (t === r) continue;
40
+ let i = e[t];
41
+ if (!i) {
42
+ n.push({
43
+ rule: "missing-locale",
44
+ severity: "error",
45
+ message: `Catalog for locale "${t}" not found`,
46
+ locale: t
47
+ });
48
+ continue;
49
+ }
50
+ for (let e of o) {
51
+ let r = a[e], o = i[e];
52
+ if (!o || !o.translation || o.translation.length === 0) {
53
+ n.push({
54
+ rule: "missing-translation",
55
+ severity: "error",
56
+ message: `Missing translation for "${e}" in locale "${t}"`,
57
+ messageId: e,
58
+ locale: t
59
+ });
60
+ continue;
61
+ }
62
+ let s = N(r.message ?? e), c = N(o.translation), l = s.filter((e) => !c.includes(e)), u = c.filter((e) => !s.includes(e));
63
+ l.length > 0 && n.push({
64
+ rule: "inconsistent-placeholders",
65
+ severity: "error",
66
+ message: `Translation for "${e}" in "${t}" is missing placeholders: ${l.map((e) => `{${e}}`).join(", ")}`,
67
+ messageId: e,
68
+ locale: t
69
+ }), u.length > 0 && n.push({
70
+ rule: "inconsistent-placeholders",
71
+ severity: "warning",
72
+ message: `Translation for "${e}" in "${t}" has extra placeholders: ${u.map((e) => `{${e}}`).join(", ")}`,
73
+ messageId: e,
74
+ locale: t
75
+ }), o.fuzzy && n.push({
76
+ rule: "fuzzy-translation",
77
+ severity: "warning",
78
+ message: `Translation for "${e}" in "${t}" is marked as fuzzy`,
79
+ messageId: e,
80
+ locale: t
81
+ });
82
+ }
83
+ for (let [e, r] of Object.entries(i)) r.obsolete || (!a[e] || a[e].obsolete) && n.push({
84
+ rule: "orphan-translation",
85
+ severity: "warning",
86
+ message: `Translation "${e}" in "${t}" has no corresponding source message`,
87
+ messageId: e,
88
+ locale: t
89
+ });
90
+ }
91
+ let s = /* @__PURE__ */ new Map();
92
+ for (let [e, t] of Object.entries(a)) {
93
+ if (t.obsolete) continue;
94
+ let n = t.message ?? e, r = s.get(n);
95
+ r ? r.push(e) : s.set(n, [e]);
96
+ }
97
+ for (let [e, t] of s) t.length > 1 && n.push({
98
+ rule: "duplicate-message",
99
+ severity: "info",
100
+ message: `Duplicate source message "${e.slice(0, 60)}${e.length > 60 ? "..." : ""}" used by ${t.length} entries: ${t.join(", ")}`,
101
+ locale: r
102
+ });
103
+ return n;
104
+ }
105
+ function N(e) {
106
+ let t = [], n = /\{(\w+)(?:\s*,\s*(?:plural|select|selectordinal|number|date|time))?/g, r;
107
+ for (; (r = n.exec(e)) !== null;) {
108
+ let e = r[1];
109
+ t.includes(e) || t.push(e);
110
+ }
111
+ return t.sort();
112
+ }
113
+ function P(e) {
114
+ if (e.length === 0) return " ✓ All checks passed";
115
+ let t = [], n = re(e, (e) => e.rule);
116
+ for (let [e, r] of Object.entries(n)) {
117
+ t.push(` ${e} (${r.length}):`);
118
+ for (let e of r) {
119
+ let n = e.severity === "error" ? "✗" : e.severity === "warning" ? "⚠" : "ℹ";
120
+ t.push(` ${n} ${e.message}`);
121
+ }
122
+ }
123
+ let r = e.filter((e) => e.severity === "error").length, i = e.filter((e) => e.severity === "warning").length, a = e.filter((e) => e.severity === "info").length;
124
+ return t.push(""), t.push(` Summary: ${r} errors, ${i} warnings, ${a} info`), t.join("\n");
125
+ }
126
+ function re(e, t) {
127
+ let n = {};
128
+ for (let r of e) {
129
+ let e = t(r);
130
+ (n[e] ?? (n[e] = [])).push(r);
131
+ }
132
+ return n;
133
+ }
134
+ //#endregion
135
+ //#region src/check.ts
136
+ function ie(e, t) {
137
+ let { sourceLocale: n, minCoverage: r, locale: i } = t, a = e[n];
138
+ if (!a) return {
139
+ results: [],
140
+ passed: !1,
141
+ minCoverage: r,
142
+ actualCoverage: 0,
143
+ diagnostics: [{
144
+ rule: "missing-source",
145
+ severity: "error",
146
+ message: `Source locale catalog "${n}" not found`
147
+ }]
148
+ };
149
+ let o = Object.entries(a).filter(([, e]) => !e.obsolete).map(([e]) => e), s = o.length, c = i ? [i] : Object.keys(e).filter((e) => e !== n), l = [];
150
+ for (let t of c) {
151
+ let n = e[t];
152
+ if (!n) {
153
+ l.push({
154
+ locale: t,
155
+ total: s,
156
+ translated: 0,
157
+ missing: s,
158
+ fuzzy: 0,
159
+ coverage: 0
160
+ });
161
+ continue;
162
+ }
163
+ let r = 0, i = 0, a = 0;
164
+ for (let e of o) {
165
+ let t = n[e];
166
+ !t || !t.translation || t.translation.length === 0 ? i++ : (r++, t.fuzzy && a++);
167
+ }
168
+ let c = s > 0 ? r / s * 100 : 100;
169
+ l.push({
170
+ locale: t,
171
+ total: s,
172
+ translated: r,
173
+ missing: i,
174
+ fuzzy: a,
175
+ coverage: c
176
+ });
177
+ }
178
+ let u = l.reduce((e, t) => e + t.translated, 0), d = l.reduce((e, t) => e + t.total, 0), f = d > 0 ? u / d * 100 : 100, p = l.every((e) => e.coverage >= r), m = { sourceLocale: n };
179
+ return i && (m.locales = [n, i]), {
180
+ results: l,
181
+ passed: p,
182
+ minCoverage: r,
183
+ actualCoverage: f,
184
+ diagnostics: M(e, m)
185
+ };
186
+ }
187
+ function ae(e) {
188
+ let t = [];
189
+ for (let n of e.results) {
190
+ let r = n.coverage >= e.minCoverage ? "✓" : "✗", i = n.coverage.toFixed(1), a = n.missing > 0 ? ` — ${n.missing} missing` : "", o = n.fuzzy > 0 ? `, ${n.fuzzy} fuzzy` : "";
191
+ t.push(`${r} ${n.locale}: ${i}% (${n.translated}/${n.total})${a}${o}`);
192
+ }
193
+ t.push("");
194
+ let n = e.actualCoverage.toFixed(1), r = e.passed ? "PASSED" : "FAILED";
195
+ return t.push(`Coverage: ${n}% (min: ${e.minCoverage}%) — ${r}`), t.join("\n");
196
+ }
197
+ function oe(e, t, n) {
198
+ let r = [], i = n === "json" ? ".json" : ".po";
199
+ for (let n of e.results) if (n.coverage < e.minCoverage) {
200
+ let a = `${t}/${n.locale}${i}`;
201
+ r.push(`::error file=${a}::Translation coverage ${n.coverage.toFixed(1)}% below minimum ${e.minCoverage}%`);
202
+ }
203
+ let a = e.diagnostics.filter((e) => e.rule === "missing-translation");
204
+ for (let e of a) if (e.locale) {
205
+ let n = `${t}/${e.locale}${i}`;
206
+ r.push(`::warning file=${n}::${e.message}`);
207
+ }
208
+ return r.join("\n");
209
+ }
210
+ function se(e) {
211
+ return JSON.stringify({
212
+ results: e.results,
213
+ passed: e.passed,
214
+ minCoverage: e.minCoverage,
215
+ actualCoverage: Math.round(e.actualCoverage * 10) / 10
216
+ }, null, 2);
217
+ }
218
+ //#endregion
219
+ //#region src/compile-cache.ts
220
+ var F = "1", ce = class {
221
+ data;
222
+ cachePath;
223
+ dirty = !1;
224
+ constructor(e, t) {
225
+ this.cachePath = S(t ? S(e, ".cache", t) : S(e, ".cache"), "compile-cache.json"), this.data = this.load();
226
+ }
227
+ isUpToDate(e, t) {
228
+ let n = this.data.entries[e];
229
+ if (!n) return !1;
230
+ let r = I(t);
231
+ return n.inputHash === r;
232
+ }
233
+ set(e, t) {
234
+ this.data.entries[e] = { inputHash: I(t) }, this.dirty = !0;
235
+ }
236
+ save() {
237
+ this.dirty &&= (g(y(this.cachePath), { recursive: !0 }), v(this.cachePath, JSON.stringify(this.data), "utf-8"), !1);
238
+ }
239
+ load() {
240
+ try {
241
+ if (h(this.cachePath)) {
242
+ let e = _(this.cachePath, "utf-8"), t = JSON.parse(e);
243
+ if (t.version === F) return t;
244
+ }
245
+ } catch {}
246
+ return {
247
+ version: F,
248
+ entries: {}
249
+ };
250
+ }
251
+ };
252
+ function I(e) {
253
+ return w("md5").update(e).digest("hex");
254
+ }
255
+ //#endregion
26
256
  //#region src/translate.ts
27
- var k = T(w);
28
- function A(e, t, n) {
29
- let r = JSON.stringify(n, null, 2);
257
+ var L = k(O);
258
+ function R(e, t, n, r) {
259
+ let i = JSON.stringify(n, null, 2);
30
260
  return [
31
261
  `You are a professional translator. Translate the following messages from "${e}" to "${t}".`,
32
262
  "",
263
+ ...r ? [`Project context: ${r}`, ""] : [],
33
264
  "Input (JSON):",
34
- r,
265
+ i,
35
266
  "",
36
267
  "Rules:",
37
268
  "- Output ONLY valid JSON with the same keys and translated values.",
@@ -40,14 +271,14 @@ function A(e, t, n) {
40
271
  "- Do not add any explanation or markdown formatting, output raw JSON only."
41
272
  ].join("\n");
42
273
  }
43
- async function j(e, t) {
274
+ async function z(e, t) {
44
275
  let n = 10 * 1024 * 1024;
45
276
  try {
46
277
  if (e === "claude") {
47
- let { stdout: e } = await k("claude", ["-p", t], { maxBuffer: n });
278
+ let { stdout: e } = await L("claude", ["-p", t], { maxBuffer: n });
48
279
  return e;
49
280
  } else {
50
- let { stdout: e } = await k("codex", [
281
+ let { stdout: e } = await L("codex", [
51
282
  "-p",
52
283
  t,
53
284
  "--full-auto"
@@ -58,19 +289,24 @@ async function j(e, t) {
58
289
  throw t.code === "ENOENT" ? Error(`"${e}" CLI not found. Please install it first:\n` + (e === "claude" ? " npm install -g @anthropic-ai/claude-code" : " npm install -g @openai/codex")) : t;
59
290
  }
60
291
  }
61
- function M(e) {
292
+ function B(e) {
62
293
  let t = e.match(/\{[\s\S]*\}/);
63
294
  if (!t) throw Error("No JSON object found in AI response");
64
- let n = JSON.parse(t[0]);
295
+ let n;
296
+ try {
297
+ n = JSON.parse(t[0]);
298
+ } catch {
299
+ throw Error(`Failed to parse JSON from AI response: ${t[0].slice(0, 200)}`);
300
+ }
65
301
  if (typeof n != "object" || !n || Array.isArray(n)) throw Error("AI response is not a valid JSON object");
66
302
  return n;
67
303
  }
68
- function N(e) {
304
+ function V(e) {
69
305
  let t = {};
70
306
  for (let [n, r] of Object.entries(e)) r.obsolete || (!r.translation || r.translation.length === 0) && (t[n] = r.message ?? n);
71
307
  return t;
72
308
  }
73
- function P(e, t) {
309
+ function H(e, t) {
74
310
  let n = Object.keys(e), r = [];
75
311
  for (let i = 0; i < n.length; i += t) {
76
312
  let a = {};
@@ -79,31 +315,31 @@ function P(e, t) {
79
315
  }
80
316
  return r;
81
317
  }
82
- async function F(e) {
83
- let { provider: t, sourceLocale: n, targetLocale: r, catalog: i, batchSize: a } = e, o = N(i), s = Object.keys(o).length;
84
- if (s === 0) return {
318
+ async function U(e) {
319
+ let { provider: t, sourceLocale: n, targetLocale: r, catalog: i, batchSize: a, context: o } = e, s = V(i), c = Object.keys(s).length;
320
+ if (c === 0) return {
85
321
  catalog: { ...i },
86
322
  translated: 0
87
323
  };
88
- S.info(` ${s} untranslated messages, translating with ${t}...`);
89
- let c = { ...i }, l = P(o, a), u = 0;
90
- for (let e = 0; e < l.length; e++) {
91
- let i = l[e], a = Object.keys(i);
92
- l.length > 1 && S.info(` Batch ${e + 1}/${l.length} (${a.length} messages)`);
93
- let o = M(await j(t, A(n, r, i)));
94
- for (let e of a) o[e] && typeof o[e] == "string" ? (c[e] = {
95
- ...c[e],
96
- translation: o[e]
97
- }, u++) : S.warn(` Missing translation for key: ${e}`);
324
+ D.info(` ${c} untranslated messages, translating with ${t}...`);
325
+ let l = { ...i }, u = H(s, a), d = 0;
326
+ for (let e = 0; e < u.length; e++) {
327
+ let i = u[e], a = Object.keys(i);
328
+ u.length > 1 && D.info(` Batch ${e + 1}/${u.length} (${a.length} messages)`);
329
+ let s = B(await z(t, R(n, r, i, o)));
330
+ for (let e of a) s[e] && typeof s[e] == "string" ? (l[e] = {
331
+ ...l[e],
332
+ translation: s[e]
333
+ }, d++) : D.warn(` Missing translation for key: ${e}`);
98
334
  }
99
335
  return {
100
- catalog: c,
101
- translated: u
336
+ catalog: l,
337
+ translated: d
102
338
  };
103
339
  }
104
340
  //#endregion
105
341
  //#region src/migrate.ts
106
- var I = T(w), L = {
342
+ var W = k(O), G = {
107
343
  "vue-i18n": {
108
344
  name: "vue-i18n",
109
345
  framework: "Vue",
@@ -243,29 +479,29 @@ var I = T(w), L = {
243
479
  ],
244
480
  migrationGuide: "react/llms-migration.txt"
245
481
  }
246
- }, R = Object.keys(L);
247
- function z(e) {
482
+ }, K = Object.keys(G);
483
+ function le(e) {
248
484
  let t = e.toLowerCase().replace(/^@nuxtjs\//, "nuxt-").replace(/^@/, "");
249
- return R.find((e) => e === t);
485
+ return K.find((e) => e === t);
250
486
  }
251
- async function B(e) {
487
+ async function ue(e) {
252
488
  let t = {
253
489
  configFiles: [],
254
490
  localeFiles: [],
255
491
  sampleSources: [],
256
492
  packageJson: void 0
257
- }, n = y("package.json");
258
- f(n) && (t.packageJson = m(n, "utf-8"));
493
+ }, n = S("package.json");
494
+ h(n) && (t.packageJson = _(n, "utf-8"));
259
495
  for (let n of e.configPatterns) {
260
- let e = y(n);
261
- f(e) && t.configFiles.push({
496
+ let e = S(n);
497
+ h(e) && t.configFiles.push({
262
498
  path: n,
263
- content: m(e, "utf-8")
499
+ content: _(e, "utf-8")
264
500
  });
265
501
  }
266
502
  let r = await C(e.localePatterns, { absolute: !1 });
267
503
  for (let e of r.slice(0, 10)) {
268
- let n = m(y(e), "utf-8");
504
+ let n = _(S(e), "utf-8");
269
505
  t.localeFiles.push({
270
506
  path: e,
271
507
  content: n.length > 5e3 ? n.slice(0, 5e3) + "\n... (truncated)" : n
@@ -273,7 +509,7 @@ async function B(e) {
273
509
  }
274
510
  let i = await C(e.sourcePatterns, { absolute: !1 });
275
511
  for (let e of i.slice(0, 5)) {
276
- let n = m(y(e), "utf-8");
512
+ let n = _(S(e), "utf-8");
277
513
  t.sampleSources.push({
278
514
  path: e,
279
515
  content: n.length > 3e3 ? n.slice(0, 3e3) + "\n... (truncated)" : n
@@ -281,16 +517,16 @@ async function B(e) {
281
517
  }
282
518
  return t;
283
519
  }
284
- function V(e) {
520
+ function de(e) {
285
521
  let t = [
286
- y("node_modules", "@fluenti", "cli", "..", "..", e),
287
- v(__dirname, "..", "..", "..", e),
288
- v(__dirname, "..", "..", e)
522
+ S("node_modules", "@fluenti", "cli", "..", "..", e),
523
+ x(__dirname, "..", "..", "..", e),
524
+ x(__dirname, "..", "..", e)
289
525
  ];
290
- for (let e of t) if (f(e)) return m(e, "utf-8");
526
+ for (let e of t) if (h(e)) return _(e, "utf-8");
291
527
  return "";
292
528
  }
293
- function H(e, t, n) {
529
+ function fe(e, t, n) {
294
530
  let r = [];
295
531
  if (r.push(`You are a migration assistant helping convert a ${e.framework} project from "${e.name}" to Fluenti (@fluenti).`, "", "Your task:", "1. Generate a `fluenti.config.ts` file based on the existing i18n configuration", "2. Convert each locale/translation file to Fluenti PO format", "3. List the code changes needed (file by file) to migrate source code from the old API to Fluenti API", ""), n && r.push("=== MIGRATION GUIDE ===", n, ""), t.packageJson && r.push("=== package.json ===", t.packageJson, ""), t.configFiles.length > 0) {
296
532
  r.push("=== EXISTING CONFIG FILES ===");
@@ -306,14 +542,14 @@ function H(e, t, n) {
306
542
  }
307
543
  return r.push("", "=== OUTPUT FORMAT ===", "Respond with the following sections, each starting with the exact header shown:", "", "### FLUENTI_CONFIG", "```ts", "// The fluenti.config.ts content", "```", "", "### LOCALE_FILES", "For each locale file, output:", "#### LOCALE: {locale_code}", "```po", "// The PO file content", "```", "", "### MIGRATION_STEPS", "A numbered checklist of specific code changes needed, with before/after examples.", "", "### INSTALL_COMMANDS", "```bash", "// The install and uninstall commands", "```"), r.join("\n");
308
544
  }
309
- async function U(e, t) {
545
+ async function pe(e, t) {
310
546
  let n = 10 * 1024 * 1024;
311
547
  try {
312
548
  if (e === "claude") {
313
- let { stdout: e } = await I("claude", ["-p", t], { maxBuffer: n });
549
+ let { stdout: e } = await W("claude", ["-p", t], { maxBuffer: n });
314
550
  return e;
315
551
  } else {
316
- let { stdout: e } = await I("codex", [
552
+ let { stdout: e } = await W("codex", [
317
553
  "-p",
318
554
  t,
319
555
  "--full-auto"
@@ -324,7 +560,7 @@ async function U(e, t) {
324
560
  throw t.code === "ENOENT" ? Error(`"${e}" CLI not found. Please install it first:\n` + (e === "claude" ? " npm install -g @anthropic-ai/claude-code" : " npm install -g @openai/codex")) : t;
325
561
  }
326
562
  }
327
- function W(e) {
563
+ function me(e) {
328
564
  let t = {
329
565
  config: void 0,
330
566
  localeFiles: [],
@@ -345,58 +581,58 @@ function W(e) {
345
581
  let a = e.match(/### INSTALL_COMMANDS[\s\S]*?```(?:bash|sh)?\n([\s\S]*?)```/);
346
582
  return a && (t.installCommands = a[1].trim()), t;
347
583
  }
348
- async function G(e) {
349
- let { from: t, provider: n, write: r } = e, i = z(t);
584
+ async function he(e) {
585
+ let { from: t, provider: n, write: r } = e, i = le(t);
350
586
  if (!i) {
351
- S.error(`Unsupported library "${t}". Supported libraries:`);
352
- for (let e of R) S.log(` - ${e}`);
587
+ D.error(`Unsupported library "${t}". Supported libraries:`);
588
+ for (let e of K) D.log(` - ${e}`);
353
589
  return;
354
590
  }
355
- let a = L[i];
356
- S.info(`Migrating from ${a.name} (${a.framework}) to Fluenti`), S.info("Scanning project for existing i18n files...");
357
- let o = await B(a);
591
+ let a = G[i];
592
+ D.info(`Migrating from ${a.name} (${a.framework}) to Fluenti`), D.info("Scanning project for existing i18n files...");
593
+ let o = await ue(a);
358
594
  if (o.configFiles.length === 0 && o.localeFiles.length === 0) {
359
- S.warn(`No ${a.name} configuration or locale files found.`), S.info("Make sure you are running this command from the project root directory.");
595
+ D.warn(`No ${a.name} configuration or locale files found.`), D.info("Make sure you are running this command from the project root directory.");
360
596
  return;
361
597
  }
362
- S.info(`Found: ${o.configFiles.length} config file(s), ${o.localeFiles.length} locale file(s), ${o.sampleSources.length} source file(s)`);
363
- let s = V(a.migrationGuide);
364
- S.info(`Generating migration plan with ${n}...`);
365
- let c = W(await U(n, H(a, o, s)));
366
- if (c.installCommands && (S.log(""), S.box({
598
+ D.info(`Found: ${o.configFiles.length} config file(s), ${o.localeFiles.length} locale file(s), ${o.sampleSources.length} source file(s)`);
599
+ let s = de(a.migrationGuide);
600
+ D.info(`Generating migration plan with ${n}...`);
601
+ let c = me(await pe(n, fe(a, o, s)));
602
+ if (c.installCommands && (D.log(""), D.box({
367
603
  title: "Install Commands",
368
604
  message: c.installCommands
369
605
  })), c.config) if (r) {
370
- let { writeFileSync: e } = await import("node:fs"), t = y("fluenti.config.ts");
371
- e(t, c.config, "utf-8"), S.success(`Written: ${t}`);
372
- } else S.log(""), S.box({
606
+ let { writeFileSync: e } = await import("node:fs"), t = S("fluenti.config.ts");
607
+ e(t, c.config, "utf-8"), D.success(`Written: ${t}`);
608
+ } else D.log(""), D.box({
373
609
  title: "fluenti.config.ts",
374
610
  message: c.config
375
611
  });
376
612
  if (c.localeFiles.length > 0) if (r) {
377
613
  let { writeFileSync: e, mkdirSync: t } = await import("node:fs"), n = "./locales";
378
- t(y(n), { recursive: !0 });
614
+ t(S(n), { recursive: !0 });
379
615
  for (let t of c.localeFiles) {
380
- let r = y(n, `${t.locale}.po`);
381
- e(r, t.content, "utf-8"), S.success(`Written: ${r}`);
616
+ let r = S(n, `${t.locale}.po`);
617
+ e(r, t.content, "utf-8"), D.success(`Written: ${r}`);
382
618
  }
383
- } else for (let e of c.localeFiles) S.log(""), S.box({
619
+ } else for (let e of c.localeFiles) D.log(""), D.box({
384
620
  title: `locales/${e.locale}.po`,
385
621
  message: e.content.length > 500 ? e.content.slice(0, 500) + "\n... (use --write to save full file)" : e.content
386
622
  });
387
- c.steps && (S.log(""), S.box({
623
+ c.steps && (D.log(""), D.box({
388
624
  title: "Migration Steps",
389
625
  message: c.steps
390
- })), !r && (c.config || c.localeFiles.length > 0) && (S.log(""), S.info("Run with --write to save generated files to disk:"), S.log(` fluenti migrate --from ${t} --write`));
626
+ })), !r && (c.config || c.localeFiles.length > 0) && (D.log(""), D.info("Run with --write to save generated files to disk:"), D.log(` fluenti migrate --from ${t} --write`));
391
627
  }
392
628
  //#endregion
393
629
  //#region src/init.ts
394
- var K = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*$/;
630
+ var ge = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*$/;
395
631
  function q(e) {
396
- if (!K.test(e)) throw Error(`Invalid locale format: "${e}"`);
632
+ if (!ge.test(e)) throw Error(`Invalid locale format: "${e}"`);
397
633
  return e;
398
634
  }
399
- var J = [
635
+ var _e = [
400
636
  {
401
637
  dep: "next",
402
638
  name: "nextjs",
@@ -428,8 +664,8 @@ var J = [
428
664
  pluginPackage: "@fluenti/react"
429
665
  }
430
666
  ];
431
- function Y(e) {
432
- for (let t of J) if (t.dep in e) return {
667
+ function ve(e) {
668
+ for (let t of _e) if (t.dep in e) return {
433
669
  name: t.name,
434
670
  pluginPackage: t.pluginPackage
435
671
  };
@@ -438,7 +674,7 @@ function Y(e) {
438
674
  pluginPackage: null
439
675
  };
440
676
  }
441
- function X(e) {
677
+ function ye(e) {
442
678
  let t = e.locales.map((e) => `'${e}'`).join(", ");
443
679
  return `import { defineConfig } from '@fluenti/cli'
444
680
 
@@ -452,35 +688,35 @@ export default defineConfig({
452
688
  })
453
689
  `;
454
690
  }
455
- async function Z(e) {
456
- let t = y(e.cwd, "package.json");
457
- if (!f(t)) {
458
- S.error("No package.json found in current directory.");
691
+ async function be(e) {
692
+ let t = S(e.cwd, "package.json");
693
+ if (!h(t)) {
694
+ D.error("No package.json found in current directory.");
459
695
  return;
460
696
  }
461
- let n = JSON.parse(m(t, "utf-8")), r = Y({
697
+ let n = JSON.parse(_(t, "utf-8")), r = ve({
462
698
  ...n.dependencies,
463
699
  ...n.devDependencies
464
700
  });
465
- S.info(`Detected framework: ${r.name}`), r.pluginPackage && S.info(`Recommended plugin: ${r.pluginPackage}`);
466
- let i = y(e.cwd, "fluenti.config.ts");
467
- if (f(i)) {
468
- S.warn("fluenti.config.ts already exists. Skipping config generation.");
701
+ D.info(`Detected framework: ${r.name}`), r.pluginPackage && D.info(`Recommended plugin: ${r.pluginPackage}`);
702
+ let i = S(e.cwd, "fluenti.config.ts");
703
+ if (h(i)) {
704
+ D.warn("fluenti.config.ts already exists. Skipping config generation.");
469
705
  return;
470
706
  }
471
- let a = await S.prompt("Source locale?", {
707
+ let a = await D.prompt("Source locale?", {
472
708
  type: "text",
473
709
  default: "en",
474
710
  placeholder: "en"
475
711
  });
476
712
  if (typeof a == "symbol") return;
477
- let o = await S.prompt("Target locales (comma-separated)?", {
713
+ let o = await D.prompt("Target locales (comma-separated)?", {
478
714
  type: "text",
479
715
  default: "ja,zh-CN",
480
716
  placeholder: "ja,zh-CN"
481
717
  });
482
718
  if (typeof o == "symbol") return;
483
- let s = await S.prompt("Catalog format?", {
719
+ let s = await D.prompt("Catalog format?", {
484
720
  type: "select",
485
721
  options: ["po", "json"],
486
722
  initial: "po"
@@ -489,25 +725,25 @@ async function Z(e) {
489
725
  let c = o.split(",").map((e) => e.trim()).filter(Boolean);
490
726
  q(a);
491
727
  for (let e of c) q(e);
492
- h(i, X({
728
+ v(i, ye({
493
729
  sourceLocale: a,
494
730
  locales: [a, ...c.filter((e) => e !== a)],
495
731
  format: s
496
- }), "utf-8"), S.success("Created fluenti.config.ts");
497
- let l = y(e.cwd, ".gitignore"), u = "src/locales/compiled/";
498
- f(l) ? m(l, "utf-8").includes(u) || (d(l, `\n# Fluenti compiled catalogs\n${u}\n`), S.success("Updated .gitignore")) : (h(l, `# Fluenti compiled catalogs\n${u}\n`), S.success("Created .gitignore"));
499
- let p = n.scripts ?? {}, g = {}, _ = !1;
500
- if (p["i18n:extract"] || (g["i18n:extract"] = "fluenti extract", _ = !0), p["i18n:compile"] || (g["i18n:compile"] = "fluenti compile", _ = !0), _) {
732
+ }), "utf-8"), D.success("Created fluenti.config.ts");
733
+ let l = S(e.cwd, ".gitignore"), u = "src/locales/compiled/";
734
+ h(l) ? _(l, "utf-8").includes(u) || (m(l, `\n# Fluenti compiled catalogs\n${u}\n`), D.success("Updated .gitignore")) : (v(l, `# Fluenti compiled catalogs\n${u}\n`), D.success("Created .gitignore"));
735
+ let d = n.scripts ?? {}, f = {}, p = !1;
736
+ if (d["i18n:extract"] || (f["i18n:extract"] = "fluenti extract", p = !0), d["i18n:compile"] || (f["i18n:compile"] = "fluenti compile", p = !0), p) {
501
737
  let e = {
502
738
  ...n,
503
739
  scripts: {
504
- ...p,
505
- ...g
740
+ ...d,
741
+ ...f
506
742
  }
507
743
  };
508
- h(t, JSON.stringify(e, null, 2) + "\n", "utf-8"), S.success("Added i18n:extract and i18n:compile scripts to package.json");
744
+ v(t, JSON.stringify(e, null, 2) + "\n", "utf-8"), D.success("Added i18n:extract and i18n:compile scripts to package.json");
509
745
  }
510
- S.log(""), S.box({
746
+ D.log(""), D.box({
511
747
  title: "Next steps",
512
748
  message: [
513
749
  r.pluginPackage ? `1. Install: pnpm add -D ${r.pluginPackage} @fluenti/cli` : "1. Install: pnpm add -D @fluenti/cli",
@@ -520,24 +756,27 @@ async function Z(e) {
520
756
  }
521
757
  //#endregion
522
758
  //#region src/cli.ts
523
- function Q(e, t) {
524
- if (!f(e)) return {};
525
- let r = m(e, "utf-8");
526
- return t === "json" ? n(r) : o(r);
527
- }
528
- function $(e, t, n) {
529
- p(g(e), { recursive: !0 }), h(e, n === "json" ? i(t) : c(t), "utf-8");
530
- }
531
- async function ne(t, n) {
532
- if (_(t) === ".vue") try {
533
- let { extractFromVue: e } = await import("./vue-extractor-iUl6SUkv.js").then((e) => e.n);
534
- return e(n, t);
759
+ function J(e) {
760
+ return w("md5").update(e).digest("hex").slice(0, 8);
761
+ }
762
+ function Y(e, t) {
763
+ if (!h(e)) return {};
764
+ let n = _(e, "utf-8");
765
+ return t === "json" ? l(n) : s(n);
766
+ }
767
+ function X(e, t, n) {
768
+ g(y(e), { recursive: !0 }), v(e, n === "json" ? d(t) : a(t), "utf-8");
769
+ }
770
+ async function xe(e, t, n) {
771
+ if (b(e) === ".vue") try {
772
+ let { extractFromVue: r } = await import("./vue-extractor.js");
773
+ return r(t, e, n);
535
774
  } catch {
536
- return S.warn(`Skipping ${t}: install @vue/compiler-sfc to extract from .vue files`), [];
775
+ return D.warn(`Skipping ${e}: install @vue/compiler-sfc to extract from .vue files`), [];
537
776
  }
538
- return e(n, t);
777
+ return i(t, e, n);
539
778
  }
540
- var re = b({
779
+ var Se = T({
541
780
  meta: {
542
781
  name: "extract",
543
782
  description: "Extract messages from source files"
@@ -556,26 +795,73 @@ var re = b({
556
795
  type: "boolean",
557
796
  description: "Strip fuzzy flags from all entries",
558
797
  default: !1
798
+ },
799
+ "no-cache": {
800
+ type: "boolean",
801
+ description: "Disable incremental extraction cache",
802
+ default: !1
559
803
  }
560
804
  },
561
805
  async run({ args: e }) {
562
- let t = await l(e.config);
563
- S.info(`Extracting messages from ${t.include.join(", ")}`);
564
- let n = await C(t.include), r = [];
565
- for (let e of n) {
566
- let t = await ne(e, m(e, "utf-8"));
567
- r.push(...t);
806
+ let t = await u(e.config), n = p(t.locales);
807
+ D.info(`Extracting messages from ${t.include.join(", ")}`);
808
+ let r = await C(t.include, { ignore: t.exclude ?? [] }), i = [], a = e["no-cache"] ?? !1 ? null : new f(t.catalogDir, J(process.cwd())), s = 0;
809
+ for (let e of r) {
810
+ if (a) {
811
+ let t = a.get(e);
812
+ if (t) {
813
+ i.push(...t), s++;
814
+ continue;
815
+ }
816
+ }
817
+ let n = await xe(e, _(e, "utf-8"), t.idGenerator);
818
+ i.push(...n), a && a.set(e, n);
568
819
  }
569
- S.info(`Found ${r.length} messages in ${n.length} files`);
570
- let i = t.format === "json" ? ".json" : ".po", a = e.clean ?? !1, o = e["no-fuzzy"] ?? !1;
571
- for (let e of t.locales) {
572
- let n = y(t.catalogDir, `${e}${i}`), { catalog: s, result: c } = u(Q(n, t.format), r, { stripFuzzy: o });
573
- $(n, a ? Object.fromEntries(Object.entries(s).filter(([, e]) => !e.obsolete)) : s, t.format);
574
- let l = a ? `${c.obsolete} removed` : `${c.obsolete} obsolete`;
575
- S.success(`${e}: ${c.added} added, ${c.unchanged} unchanged, ${l}`);
820
+ a && (a.prune(new Set(r)), a.save()), s > 0 ? D.info(`Found ${i.length} messages in ${r.length} files (${s} cached)`) : D.info(`Found ${i.length} messages in ${r.length} files`);
821
+ let c = t.format === "json" ? ".json" : ".po", l = e.clean ?? !1, d = e["no-fuzzy"] ?? !1;
822
+ for (let e of n) {
823
+ let n = S(t.catalogDir, `${e}${c}`), { catalog: r, result: a } = o(Y(n, t.format), i, { stripFuzzy: d });
824
+ X(n, l ? Object.fromEntries(Object.entries(r).filter(([, e]) => !e.obsolete)) : r, t.format);
825
+ let s = l ? `${a.obsolete} removed` : `${a.obsolete} obsolete`;
826
+ D.success(`${e}: ${a.added} added, ${a.unchanged} unchanged, ${s}`);
576
827
  }
828
+ for (let e of t.plugins ?? []) await e.onAfterExtract?.({
829
+ messages: new Map(i.map((e) => [e.id, e])),
830
+ sourceLocale: t.sourceLocale,
831
+ targetLocales: n.filter((e) => e !== t.sourceLocale),
832
+ config: t
833
+ });
577
834
  }
578
- }), ie = b({
835
+ });
836
+ function Ce(e) {
837
+ let t = {};
838
+ for (let [n, r] of Object.entries(e)) r.translation && r.translation.length > 0 ? t[n] = r.translation : r.message && (t[n] = r.message);
839
+ return t;
840
+ }
841
+ async function Z(e, t, n) {
842
+ let r = Ce(e);
843
+ for (let e of n) e.transformMessages && (r = await e.transformMessages(r, t));
844
+ let i = {};
845
+ for (let [t, n] of Object.entries(e)) {
846
+ let e = r[t];
847
+ i[t] = e === void 0 ? { ...n } : {
848
+ ...n,
849
+ translation: e
850
+ };
851
+ }
852
+ return i;
853
+ }
854
+ function Q(e, t, n, r) {
855
+ let i = {};
856
+ for (let [e, n] of Object.entries(t)) n.translation && n.translation.length > 0 ? i[e] = n.translation : n.message && (i[e] = n.message);
857
+ return {
858
+ locale: e,
859
+ messages: i,
860
+ outDir: n,
861
+ config: r
862
+ };
863
+ }
864
+ var $ = T({
579
865
  meta: {
580
866
  name: "compile",
581
867
  description: "Compile message catalogs to JS modules"
@@ -589,29 +875,87 @@ var re = b({
589
875
  type: "boolean",
590
876
  description: "Exclude fuzzy entries from compilation",
591
877
  default: !1
878
+ },
879
+ "no-cache": {
880
+ type: "boolean",
881
+ description: "Disable compilation cache",
882
+ default: !1
883
+ },
884
+ parallel: {
885
+ type: "boolean",
886
+ description: "Enable parallel compilation using worker threads",
887
+ default: !1
888
+ },
889
+ concurrency: {
890
+ type: "string",
891
+ description: "Max number of worker threads (default: auto)"
592
892
  }
593
893
  },
594
- async run({ args: e }) {
595
- let n = await l(e.config), i = n.format === "json" ? ".json" : ".po";
596
- p(n.compileOutDir, { recursive: !0 });
597
- let o = {};
598
- for (let e of n.locales) o[e] = Q(y(n.catalogDir, `${e}${i}`), n.format);
599
- let c = a(o);
600
- S.info(`Compiling ${c.length} messages across ${n.locales.length} locales`);
601
- let u = e["skip-fuzzy"] ?? !1;
602
- for (let e of n.locales) {
603
- let { code: t, stats: r } = s(o[e], e, c, n.sourceLocale, { skipFuzzy: u }), i = y(n.compileOutDir, `${e}.js`);
604
- if (h(i, t, "utf-8"), r.missing.length > 0) {
605
- S.warn(`${e}: ${r.compiled} compiled, ${r.missing.length} missing translations`);
606
- for (let e of r.missing) S.warn(` ⤷ ${e}`);
607
- } else S.success(`Compiled ${e}: ${r.compiled} messages → ${i}`);
894
+ async run({ args: i }) {
895
+ let a = await u(i.config), o = p(a.locales), d = a.format === "json" ? ".json" : ".po";
896
+ g(a.compileOutDir, { recursive: !0 });
897
+ let f = {}, m = {};
898
+ for (let e of o) {
899
+ let t = S(a.catalogDir, `${e}${d}`);
900
+ if (h(t)) {
901
+ let n = _(t, "utf-8");
902
+ m[e] = n, f[e] = a.format === "json" ? l(n) : s(n);
903
+ } else m[e] = "", f[e] = {};
608
904
  }
609
- let d = r(n.locales, n.compileOutDir), f = y(n.compileOutDir, "index.js");
610
- h(f, d, "utf-8"), S.success(`Generated index ${f}`);
611
- let m = t(c, o, n.sourceLocale), g = y(n.compileOutDir, "messages.d.ts");
612
- h(g, m, "utf-8"), S.success(`Generated types ${g}`);
905
+ let y = r(f);
906
+ D.info(`Compiling ${y.length} messages across ${o.length} locales`);
907
+ let b = i["skip-fuzzy"] ?? !1, x = i["no-cache"] ?? !1 ? null : new ce(a.catalogDir, J(process.cwd())), C = i.parallel ?? !1, w = i.concurrency ? parseInt(i.concurrency, 10) : void 0;
908
+ if (w !== void 0 && (isNaN(w) || w < 1)) {
909
+ D.error("Invalid --concurrency. Must be a positive integer."), process.exitCode = 1;
910
+ return;
911
+ }
912
+ let T = 0, E = !1, O = [];
913
+ for (let e of o) {
914
+ if (x && x.isUpToDate(e, m[e]) && h(S(a.compileOutDir, `${e}.js`))) {
915
+ T++;
916
+ continue;
917
+ }
918
+ O.push(e);
919
+ }
920
+ if (O.length > 0 && (E = !0), C && O.length > 1) {
921
+ let e = a.plugins ?? [], t = {};
922
+ for (let n of O) {
923
+ for (let t of e) await t.onBeforeCompile?.(Q(n, f[n], a.compileOutDir, a));
924
+ t[n] = e.length > 0 ? await Z(f[n], n, e) : f[n];
925
+ }
926
+ let n = await c(O.map((e) => ({
927
+ locale: e,
928
+ catalog: t[e],
929
+ allIds: y,
930
+ sourceLocale: a.sourceLocale,
931
+ options: { skipFuzzy: b }
932
+ })), w);
933
+ for (let e of n) {
934
+ let t = S(a.compileOutDir, `${e.locale}.js`);
935
+ if (v(t, e.code, "utf-8"), x && x.set(e.locale, m[e.locale]), e.stats.missing.length > 0) {
936
+ D.warn(`${e.locale}: ${e.stats.compiled} compiled, ${e.stats.missing.length} missing translations`);
937
+ for (let t of e.stats.missing) D.warn(` ⤷ ${t}`);
938
+ } else D.success(`Compiled ${e.locale}: ${e.stats.compiled} messages → ${t}`);
939
+ }
940
+ for (let n of O) for (let r of e) await r.onAfterCompile?.(Q(n, t[n], a.compileOutDir, a));
941
+ } else {
942
+ let e = a.plugins ?? [];
943
+ for (let n of O) {
944
+ let r = S(a.compileOutDir, `${n}.js`);
945
+ for (let t of e) await t.onBeforeCompile?.(Q(n, f[n], a.compileOutDir, a));
946
+ let i = e.length > 0 ? await Z(f[n], n, e) : f[n], { code: o, stats: s } = t(i, n, y, a.sourceLocale, { skipFuzzy: b });
947
+ if (v(r, o, "utf-8"), x && x.set(n, m[n]), s.missing.length > 0) {
948
+ D.warn(`${n}: ${s.compiled} compiled, ${s.missing.length} missing translations`);
949
+ for (let e of s.missing) D.warn(` ⤷ ${e}`);
950
+ } else D.success(`Compiled ${n}: ${s.compiled} messages → ${r}`);
951
+ for (let t of e) await t.onAfterCompile?.(Q(n, i, a.compileOutDir, a));
952
+ }
953
+ }
954
+ T > 0 && D.info(`${T} locale(s) unchanged — skipped`), x && x.save();
955
+ let k = S(a.compileOutDir, "index.js"), A = S(a.compileOutDir, "messages.d.ts");
956
+ (E || !h(k)) && (v(k, n(o, a.compileOutDir), "utf-8"), D.success(`Generated index → ${k}`)), (E || !h(A)) && (v(A, e(y, f, a.sourceLocale), "utf-8"), D.success(`Generated types → ${A}`));
613
957
  }
614
- }), ae = b({
958
+ }), we = T({
615
959
  meta: {
616
960
  name: "stats",
617
961
  description: "Show translation progress"
@@ -621,21 +965,113 @@ var re = b({
621
965
  description: "Path to config file"
622
966
  } },
623
967
  async run({ args: e }) {
624
- let t = await l(e.config), n = t.format === "json" ? ".json" : ".po", r = [];
625
- for (let e of t.locales) {
626
- let i = Q(y(t.catalogDir, `${e}${n}`), t.format), a = Object.values(i).filter((e) => !e.obsolete), o = a.length, s = a.filter((e) => e.translation && e.translation.length > 0).length, c = o > 0 ? (s / o * 100).toFixed(1) + "%" : "—";
627
- r.push({
968
+ let t = await u(e.config), n = p(t.locales), r = t.format === "json" ? ".json" : ".po", i = [];
969
+ for (let e of n) {
970
+ let n = Y(S(t.catalogDir, `${e}${r}`), t.format), a = Object.values(n).filter((e) => !e.obsolete), o = a.length, s = a.filter((e) => e.translation && e.translation.length > 0).length, c = o > 0 ? (s / o * 100).toFixed(1) + "%" : "—";
971
+ i.push({
628
972
  locale: e,
629
973
  total: o,
630
974
  translated: s,
631
975
  pct: c
632
976
  });
633
977
  }
634
- S.log(""), S.log(" Locale │ Total │ Translated │ Progress"), S.log(" ────────┼───────┼────────────┼─────────────────────────────");
635
- for (let e of r) S.log(te(e.locale, e.total, e.translated));
636
- S.log("");
978
+ D.log(""), D.log(" Locale │ Total │ Translated │ Progress"), D.log(" ────────┼───────┼────────────┼─────────────────────────────");
979
+ for (let e of i) D.log(ne(e.locale, e.total, e.translated));
980
+ D.log("");
637
981
  }
638
- }), oe = b({
982
+ }), Te = T({
983
+ meta: {
984
+ name: "lint",
985
+ description: "Check translation quality (missing, inconsistent placeholders, fuzzy)"
986
+ },
987
+ args: {
988
+ config: {
989
+ type: "string",
990
+ description: "Path to config file"
991
+ },
992
+ strict: {
993
+ type: "boolean",
994
+ description: "Treat warnings as errors",
995
+ default: !1
996
+ },
997
+ locale: {
998
+ type: "string",
999
+ description: "Lint a specific locale only"
1000
+ }
1001
+ },
1002
+ async run({ args: e }) {
1003
+ let t = await u(e.config), n = p(t.locales), r = t.format === "json" ? ".json" : ".po", i = {};
1004
+ for (let e of n) i[e] = Y(S(t.catalogDir, `${e}${r}`), t.format);
1005
+ let a = e.locale ? [e.locale] : void 0;
1006
+ D.info(`Linting ${a ? a.join(", ") : "all locales"} (source: ${t.sourceLocale})`);
1007
+ let o = {
1008
+ sourceLocale: t.sourceLocale,
1009
+ strict: e.strict ?? !1
1010
+ };
1011
+ a && (o.locales = a);
1012
+ let s = M(i, o);
1013
+ D.log(""), D.log(P(s)), D.log("");
1014
+ let c = s.filter((e) => e.severity === "error"), l = s.filter((e) => e.severity === "warning");
1015
+ (c.length > 0 || e.strict && l.length > 0) && (process.exitCode = 1);
1016
+ }
1017
+ }), Ee = T({
1018
+ meta: {
1019
+ name: "check",
1020
+ description: "Check translation coverage for CI"
1021
+ },
1022
+ args: {
1023
+ config: {
1024
+ type: "string",
1025
+ description: "Path to config file"
1026
+ },
1027
+ ci: {
1028
+ type: "boolean",
1029
+ description: "Alias for --format github",
1030
+ default: !1
1031
+ },
1032
+ "min-coverage": {
1033
+ type: "string",
1034
+ description: "Minimum coverage percentage (0-100)",
1035
+ default: "100"
1036
+ },
1037
+ format: {
1038
+ type: "string",
1039
+ description: "Output format: text | json | github"
1040
+ },
1041
+ locale: {
1042
+ type: "string",
1043
+ description: "Check a specific locale only"
1044
+ }
1045
+ },
1046
+ async run({ args: e }) {
1047
+ let t = await u(e.config), n = p(t.locales), r = t.format === "json" ? ".json" : ".po", i = {};
1048
+ for (let e of n) i[e] = Y(S(t.catalogDir, `${e}${r}`), t.format);
1049
+ let a = parseFloat(e["min-coverage"] ?? "100");
1050
+ if (isNaN(a) || a < 0 || a > 100) {
1051
+ D.error("Invalid --min-coverage. Must be a number between 0 and 100."), process.exitCode = 1;
1052
+ return;
1053
+ }
1054
+ let o = e.format ?? (e.ci ? "github" : "text"), s = {
1055
+ sourceLocale: t.sourceLocale,
1056
+ minCoverage: a,
1057
+ format: o
1058
+ };
1059
+ e.locale && (s.locale = e.locale);
1060
+ let c = ie(i, s);
1061
+ switch (o) {
1062
+ case "json":
1063
+ D.log(se(c));
1064
+ break;
1065
+ case "github":
1066
+ D.log(oe(c, t.catalogDir, t.format));
1067
+ break;
1068
+ default:
1069
+ D.log(""), D.log(ae(c)), D.log("");
1070
+ break;
1071
+ }
1072
+ c.passed || (process.exitCode = 1);
1073
+ }
1074
+ }), De = T({
639
1075
  meta: {
640
1076
  name: "translate",
641
1077
  description: "Translate messages using AI (Claude Code or Codex CLI)"
@@ -658,39 +1094,58 @@ var re = b({
658
1094
  type: "string",
659
1095
  description: "Messages per batch",
660
1096
  default: "50"
1097
+ },
1098
+ "dry-run": {
1099
+ type: "boolean",
1100
+ description: "Preview translation results without writing files",
1101
+ default: !1
1102
+ },
1103
+ context: {
1104
+ type: "string",
1105
+ description: "Project context description to improve translation quality"
661
1106
  }
662
1107
  },
663
1108
  async run({ args: e }) {
664
- let t = await l(e.config), n = e.provider;
665
- if (n !== "claude" && n !== "codex") {
666
- S.error(`Invalid provider "${n}". Use "claude" or "codex".`);
1109
+ let t = await u(e.config), n = p(t.locales), r = e.provider;
1110
+ if (r !== "claude" && r !== "codex") {
1111
+ D.error(`Invalid provider "${r}". Use "claude" or "codex".`);
667
1112
  return;
668
1113
  }
669
- let r = parseInt(e["batch-size"] ?? "50", 10);
670
- if (isNaN(r) || r < 1) {
671
- S.error("Invalid batch-size. Must be a positive integer.");
1114
+ let i = parseInt(e["batch-size"] ?? "50", 10);
1115
+ if (isNaN(i) || i < 1) {
1116
+ D.error("Invalid batch-size. Must be a positive integer.");
672
1117
  return;
673
1118
  }
674
- let i = e.locale ? [e.locale] : t.locales.filter((e) => e !== t.sourceLocale);
675
- if (i.length === 0) {
676
- S.warn("No target locales to translate.");
1119
+ let a = e.locale ? [e.locale] : n.filter((e) => e !== t.sourceLocale);
1120
+ if (a.length === 0) {
1121
+ D.warn("No target locales to translate.");
677
1122
  return;
678
1123
  }
679
- S.info(`Translating with ${n} (batch size: ${r})`);
680
- let a = t.format === "json" ? ".json" : ".po";
681
- for (let e of i) {
682
- S.info(`\n[${e}]`);
683
- let i = y(t.catalogDir, `${e}${a}`), o = Q(i, t.format), { catalog: s, translated: c } = await F({
684
- provider: n,
1124
+ D.info(`Translating with ${r} (batch size: ${i})`);
1125
+ let o = t.format === "json" ? ".json" : ".po";
1126
+ for (let n of a) {
1127
+ D.info(`\n[${n}]`);
1128
+ let a = S(t.catalogDir, `${n}${o}`), s = Y(a, t.format);
1129
+ if (e["dry-run"]) {
1130
+ let e = Object.entries(s).filter(([, e]) => !e.obsolete && (!e.translation || e.translation.length === 0));
1131
+ if (e.length > 0) {
1132
+ for (let [t, n] of e) D.log(` ${t}: ${n.message ?? t}`);
1133
+ D.success(` ${n}: ${e.length} messages would be translated (dry-run)`);
1134
+ } else D.success(` ${n}: already fully translated`);
1135
+ continue;
1136
+ }
1137
+ let { catalog: c, translated: l } = await U({
1138
+ provider: r,
685
1139
  sourceLocale: t.sourceLocale,
686
- targetLocale: e,
687
- catalog: o,
688
- batchSize: r
1140
+ targetLocale: n,
1141
+ catalog: s,
1142
+ batchSize: i,
1143
+ ...e.context ? { context: e.context } : {}
689
1144
  });
690
- c > 0 ? ($(i, s, t.format), S.success(` ${e}: ${c} messages translated`)) : S.success(` ${e}: already fully translated`);
1145
+ l > 0 ? (X(a, c, t.format), D.success(` ${n}: ${l} messages translated`)) : D.success(` ${n}: already fully translated`);
691
1146
  }
692
1147
  }
693
- }), se = b({
1148
+ }), Oe = T({
694
1149
  meta: {
695
1150
  name: "migrate",
696
1151
  description: "Migrate from another i18n library using AI"
@@ -715,38 +1170,40 @@ var re = b({
715
1170
  async run({ args: e }) {
716
1171
  let t = e.provider;
717
1172
  if (t !== "claude" && t !== "codex") {
718
- S.error(`Invalid provider "${t}". Use "claude" or "codex".`);
1173
+ D.error(`Invalid provider "${t}". Use "claude" or "codex".`);
719
1174
  return;
720
1175
  }
721
- await G({
1176
+ await he({
722
1177
  from: e.from,
723
1178
  provider: t,
724
1179
  write: e.write ?? !1
725
1180
  });
726
1181
  }
727
1182
  });
728
- x(b({
1183
+ E(T({
729
1184
  meta: {
730
1185
  name: "fluenti",
731
1186
  version: "0.0.1",
732
1187
  description: "Compile-time i18n for modern frameworks"
733
1188
  },
734
1189
  subCommands: {
735
- init: b({
1190
+ init: T({
736
1191
  meta: {
737
1192
  name: "init",
738
1193
  description: "Initialize Fluenti in your project"
739
1194
  },
740
1195
  args: {},
741
1196
  async run() {
742
- await Z({ cwd: process.cwd() });
1197
+ await be({ cwd: process.cwd() });
743
1198
  }
744
1199
  }),
745
- extract: re,
746
- compile: ie,
747
- stats: ae,
748
- translate: oe,
749
- migrate: se
1200
+ extract: Se,
1201
+ compile: $,
1202
+ stats: we,
1203
+ lint: Te,
1204
+ check: Ee,
1205
+ translate: De,
1206
+ migrate: Oe
750
1207
  }
751
1208
  }));
752
1209
  //#endregion