@fluenti/cli 0.2.1 → 0.3.1

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 (75) 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 +638 -199
  10. package/dist/cli.js.map +1 -1
  11. package/dist/compile-CBSy1rNl.cjs +8 -0
  12. package/dist/compile-CBSy1rNl.cjs.map +1 -0
  13. package/dist/compile-cache.d.ts +26 -0
  14. package/dist/compile-cache.d.ts.map +1 -0
  15. package/dist/compile-jumIhf8m.js +192 -0
  16. package/dist/compile-jumIhf8m.js.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-CmnwPMdA.js +304 -0
  31. package/dist/extract-cache-CmnwPMdA.js.map +1 -0
  32. package/dist/extract-cache-IDp-S-ux.cjs +10 -0
  33. package/dist/extract-cache-IDp-S-ux.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 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +117 -25
  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/migrate.d.ts.map +1 -1
  49. package/dist/parallel-compile.d.ts +26 -0
  50. package/dist/parallel-compile.d.ts.map +1 -0
  51. package/dist/translate.d.ts.map +1 -1
  52. package/dist/{tsx-extractor-CcFjsYI-.js → tsx-extractor-B9fnGNTG.js} +61 -53
  53. package/dist/tsx-extractor-B9fnGNTG.js.map +1 -0
  54. package/dist/tsx-extractor-j_z4fneM.cjs +2 -0
  55. package/dist/tsx-extractor-j_z4fneM.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 +2 -2
  61. package/dist/vue-extractor.cjs.map +1 -1
  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 +42 -42
  65. package/dist/vue-extractor.js.map +1 -1
  66. package/llms-full.txt +297 -0
  67. package/llms.txt +86 -0
  68. package/package.json +4 -3
  69. package/dist/config-loader-BgAoTfxH.js +0 -387
  70. package/dist/config-loader-BgAoTfxH.js.map +0 -1
  71. package/dist/config-loader-D3RGkK_r.cjs +0 -16
  72. package/dist/config-loader-D3RGkK_r.cjs.map +0 -1
  73. package/dist/tsx-extractor-CcFjsYI-.js.map +0 -1
  74. package/dist/tsx-extractor-D__s_cP8.cjs +0 -2
  75. package/dist/tsx-extractor-D__s_cP8.cjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { t as e } from "./tsx-extractor-CcFjsYI-.js";
2
- import { createMessageId as t } from "@fluenti/core/internal";
1
+ import { t as e } from "./tsx-extractor-B9fnGNTG.js";
2
+ import { createMessageId as t } from "@fluenti/core/transform";
3
3
  import { parse as n } from "@vue/compiler-sfc";
4
4
  //#region src/vue-extractor.ts
5
5
  var r = 1, i = 2, a = 7, o = 6;
@@ -33,13 +33,13 @@ function l(e) {
33
33
  }
34
34
  return r.length === 0 ? "" : `{${t}, plural, ${i ? `offset:${i} ` : ""}${r.join(" ")}}`;
35
35
  }
36
- function u(e, n, i) {
36
+ function u(e, n, i, p) {
37
37
  if (e.type === r) {
38
38
  let r = e.props?.find((e) => e.type === a && f(e) === "t");
39
39
  if (r) {
40
- let a = new Set(["plural"]), o = (r.modifiers ?? []).map((e) => typeof e == "string" ? e : e.content), l = o.includes("plural"), u = o.filter((e) => !a.has(e)), d = r.arg?.content, f = d ? [d, ...u].join(".") : void 0, p = s(e.children ?? []);
40
+ let a = new Set(["plural"]), o = (r.modifiers ?? []).map((e) => typeof e == "string" ? e : e.content), l = o.includes("plural"), u = o.filter((e) => !a.has(e)), d = r.arg?.content, f = d ? [d, ...u].join(".") : void 0, m = s(e.children ?? []);
41
41
  if (l) {
42
- let e = c(p, r.exp?.content ?? "count"), a = f ?? t(e);
42
+ let e = c(m, r.exp?.content ?? "count"), a = f ?? (p ?? t)(e);
43
43
  i.push({
44
44
  id: a,
45
45
  message: e,
@@ -49,11 +49,11 @@ function u(e, n, i) {
49
49
  column: r.loc.start.column
50
50
  }
51
51
  });
52
- } else if (p) {
53
- let e = f ?? t(p);
52
+ } else if (m) {
53
+ let e = f ?? (p ?? t)(m);
54
54
  i.push({
55
55
  id: e,
56
- message: p,
56
+ message: m,
57
57
  origin: {
58
58
  file: n,
59
59
  line: r.loc.start.line,
@@ -65,9 +65,9 @@ function u(e, n, i) {
65
65
  if (e.tag === "Trans") {
66
66
  let r = e.props?.find((e) => e.type === o && f(e) === "message"), a = e.props?.find((e) => e.type === o && f(e) === "id"), s = e.props?.find((e) => e.type === o && f(e) === "context"), c = e.props?.find((e) => e.type === o && f(e) === "comment"), l = s?.value?.content, u = c?.value?.content;
67
67
  if (r?.value) {
68
- let o = r.value.content, s = a?.value?.content ?? t(o, l);
68
+ let o = r.value.content, s = p ?? t, c = a?.value?.content ?? s(o, l);
69
69
  i.push({
70
- id: s,
70
+ id: c,
71
71
  message: o,
72
72
  ...l === void 0 ? {} : { context: l },
73
73
  ...u === void 0 ? {} : { comment: u },
@@ -80,9 +80,9 @@ function u(e, n, i) {
80
80
  } else if (e.children && e.children.length > 0) {
81
81
  let r = d(e.children);
82
82
  if (r.message) {
83
- let o = a?.value?.content ?? t(r.message, l);
83
+ let o = p ?? t, s = a?.value?.content ?? o(r.message, l);
84
84
  i.push({
85
- id: o,
85
+ id: s,
86
86
  message: r.message,
87
87
  ...l === void 0 ? {} : { context: l },
88
88
  ...u === void 0 ? {} : { comment: u },
@@ -98,16 +98,16 @@ function u(e, n, i) {
98
98
  if (e.tag === "Plural") {
99
99
  let r = {}, s, c;
100
100
  for (let t of e.props ?? []) t.type === o && t.value && (r[f(t)] = t.value.content), t.type === a && f(t) === "bind" && t.arg?.content === "value" && t.exp && (s = t.exp.content), t.type === a && f(t) === "bind" && t.arg?.content === "offset" && t.exp && (c = t.exp.content);
101
- let u = s ?? r.count ?? "count", d = c ?? r.offset, p = l({
101
+ let u = s ?? r.count ?? "count", d = c ?? r.offset, m = l({
102
102
  ...r,
103
103
  count: u,
104
104
  ...d === void 0 ? {} : { offset: d }
105
105
  });
106
- if (p) {
107
- let a = r.id ?? t(p);
106
+ if (m) {
107
+ let a = r.id ?? (p ?? t)(m);
108
108
  i.push({
109
109
  id: a,
110
- message: p,
110
+ message: m,
111
111
  origin: {
112
112
  file: n,
113
113
  line: e.loc.start.line,
@@ -117,7 +117,7 @@ function u(e, n, i) {
117
117
  }
118
118
  }
119
119
  }
120
- if (e.children) for (let t of e.children) u(t, n, i);
120
+ if (e.children) for (let t of e.children) u(t, n, i, p);
121
121
  }
122
122
  function d(e) {
123
123
  let t = 0, n = !1;
@@ -137,37 +137,37 @@ function d(e) {
137
137
  function f(e) {
138
138
  return typeof e.name == "string" ? e.name : e.name.content;
139
139
  }
140
- function p(t, n) {
141
- let r = [], i = /\{\{([\s\S]*?)\}\}/g, a;
142
- for (; (a = i.exec(t)) !== null;) {
143
- let i = a[1]?.trim();
144
- if (!i) continue;
145
- let o = e(i, n);
146
- if (o.length === 0) continue;
147
- let s = t.slice(0, a.index).split("\n").length - 1;
148
- for (let e of o) r.push({
140
+ function p(t, n, r) {
141
+ let i = [], a = /\{\{([\s\S]*?)\}\}/g, o;
142
+ for (; (o = a.exec(t)) !== null;) {
143
+ let a = o[1]?.trim();
144
+ if (!a) continue;
145
+ let s = e(a, n, r);
146
+ if (s.length === 0) continue;
147
+ let c = t.slice(0, o.index).split("\n").length - 1;
148
+ for (let e of s) i.push({
149
149
  ...e,
150
150
  origin: {
151
151
  ...e.origin,
152
- line: e.origin.line + s
152
+ line: e.origin.line + c
153
153
  }
154
154
  });
155
155
  }
156
- return r;
156
+ return i;
157
157
  }
158
- function m(t, r) {
159
- let i = [], { descriptor: a } = n(t, { filename: r });
160
- if (a.template?.ast && u(a.template.ast, r, i), a.template?.content) {
161
- let t = e(a.template.content, r), n = a.template.loc.start.line - 1, o = new Set(i.map((e) => e.id));
162
- for (let e of t) o.has(e.id) || i.push({
158
+ function m(t, r, i) {
159
+ let a = [], { descriptor: o } = n(t, { filename: r });
160
+ if (o.template?.ast && u(o.template.ast, r, a, i), o.template?.content) {
161
+ let t = e(o.template.content, r, i), n = o.template.loc.start.line - 1, s = new Set(a.map((e) => e.id));
162
+ for (let e of t) s.has(e.id) || a.push({
163
163
  ...e,
164
164
  origin: {
165
165
  ...e.origin,
166
166
  line: e.origin.line + n
167
167
  }
168
168
  });
169
- let s = p(a.template.content, r);
170
- for (let e of s) o.has(e.id) || i.push({
169
+ let c = p(o.template.content, r, i);
170
+ for (let e of c) s.has(e.id) || a.push({
171
171
  ...e,
172
172
  origin: {
173
173
  ...e.origin,
@@ -175,9 +175,9 @@ function m(t, r) {
175
175
  }
176
176
  });
177
177
  }
178
- if (a.scriptSetup?.content) {
179
- let t = e(a.scriptSetup.content, r), n = a.scriptSetup.loc.start.line - 1;
180
- for (let e of t) i.push({
178
+ if (o.scriptSetup?.content) {
179
+ let t = e(o.scriptSetup.content, r, i), n = o.scriptSetup.loc.start.line - 1;
180
+ for (let e of t) a.push({
181
181
  ...e,
182
182
  origin: {
183
183
  ...e.origin,
@@ -185,9 +185,9 @@ function m(t, r) {
185
185
  }
186
186
  });
187
187
  }
188
- if (a.script?.content) {
189
- let t = e(a.script.content, r), n = a.script.loc.start.line - 1;
190
- for (let e of t) i.push({
188
+ if (o.script?.content) {
189
+ let t = e(o.script.content, r, i), n = o.script.loc.start.line - 1;
190
+ for (let e of t) a.push({
191
191
  ...e,
192
192
  origin: {
193
193
  ...e.origin,
@@ -195,7 +195,7 @@ function m(t, r) {
195
195
  }
196
196
  });
197
197
  }
198
- return i;
198
+ return a;
199
199
  }
200
200
  //#endregion
201
201
  export { m as extractFromVue };
@@ -1 +1 @@
1
- {"version":3,"file":"vue-extractor.js","names":[],"sources":["../src/vue-extractor.ts"],"sourcesContent":["import type { ExtractedMessage } from '@fluenti/core'\nimport { parse as parseSFC } from '@vue/compiler-sfc'\nimport { createMessageId } from '@fluenti/core/internal'\nimport { extractFromTsx } from './tsx-extractor'\n\n// Vue template AST node types\nconst ELEMENT_NODE = 1\nconst TEXT_NODE = 2\nconst DIRECTIVE_PROP = 7\nconst ATTRIBUTE_PROP = 6\n\ninterface LocInfo {\n line: number\n column: number\n offset: number\n}\n\ninterface SourceLoc {\n start: LocInfo\n end: LocInfo\n source: string\n}\n\ninterface TemplateNode {\n type: number\n tag?: string\n tagType?: number\n props?: TemplateProp[]\n children?: TemplateNode[]\n content?: string\n loc: SourceLoc\n}\n\ninterface TemplateProp {\n type: number\n name: string | { content: string }\n rawName?: string\n arg?: { content: string; isStatic: boolean }\n exp?: { content: string }\n modifiers?: Array<{ content: string } | string>\n value?: { content: string }\n nameLoc?: SourceLoc\n loc: SourceLoc\n}\n\nfunction getTextContent(children: TemplateNode[]): string {\n return children\n .filter((c) => c.type === TEXT_NODE)\n .map((c) => (c.content ?? '').trim())\n .join('')\n}\n\nfunction buildPluralICUFromPipe(text: string, countVar: string): string {\n const forms = text.split('|').map((s) => s.trim())\n const categories = ['one', 'other', 'zero', 'few', 'many']\n const options: string[] = []\n\n if (forms.length === 2) {\n options.push(`one {${forms[0]}}`)\n options.push(`other {${forms[1]}}`)\n } else {\n for (let i = 0; i < forms.length && i < categories.length; i++) {\n options.push(`${categories[i]} {${forms[i]}}`)\n }\n }\n\n return `{${countVar}, plural, ${options.join(' ')}}`\n}\n\nfunction buildPluralICUFromProps(props: Record<string, string>): string {\n const countVar = props['count'] ?? 'count'\n const categories = ['zero', 'one', 'two', 'few', 'many', 'other']\n const options: string[] = []\n const offset = props['offset']\n\n for (const cat of categories) {\n if (props[cat] !== undefined) {\n const key = cat === 'zero' ? '=0' : cat\n options.push(`${key} {${props[cat]}}`)\n }\n }\n\n if (options.length === 0) return ''\n const offsetPrefix = offset ? `offset:${offset} ` : ''\n return `{${countVar}, plural, ${offsetPrefix}${options.join(' ')}}`\n}\n\nfunction walkTemplate(\n node: TemplateNode,\n filename: string,\n messages: ExtractedMessage[],\n): void {\n if (node.type === ELEMENT_NODE) {\n const vtDirective = node.props?.find(\n (p) => p.type === DIRECTIVE_PROP && getPropName(p) === 't',\n )\n\n if (vtDirective) {\n const RESERVED_MODIFIERS = new Set(['plural'])\n const modifiers = (vtDirective.modifiers ?? []).map(\n (m: string | { content: string }) => (typeof m === 'string' ? m : m.content),\n )\n const isPlural = modifiers.includes('plural')\n // Reconstruct dotted ID: v-t:checkout.title → arg=\"checkout\", modifier=\"title\" → \"checkout.title\"\n // Non-reserved modifiers are treated as ID path segments\n const idSegments = modifiers.filter((m: string) => !RESERVED_MODIFIERS.has(m))\n const argContent = vtDirective.arg?.content\n const explicitId = argContent\n ? [argContent, ...idSegments].join('.')\n : undefined\n const textContent = getTextContent(node.children ?? [])\n\n if (isPlural) {\n const countVar = vtDirective.exp?.content ?? 'count'\n const message = buildPluralICUFromPipe(textContent, countVar)\n const id = explicitId ?? createMessageId(message)\n messages.push({\n id,\n message,\n origin: {\n file: filename,\n line: vtDirective.loc.start.line,\n column: vtDirective.loc.start.column,\n },\n })\n } else if (textContent) {\n const id = explicitId ?? createMessageId(textContent)\n messages.push({\n id,\n message: textContent,\n origin: {\n file: filename,\n line: vtDirective.loc.start.line,\n column: vtDirective.loc.start.column,\n },\n })\n }\n }\n\n if (node.tag === 'Trans') {\n const messageProp = node.props?.find(\n (p) => p.type === ATTRIBUTE_PROP && getPropName(p) === 'message',\n )\n const idProp = node.props?.find(\n (p) => p.type === ATTRIBUTE_PROP && getPropName(p) === 'id',\n )\n const contextProp = node.props?.find(\n (p) => p.type === ATTRIBUTE_PROP && getPropName(p) === 'context',\n )\n const commentProp = node.props?.find(\n (p) => p.type === ATTRIBUTE_PROP && getPropName(p) === 'comment',\n )\n const context = contextProp?.value?.content\n const comment = commentProp?.value?.content\n\n if (messageProp?.value) {\n // Old API: <Trans message=\"...\" />\n const message = messageProp.value.content\n const id = idProp?.value?.content ?? createMessageId(message, context)\n messages.push({\n id,\n message,\n ...(context !== undefined ? { context } : {}),\n ...(comment !== undefined ? { comment } : {}),\n origin: {\n file: filename,\n line: node.loc.start.line,\n column: node.loc.start.column,\n },\n })\n } else if (node.children && node.children.length > 0) {\n // New API: <Trans>content with <a>rich text</a></Trans>\n const richText = extractRichTextFromTemplateChildren(node.children)\n if (richText.message) {\n const id = idProp?.value?.content ?? createMessageId(richText.message, context)\n messages.push({\n id,\n message: richText.message,\n ...(context !== undefined ? { context } : {}),\n ...(comment !== undefined ? { comment } : {}),\n origin: {\n file: filename,\n line: node.loc.start.line,\n column: node.loc.start.column,\n },\n })\n }\n }\n }\n\n if (node.tag === 'Plural') {\n const propsMap: Record<string, string> = {}\n let valueExpr: string | undefined\n let offsetExpr: string | undefined\n for (const prop of node.props ?? []) {\n if (prop.type === ATTRIBUTE_PROP && prop.value) {\n propsMap[getPropName(prop)] = prop.value.content\n }\n // Handle :value=\"expr\" binding (directive prop)\n if (prop.type === DIRECTIVE_PROP && getPropName(prop) === 'bind' && prop.arg?.content === 'value' && prop.exp) {\n valueExpr = prop.exp.content\n }\n if (prop.type === DIRECTIVE_PROP && getPropName(prop) === 'bind' && prop.arg?.content === 'offset' && prop.exp) {\n offsetExpr = prop.exp.content\n }\n }\n\n // Use :value binding expression as count variable, fall back to 'count' static prop\n const countVar = valueExpr ?? propsMap['count'] ?? 'count'\n const offset = offsetExpr ?? propsMap['offset']\n const pluralMessage = buildPluralICUFromProps({\n ...propsMap,\n count: countVar,\n ...(offset !== undefined ? { offset } : {}),\n })\n if (pluralMessage) {\n const id = propsMap['id'] ?? createMessageId(pluralMessage)\n messages.push({\n id,\n message: pluralMessage,\n origin: {\n file: filename,\n line: node.loc.start.line,\n column: node.loc.start.column,\n },\n })\n }\n }\n }\n\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, filename, messages)\n }\n }\n}\n\nfunction extractRichTextFromTemplateChildren(\n children: TemplateNode[],\n): { message: string; hasElements: boolean } {\n let elementIndex = 0\n let hasElements = false\n\n const parts = children.map((child) => {\n if (child.type === TEXT_NODE) {\n return (child.content ?? '').trim() ? child.content ?? '' : ''\n }\n if (child.type === ELEMENT_NODE && child.tag) {\n hasElements = true\n const idx = elementIndex++\n const innerText = extractRichTextFromTemplateChildren(child.children ?? []).message\n return `<${idx}>${innerText}</${idx}>`\n }\n return ''\n })\n\n return {\n message: parts.join('').trim(),\n hasElements,\n }\n}\n\nfunction getPropName(prop: TemplateProp): string {\n if (typeof prop.name === 'string') return prop.name\n return prop.name.content\n}\n\nfunction extractTemplateInterpolations(\n content: string,\n filename: string,\n): ExtractedMessage[] {\n const messages: ExtractedMessage[] = []\n const interpolationRegex = /\\{\\{([\\s\\S]*?)\\}\\}/g\n let match: RegExpExecArray | null\n\n while ((match = interpolationRegex.exec(content)) !== null) {\n const expression = match[1]?.trim()\n if (!expression) continue\n\n const extracted = extractFromTsx(expression, filename)\n if (extracted.length === 0) continue\n\n const lineOffset = content.slice(0, match.index).split('\\n').length - 1\n for (const msg of extracted) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n\n return messages\n}\n\n/** Extract messages from Vue SFC files */\nexport function extractFromVue(code: string, filename: string): ExtractedMessage[] {\n const messages: ExtractedMessage[] = []\n\n const { descriptor } = parseSFC(code, { filename })\n\n if (descriptor.template?.ast) {\n walkTemplate(descriptor.template.ast as unknown as TemplateNode, filename, messages)\n }\n\n // Also extract t() function calls from raw template source\n // (picks up t('source text') in template expressions like {{ t('...') }})\n if (descriptor.template?.content) {\n const templateMessages = extractFromTsx(descriptor.template.content, filename)\n const templateLoc = descriptor.template.loc\n const lineOffset = templateLoc.start.line - 1\n const existingIds = new Set(messages.map((m) => m.id))\n for (const msg of templateMessages) {\n if (!existingIds.has(msg.id)) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n\n const interpolationMessages = extractTemplateInterpolations(descriptor.template.content, filename)\n for (const msg of interpolationMessages) {\n if (!existingIds.has(msg.id)) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n }\n\n if (descriptor.scriptSetup?.content) {\n const scriptMessages = extractFromTsx(descriptor.scriptSetup.content, filename)\n const scriptLoc = descriptor.scriptSetup.loc\n const lineOffset = scriptLoc.start.line - 1\n for (const msg of scriptMessages) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n\n if (descriptor.script?.content) {\n const scriptMessages = extractFromTsx(descriptor.script.content, filename)\n const scriptLoc = descriptor.script.loc\n const lineOffset = scriptLoc.start.line - 1\n for (const msg of scriptMessages) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n\n return messages\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"vue-extractor.js","names":[],"sources":["../src/vue-extractor.ts"],"sourcesContent":["import type { ExtractedMessage } from '@fluenti/core/internal'\nimport { parse as parseSFC } from '@vue/compiler-sfc'\nimport { createMessageId } from '@fluenti/core/transform'\nimport { extractFromTsx } from './tsx-extractor'\n\n// Vue template AST node types\nconst ELEMENT_NODE = 1\nconst TEXT_NODE = 2\nconst DIRECTIVE_PROP = 7\nconst ATTRIBUTE_PROP = 6\n\ninterface LocInfo {\n line: number\n column: number\n offset: number\n}\n\ninterface SourceLoc {\n start: LocInfo\n end: LocInfo\n source: string\n}\n\ninterface TemplateNode {\n type: number\n tag?: string\n tagType?: number\n props?: TemplateProp[]\n children?: TemplateNode[]\n content?: string\n loc: SourceLoc\n}\n\ninterface TemplateProp {\n type: number\n name: string | { content: string }\n rawName?: string\n arg?: { content: string; isStatic: boolean }\n exp?: { content: string }\n modifiers?: Array<{ content: string } | string>\n value?: { content: string }\n nameLoc?: SourceLoc\n loc: SourceLoc\n}\n\nfunction getTextContent(children: TemplateNode[]): string {\n return children\n .filter((c) => c.type === TEXT_NODE)\n .map((c) => (c.content ?? '').trim())\n .join('')\n}\n\nfunction buildPluralICUFromPipe(text: string, countVar: string): string {\n const forms = text.split('|').map((s) => s.trim())\n const categories = ['one', 'other', 'zero', 'few', 'many']\n const options: string[] = []\n\n if (forms.length === 2) {\n options.push(`one {${forms[0]}}`)\n options.push(`other {${forms[1]}}`)\n } else {\n for (let i = 0; i < forms.length && i < categories.length; i++) {\n options.push(`${categories[i]} {${forms[i]}}`)\n }\n }\n\n return `{${countVar}, plural, ${options.join(' ')}}`\n}\n\nfunction buildPluralICUFromProps(props: Record<string, string>): string {\n const countVar = props['count'] ?? 'count'\n const categories = ['zero', 'one', 'two', 'few', 'many', 'other']\n const options: string[] = []\n const offset = props['offset']\n\n for (const cat of categories) {\n if (props[cat] !== undefined) {\n const key = cat === 'zero' ? '=0' : cat\n options.push(`${key} {${props[cat]}}`)\n }\n }\n\n if (options.length === 0) return ''\n const offsetPrefix = offset ? `offset:${offset} ` : ''\n return `{${countVar}, plural, ${offsetPrefix}${options.join(' ')}}`\n}\n\nfunction walkTemplate(\n node: TemplateNode,\n filename: string,\n messages: ExtractedMessage[],\n idGenerator?: (message: string, context?: string) => string,\n): void {\n if (node.type === ELEMENT_NODE) {\n const vtDirective = node.props?.find(\n (p) => p.type === DIRECTIVE_PROP && getPropName(p) === 't',\n )\n\n if (vtDirective) {\n const RESERVED_MODIFIERS = new Set(['plural'])\n const modifiers = (vtDirective.modifiers ?? []).map(\n (m: string | { content: string }) => (typeof m === 'string' ? m : m.content),\n )\n const isPlural = modifiers.includes('plural')\n // Reconstruct dotted ID: v-t:checkout.title → arg=\"checkout\", modifier=\"title\" → \"checkout.title\"\n // Non-reserved modifiers are treated as ID path segments\n const idSegments = modifiers.filter((m: string) => !RESERVED_MODIFIERS.has(m))\n const argContent = vtDirective.arg?.content\n const explicitId = argContent\n ? [argContent, ...idSegments].join('.')\n : undefined\n const textContent = getTextContent(node.children ?? [])\n\n if (isPlural) {\n const countVar = vtDirective.exp?.content ?? 'count'\n const message = buildPluralICUFromPipe(textContent, countVar)\n const id = explicitId ?? (idGenerator ?? createMessageId)(message)\n messages.push({\n id,\n message,\n origin: {\n file: filename,\n line: vtDirective.loc.start.line,\n column: vtDirective.loc.start.column,\n },\n })\n } else if (textContent) {\n const id = explicitId ?? (idGenerator ?? createMessageId)(textContent)\n messages.push({\n id,\n message: textContent,\n origin: {\n file: filename,\n line: vtDirective.loc.start.line,\n column: vtDirective.loc.start.column,\n },\n })\n }\n }\n\n if (node.tag === 'Trans') {\n const messageProp = node.props?.find(\n (p) => p.type === ATTRIBUTE_PROP && getPropName(p) === 'message',\n )\n const idProp = node.props?.find(\n (p) => p.type === ATTRIBUTE_PROP && getPropName(p) === 'id',\n )\n const contextProp = node.props?.find(\n (p) => p.type === ATTRIBUTE_PROP && getPropName(p) === 'context',\n )\n const commentProp = node.props?.find(\n (p) => p.type === ATTRIBUTE_PROP && getPropName(p) === 'comment',\n )\n const context = contextProp?.value?.content\n const comment = commentProp?.value?.content\n\n if (messageProp?.value) {\n // Old API: <Trans message=\"...\" />\n const message = messageProp.value.content\n const generateId = idGenerator ?? createMessageId\n const id = idProp?.value?.content ?? generateId(message, context)\n messages.push({\n id,\n message,\n ...(context !== undefined ? { context } : {}),\n ...(comment !== undefined ? { comment } : {}),\n origin: {\n file: filename,\n line: node.loc.start.line,\n column: node.loc.start.column,\n },\n })\n } else if (node.children && node.children.length > 0) {\n // New API: <Trans>content with <a>rich text</a></Trans>\n const richText = extractRichTextFromTemplateChildren(node.children)\n if (richText.message) {\n const generateId = idGenerator ?? createMessageId\n const id = idProp?.value?.content ?? generateId(richText.message, context)\n messages.push({\n id,\n message: richText.message,\n ...(context !== undefined ? { context } : {}),\n ...(comment !== undefined ? { comment } : {}),\n origin: {\n file: filename,\n line: node.loc.start.line,\n column: node.loc.start.column,\n },\n })\n }\n }\n }\n\n if (node.tag === 'Plural') {\n const propsMap: Record<string, string> = {}\n let valueExpr: string | undefined\n let offsetExpr: string | undefined\n for (const prop of node.props ?? []) {\n if (prop.type === ATTRIBUTE_PROP && prop.value) {\n propsMap[getPropName(prop)] = prop.value.content\n }\n // Handle :value=\"expr\" binding (directive prop)\n if (prop.type === DIRECTIVE_PROP && getPropName(prop) === 'bind' && prop.arg?.content === 'value' && prop.exp) {\n valueExpr = prop.exp.content\n }\n if (prop.type === DIRECTIVE_PROP && getPropName(prop) === 'bind' && prop.arg?.content === 'offset' && prop.exp) {\n offsetExpr = prop.exp.content\n }\n }\n\n // Use :value binding expression as count variable, fall back to 'count' static prop\n const countVar = valueExpr ?? propsMap['count'] ?? 'count'\n const offset = offsetExpr ?? propsMap['offset']\n const pluralMessage = buildPluralICUFromProps({\n ...propsMap,\n count: countVar,\n ...(offset !== undefined ? { offset } : {}),\n })\n if (pluralMessage) {\n const id = propsMap['id'] ?? (idGenerator ?? createMessageId)(pluralMessage)\n messages.push({\n id,\n message: pluralMessage,\n origin: {\n file: filename,\n line: node.loc.start.line,\n column: node.loc.start.column,\n },\n })\n }\n }\n }\n\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, filename, messages, idGenerator)\n }\n }\n}\n\nfunction extractRichTextFromTemplateChildren(\n children: TemplateNode[],\n): { message: string; hasElements: boolean } {\n let elementIndex = 0\n let hasElements = false\n\n const parts = children.map((child) => {\n if (child.type === TEXT_NODE) {\n return (child.content ?? '').trim() ? child.content ?? '' : ''\n }\n if (child.type === ELEMENT_NODE && child.tag) {\n hasElements = true\n const idx = elementIndex++\n const innerText = extractRichTextFromTemplateChildren(child.children ?? []).message\n return `<${idx}>${innerText}</${idx}>`\n }\n return ''\n })\n\n return {\n message: parts.join('').trim(),\n hasElements,\n }\n}\n\nfunction getPropName(prop: TemplateProp): string {\n if (typeof prop.name === 'string') return prop.name\n return prop.name.content\n}\n\nfunction extractTemplateInterpolations(\n content: string,\n filename: string,\n idGenerator?: (message: string, context?: string) => string,\n): ExtractedMessage[] {\n const messages: ExtractedMessage[] = []\n const interpolationRegex = /\\{\\{([\\s\\S]*?)\\}\\}/g\n let match: RegExpExecArray | null\n\n while ((match = interpolationRegex.exec(content)) !== null) {\n const expression = match[1]?.trim()\n if (!expression) continue\n\n const extracted = extractFromTsx(expression, filename, idGenerator)\n if (extracted.length === 0) continue\n\n const lineOffset = content.slice(0, match.index).split('\\n').length - 1\n for (const msg of extracted) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n\n return messages\n}\n\n/** Extract messages from Vue SFC files */\nexport function extractFromVue(\n code: string,\n filename: string,\n idGenerator?: (message: string, context?: string) => string,\n): ExtractedMessage[] {\n const messages: ExtractedMessage[] = []\n\n const { descriptor } = parseSFC(code, { filename })\n\n if (descriptor.template?.ast) {\n walkTemplate(descriptor.template.ast as unknown as TemplateNode, filename, messages, idGenerator)\n }\n\n // Also extract t() function calls from raw template source\n // (picks up t('source text') in template expressions like {{ t('...') }})\n if (descriptor.template?.content) {\n const templateMessages = extractFromTsx(descriptor.template.content, filename, idGenerator)\n const templateLoc = descriptor.template.loc\n const lineOffset = templateLoc.start.line - 1\n const existingIds = new Set(messages.map((m) => m.id))\n for (const msg of templateMessages) {\n if (!existingIds.has(msg.id)) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n\n const interpolationMessages = extractTemplateInterpolations(descriptor.template.content, filename, idGenerator)\n for (const msg of interpolationMessages) {\n if (!existingIds.has(msg.id)) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n }\n\n if (descriptor.scriptSetup?.content) {\n const scriptMessages = extractFromTsx(descriptor.scriptSetup.content, filename, idGenerator)\n const scriptLoc = descriptor.scriptSetup.loc\n const lineOffset = scriptLoc.start.line - 1\n for (const msg of scriptMessages) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n\n if (descriptor.script?.content) {\n const scriptMessages = extractFromTsx(descriptor.script.content, filename, idGenerator)\n const scriptLoc = descriptor.script.loc\n const lineOffset = scriptLoc.start.line - 1\n for (const msg of scriptMessages) {\n messages.push({\n ...msg,\n origin: {\n ...msg.origin,\n line: msg.origin.line + lineOffset,\n },\n })\n }\n }\n\n return messages\n}\n"],"mappings":"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"}
package/llms-full.txt ADDED
@@ -0,0 +1,297 @@
1
+ # @fluenti/cli
2
+
3
+ > CLI tool for the Fluenti i18n framework. Extracts translatable messages from Vue SFC and TSX/JSX source files, compiles PO/JSON catalogs to optimized ES modules with tree-shaking support, shows translation progress stats, and provides AI-powered translation via Claude Code or Codex CLI.
4
+
5
+ - Package: `@fluenti/cli`
6
+ - Binary: `fluenti`
7
+ - Docs: https://fluenti.dev
8
+ - Repository: https://github.com/usefluenti/fluenti/tree/main/packages/cli
9
+ - License: MIT
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pnpm add -D @fluenti/cli
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ The CLI loads config from `fluenti.config.ts`, `fluenti.config.js`, or `fluenti.config.mjs` in the project root. Config files are loaded via `jiti` for TypeScript support.
20
+
21
+ ```ts
22
+ // fluenti.config.ts
23
+ export default {
24
+ sourceLocale: 'en',
25
+ locales: ['en', 'ja', 'zh-CN'],
26
+ catalogDir: './locales',
27
+ format: 'po' as const,
28
+ include: ['./src/**/*.{vue,tsx,jsx,ts,js}'],
29
+ compileOutDir: './src/locales/compiled',
30
+ }
31
+ ```
32
+
33
+ ### Config Options (FluentiBuildConfig)
34
+
35
+ | Property | Type | Default | Description |
36
+ |---|---|---|---|
37
+ | `sourceLocale` | `string` | `'en'` | Source language code used as the base for extraction |
38
+ | `locales` | `string[]` | `['en']` | All supported locale codes |
39
+ | `catalogDir` | `string` | `'./locales'` | Directory where PO/JSON catalog files are stored |
40
+ | `format` | `'po' \| 'json'` | `'po'` | Catalog file format |
41
+ | `include` | `string[]` | `['./src/**/*.{vue,tsx,jsx,ts,js}']` | Glob patterns for source files to scan |
42
+ | `exclude` | `string[]` | `undefined` | Glob patterns to exclude from scanning |
43
+ | `compileOutDir` | `string` | `'./locales/compiled'` | Output directory for compiled JS/TS modules |
44
+ | `devWarnings` | `boolean` | `undefined` | Enable development-mode warnings |
45
+ | `fallbackChain` | `Record<string, Locale[]>` | `undefined` | Per-locale or wildcard fallback chains |
46
+ | `splitting` | `'dynamic' \| 'static' \| false` | `undefined` | Code splitting strategy |
47
+ | `defaultBuildLocale` | `string` | `undefined` | Default locale for static build strategy |
48
+ | `plugins` | `readonly FluentiPlugin[]` | `undefined` | Plugins that hook into the extract and compile pipelines |
49
+
50
+ All commands accept `--config path` to specify a custom config file path.
51
+
52
+ ## fluenti extract
53
+
54
+ Scan source files for translatable messages and update catalog files.
55
+
56
+ ```bash
57
+ fluenti extract [--config path]
58
+ ```
59
+
60
+ ### Extraction Sources
61
+
62
+ **Vue SFC files (`.vue`):**
63
+ - `v-t` directive: `<div v-t>Hello</div>`
64
+ - `v-t` with explicit ID: `<div v-t:greeting>Hello</div>`
65
+ - `v-t.plural` modifier: pipe-separated plural forms converted to ICU plural syntax
66
+ - `<Trans>` component: `message` prop or rich text children
67
+ - `<Plural>` component: category props (`zero`, `one`, `two`, `few`, `many`, `other`)
68
+ - `<Select>` component: direct case props or `options`
69
+ - direct-import `t` from `@fluenti/vue`
70
+ - runtime `t` bindings from `useI18n()`
71
+ - Template expressions: `{{ t('message.id') }}`
72
+
73
+ **TSX/JSX files:**
74
+ - Tagged templates: `` t`Hello ${name}` `` extracts as `Hello {name}`
75
+ - Function calls: `t('Hello')` extracts the string literal
76
+ - `<Trans>` component with `message` prop or children
77
+ - `<Plural>` component with category props
78
+ - `<Select>` component with direct case props or `options`
79
+ - direct-import `t` from framework packages
80
+ - runtime `t` bindings from `useI18n()`
81
+
82
+ ### Catalog Update Behavior
83
+
84
+ - New messages: added to the catalog with origin information (file path, line number)
85
+ - Existing messages: preserved with translations intact, origin updated
86
+ - Removed messages: marked as obsolete (not deleted, so translations are not lost)
87
+ - Reports per locale: `N added, N unchanged, N obsolete`
88
+
89
+ ### Catalog File Structure
90
+
91
+ Catalogs are stored as `{catalogDir}/{locale}.po` or `{catalogDir}/{locale}.json` depending on the `format` setting.
92
+
93
+ ## fluenti compile
94
+
95
+ Compile PO/JSON catalogs to optimized ES modules.
96
+
97
+ ```bash
98
+ fluenti compile [--config path]
99
+ ```
100
+
101
+ Generates JavaScript files with tree-shakeable named exports per message. Each export has an `@__PURE__` annotation so bundlers can eliminate unused messages from the production bundle. A `export default { ... }` re-export maps message ID keys to the named exports for default-import compatibility.
102
+
103
+ ```js
104
+ // locales/compiled/en.js
105
+ /* @__PURE__ */ export const _a1b2c3 = 'Hello, world!'
106
+ /* @__PURE__ */ export const _d4e5f6 = (v) => `Hello, ${v.name}!`
107
+
108
+ export default {
109
+ 'Hello, world!': _a1b2c3,
110
+ 'Hello, {name}!': _d4e5f6,
111
+ }
112
+ ```
113
+
114
+ Also generates an index module with locale list and lazy loaders:
115
+
116
+ ```js
117
+ // locales/compiled/index.js
118
+ export const locales = ['en', 'ja', 'zh-CN']
119
+ export const loaders = {
120
+ 'en': () => import('./en.js'),
121
+ 'ja': () => import('./ja.js'),
122
+ 'zh-CN': () => import('./zh-CN.js'),
123
+ }
124
+ ```
125
+
126
+ All locales share the same set of export names (union of all message IDs). Missing translations export an empty string, ensuring consistent imports across locales.
127
+
128
+ Message ID hashing uses the FNV-1a algorithm for deterministic, short export names.
129
+
130
+ Output files: `{compileOutDir}/{locale}.js` + `{compileOutDir}/index.js`
131
+
132
+ ## fluenti stats
133
+
134
+ Display a table showing translation progress per locale.
135
+
136
+ ```bash
137
+ fluenti stats [--config path]
138
+ ```
139
+
140
+ Output example:
141
+
142
+ ```
143
+ Locale | Total | Translated | Progress
144
+ --------+-------+------------+---------
145
+ en | 42 | 42 | 100.0%
146
+ ja | 42 | 38 | 90.5%
147
+ zh-CN | 42 | 20 | 47.6%
148
+ ```
149
+
150
+ Obsolete entries are excluded from all counts. An entry is considered translated if it has a non-empty `translation` field.
151
+
152
+ ## fluenti translate
153
+
154
+ AI-powered translation of untranslated messages using local CLI tools.
155
+
156
+ ```bash
157
+ fluenti translate [--provider claude|codex] [--locale ja] [--batch-size 50] [--config path]
158
+ ```
159
+
160
+ ### Options
161
+
162
+ | Option | Type | Default | Description |
163
+ |---|---|---|---|
164
+ | `--provider` | `'claude' \| 'codex'` | `'claude'` | AI CLI tool to use for translation |
165
+ | `--locale` | `string` | All non-source locales | Translate only this specific locale |
166
+ | `--batch-size` | `number` (positive integer) | `50` | Number of messages per AI call |
167
+ | `--config` | `string` | Auto-detected | Path to config file |
168
+
169
+ ### Providers
170
+
171
+ - **`claude`**: Invokes the Claude Code CLI via `claude -p "prompt"`. Requires `@anthropic-ai/claude-code` installed globally or in PATH.
172
+ - **`codex`**: Invokes the OpenAI Codex CLI via `codex -p "prompt" --full-auto`. Requires `@openai/codex` installed globally or in PATH.
173
+
174
+ ### How It Works
175
+
176
+ 1. Reads the catalog for each target locale
177
+ 2. Identifies untranslated entries (entries with empty or missing `translation` field, excluding obsolete entries)
178
+ 3. Batches untranslated messages by `--batch-size`
179
+ 4. For each batch, sends a structured prompt to the AI CLI containing:
180
+ - Source locale and target locale
181
+ - Source messages as a JSON object `{ id: sourceText, ... }`
182
+ - Rules: output valid JSON only, preserve ICU placeholders (`{name}`, `{count}`, `{gender}`), preserve HTML tags, no explanations
183
+ 5. Parses the JSON response from the AI output
184
+ 6. Writes successful translations back to the catalog file
185
+ 7. Warns about any messages that were not translated in the response
186
+
187
+ ### Translation Prompt Format
188
+
189
+ The prompt instructs the AI to:
190
+ - Translate from source locale to target locale
191
+ - Output ONLY valid JSON with the same keys and translated values
192
+ - Keep ICU MessageFormat placeholders unchanged (`{name}`, `{count}`, `{gender}`)
193
+ - Keep HTML tags unchanged
194
+ - Not add explanations or markdown formatting
195
+
196
+ ### Error Handling
197
+
198
+ - If the CLI tool is not found (`ENOENT`), provides installation instructions
199
+ - If the AI response does not contain valid JSON, throws an error
200
+ - Individual missing translations within a batch are warned but do not fail the entire operation
201
+ - Max response buffer: 10MB per AI invocation
202
+
203
+ ## Catalog Formats
204
+
205
+ ### PO Format (Gettext)
206
+
207
+ File extension: `.po`. Compatible with Poedit, Crowdin, Weblate, Transifex.
208
+
209
+ ```po
210
+ msgid ""
211
+ msgstr "Content-Type: text/plain; charset=UTF-8\n"
212
+
213
+ #: src/App.vue:3
214
+ msgid "Hello"
215
+ msgstr "Bonjour"
216
+
217
+ #: src/App.vue:5
218
+ #, fuzzy
219
+ msgid "Goodbye"
220
+ msgstr ""
221
+ ```
222
+
223
+ ### JSON Format
224
+
225
+ File extension: `.json`. Simple key-value structure for programmatic editing.
226
+
227
+ ```json
228
+ {
229
+ "Hello": {
230
+ "message": "Hello",
231
+ "translation": "Bonjour",
232
+ "origin": "src/App.vue:3"
233
+ },
234
+ "Goodbye": {
235
+ "message": "Goodbye",
236
+ "translation": "",
237
+ "origin": "src/App.vue:5",
238
+ "obsolete": false
239
+ }
240
+ }
241
+ ```
242
+
243
+ ## Programmatic API
244
+
245
+ ### translateCatalog(options)
246
+
247
+ The main translate function is exported for programmatic use:
248
+
249
+ ```ts
250
+ import { translateCatalog } from '@fluenti/cli'
251
+ import type { AIProvider, TranslateOptions } from '@fluenti/cli'
252
+
253
+ const { catalog, translated } = await translateCatalog({
254
+ provider: 'claude', // 'claude' | 'codex'
255
+ sourceLocale: 'en',
256
+ targetLocale: 'ja',
257
+ catalog: existingCatalog, // CatalogData object
258
+ batchSize: 50,
259
+ })
260
+
261
+ console.log(`Translated ${translated} messages`)
262
+ ```
263
+
264
+ **TranslateOptions**:
265
+
266
+ | Property | Type | Description |
267
+ |---|---|---|
268
+ | `provider` | `'claude' \| 'codex'` | AI provider to use |
269
+ | `sourceLocale` | `string` | Source language code |
270
+ | `targetLocale` | `string` | Target language code |
271
+ | `catalog` | `CatalogData` | Catalog data object with entries |
272
+ | `batchSize` | `number` | Messages per batch |
273
+
274
+ Returns `{ catalog: CatalogData, translated: number }`. The `catalog` is the same object passed in (mutated with translations), and `translated` is the count of newly translated messages.
275
+
276
+ ## Typical Workflow
277
+
278
+ ```bash
279
+ # 1. Extract messages from source code
280
+ fluenti extract
281
+
282
+ # 2. Translate using AI (translates all non-source locales)
283
+ fluenti translate --provider claude
284
+
285
+ # 3. Review and edit translations (optional, use Poedit or similar for PO files)
286
+
287
+ # 4. Compile to optimized JS modules with tree-shaking
288
+ fluenti compile
289
+
290
+ # 5. Check translation progress
291
+ fluenti stats
292
+ ```
293
+
294
+ ## Documentation
295
+
296
+ - Full docs: [fluenti.dev](https://fluenti.dev)
297
+ - Repository: [GitHub](https://github.com/usefluenti/fluenti)
package/llms.txt ADDED
@@ -0,0 +1,86 @@
1
+ # @fluenti/cli
2
+
3
+ > CLI tool for the Fluenti i18n framework. Extracts translatable messages from Vue SFC and TSX/JSX source files, compiles PO/JSON catalogs to optimized ES modules with tree-shaking support, shows translation progress stats, and provides AI-powered translation via Claude Code or Codex CLI.
4
+
5
+ - Package: `@fluenti/cli`
6
+ - Binary: `fluenti`
7
+ - Docs: https://fluenti.dev
8
+ - Repository: https://github.com/usefluenti/fluenti/tree/main/packages/cli
9
+ - License: MIT
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pnpm add -D @fluenti/cli
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ Create `fluenti.config.ts` (or `.js` / `.mjs`) in your project root:
20
+
21
+ ```ts
22
+ export default {
23
+ sourceLocale: 'en',
24
+ locales: ['en', 'ja', 'zh-CN'],
25
+ catalogDir: './locales',
26
+ format: 'po' as const, // 'po' or 'json'
27
+ include: ['./src/**/*.{vue,tsx,jsx,ts,js}'],
28
+ compileOutDir: './src/locales/compiled',
29
+ }
30
+ ```
31
+
32
+ | Property | Type | Default | Description |
33
+ |---|---|---|---|
34
+ | `sourceLocale` | `string` | `'en'` | Source language for extraction |
35
+ | `locales` | `string[]` | `['en']` | All supported locales |
36
+ | `catalogDir` | `string` | `'./locales'` | Directory for PO/JSON catalog files |
37
+ | `format` | `'po' \| 'json'` | `'po'` | Catalog format (PO is gettext-compatible; JSON is simple key-value) |
38
+ | `include` | `string[]` | `['./src/**/*.{vue,tsx,jsx,ts,js}']` | Glob patterns for source files to scan |
39
+ | `compileOutDir` | `string` | `'./locales/compiled'` | Output directory for compiled JS modules |
40
+
41
+ Config is loaded via `jiti` with fallback to defaults. Searched paths: `fluenti.config.ts`, `fluenti.config.js`, `fluenti.config.mjs`.
42
+
43
+ ## Commands
44
+
45
+ - `fluenti extract [--config path]`: Scan source files for translatable messages and update catalog files. Extracts from Vue SFC (`v-t`, `<Trans>`, `<Plural>`, `<Select>`, direct-import `t`, runtime `t` bindings) and TSX/JSX (`<Trans>`, `<Plural>`, `<Select>`, direct-import `t`, runtime `t` bindings). Reports added, unchanged, and obsolete counts per locale.
46
+ - `fluenti compile [--config path]`: Compile catalogs to optimized ES modules. Output: `.js` files with tree-shakeable named exports and `@__PURE__` annotations, plus a `export default { ... }` re-export with message ID keys. Also generates an `index.js` with locale list and lazy loaders.
47
+ - `fluenti stats [--config path]`: Display a table of translation progress per locale (total messages, translated count, completion percentage). Excludes obsolete entries.
48
+ - `fluenti translate [--config path] [--provider claude|codex] [--locale xx] [--batch-size N]`: AI-powered translation of untranslated messages using local CLI tools. Default provider: `claude` (Claude Code CLI). Default batch size: `50`. Preserves ICU MessageFormat placeholders and HTML tags. Translates all non-source locales by default, or a specific locale with `--locale`.
49
+
50
+ ## Workflow
51
+
52
+ ```bash
53
+ fluenti extract # 1. Extract messages from source
54
+ fluenti translate # 2. AI-translate untranslated messages
55
+ fluenti compile # 3. Compile to tree-shakeable ES modules
56
+ fluenti stats # 4. Check translation progress
57
+ ```
58
+
59
+ ## Programmatic API
60
+
61
+ All CLI commands are also available as importable functions from `@fluenti/cli`:
62
+
63
+ - `extractFromTsx(code, options?)`: Extract translatable messages from TSX/JSX source code
64
+ - `updateCatalog(catalog, extracted, options?)`: Merge extracted messages into an existing catalog, returning added/unchanged/obsolete counts
65
+ - `readJsonCatalog(path)` / `writeJsonCatalog(path, catalog)`: Read/write JSON format catalogs
66
+ - `readPoCatalog(path)` / `writePoCatalog(path, catalog)`: Read/write PO (gettext) format catalogs
67
+ - `compileCatalog(catalog, locale)`: Compile a catalog into an optimized ES module string
68
+ - `compileIndex(locales)`: Generate the `index.js` barrel with locale list and lazy loaders
69
+ - `collectAllIds(catalogs)`: Collect all unique message IDs across multiple catalogs
70
+ - `runExtract(options)`: Programmatic equivalent of `fluenti extract`
71
+ - `runCompile(options)`: Programmatic equivalent of `fluenti compile`
72
+ - `defineConfig(config)`: Type-safe config helper (identity function)
73
+ - `loadConfig(configPath?)`: Load and resolve `fluenti.config.ts` (uses jiti)
74
+ - `hashMessage(message, context?)`: Re-export of FNV-1a hash from `@fluenti/core`
75
+
76
+ ### Key Types
77
+
78
+ - `CatalogData`, `CatalogEntry`, `UpdateResult` — catalog data structures
79
+ - `CompileStats` — compilation statistics
80
+ - `RunCompileOptions`, `RunExtractOptions` — options for programmatic runners
81
+ - `FluentiBuildConfig` — re-exported from `@fluenti/core`
82
+
83
+ ## Documentation
84
+
85
+ - Full docs: [fluenti.dev](https://fluenti.dev)
86
+ - Repository: [GitHub](https://github.com/usefluenti/fluenti)