@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.
- package/dist/ai-provider.d.ts +14 -0
- package/dist/ai-provider.d.ts.map +1 -0
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/check.d.ts +47 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/cli.cjs +12 -9
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +638 -199
- package/dist/cli.js.map +1 -1
- package/dist/compile-CBSy1rNl.cjs +8 -0
- package/dist/compile-CBSy1rNl.cjs.map +1 -0
- package/dist/compile-cache.d.ts +26 -0
- package/dist/compile-cache.d.ts.map +1 -0
- package/dist/compile-jumIhf8m.js +192 -0
- package/dist/compile-jumIhf8m.js.map +1 -0
- package/dist/compile-runner.d.ts +4 -1
- package/dist/compile-runner.d.ts.map +1 -1
- package/dist/compile-worker.cjs +2 -0
- package/dist/compile-worker.cjs.map +1 -0
- package/dist/compile-worker.d.ts +18 -0
- package/dist/compile-worker.d.ts.map +1 -0
- package/dist/compile-worker.js +14 -0
- package/dist/compile-worker.js.map +1 -0
- package/dist/compile.d.ts.map +1 -1
- package/dist/config-loader.d.ts +1 -6
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/extract-cache-CmnwPMdA.js +304 -0
- package/dist/extract-cache-CmnwPMdA.js.map +1 -0
- package/dist/extract-cache-IDp-S-ux.cjs +10 -0
- package/dist/extract-cache-IDp-S-ux.cjs.map +1 -0
- package/dist/extract-cache.d.ts +33 -0
- package/dist/extract-cache.d.ts.map +1 -0
- package/dist/extract-runner.d.ts +12 -0
- package/dist/extract-runner.d.ts.map +1 -0
- package/dist/glossary.d.ts +5 -0
- package/dist/glossary.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +117 -25
- package/dist/index.js.map +1 -1
- package/dist/lint.d.ts +36 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/migrate.d.ts.map +1 -1
- package/dist/parallel-compile.d.ts +26 -0
- package/dist/parallel-compile.d.ts.map +1 -0
- package/dist/translate.d.ts.map +1 -1
- package/dist/{tsx-extractor-CcFjsYI-.js → tsx-extractor-B9fnGNTG.js} +61 -53
- package/dist/tsx-extractor-B9fnGNTG.js.map +1 -0
- package/dist/tsx-extractor-j_z4fneM.cjs +2 -0
- package/dist/tsx-extractor-j_z4fneM.cjs.map +1 -0
- package/dist/tsx-extractor.d.ts +2 -2
- package/dist/tsx-extractor.d.ts.map +1 -1
- package/dist/validation.d.ts +16 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/vue-extractor.cjs +2 -2
- package/dist/vue-extractor.cjs.map +1 -1
- package/dist/vue-extractor.d.ts +2 -2
- package/dist/vue-extractor.d.ts.map +1 -1
- package/dist/vue-extractor.js +42 -42
- package/dist/vue-extractor.js.map +1 -1
- package/llms-full.txt +297 -0
- package/llms.txt +86 -0
- package/package.json +4 -3
- package/dist/config-loader-BgAoTfxH.js +0 -387
- package/dist/config-loader-BgAoTfxH.js.map +0 -1
- package/dist/config-loader-D3RGkK_r.cjs +0 -16
- package/dist/config-loader-D3RGkK_r.cjs.map +0 -1
- package/dist/tsx-extractor-CcFjsYI-.js.map +0 -1
- package/dist/tsx-extractor-D__s_cP8.cjs +0 -2
- package/dist/tsx-extractor-D__s_cP8.cjs.map +0 -1
package/dist/vue-extractor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as e } from "./tsx-extractor-
|
|
2
|
-
import { createMessageId as t } from "@fluenti/core/
|
|
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,
|
|
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(
|
|
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 (
|
|
53
|
-
let e = f ??
|
|
52
|
+
} else if (m) {
|
|
53
|
+
let e = f ?? (p ?? t)(m);
|
|
54
54
|
i.push({
|
|
55
55
|
id: e,
|
|
56
|
-
message:
|
|
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 ??
|
|
68
|
+
let o = r.value.content, s = p ?? t, c = a?.value?.content ?? s(o, l);
|
|
69
69
|
i.push({
|
|
70
|
-
id:
|
|
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 ??
|
|
83
|
+
let o = p ?? t, s = a?.value?.content ?? o(r.message, l);
|
|
84
84
|
i.push({
|
|
85
|
-
id:
|
|
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,
|
|
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 (
|
|
107
|
-
let a = r.id ??
|
|
106
|
+
if (m) {
|
|
107
|
+
let a = r.id ?? (p ?? t)(m);
|
|
108
108
|
i.push({
|
|
109
109
|
id: a,
|
|
110
|
-
message:
|
|
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
|
|
142
|
-
for (; (
|
|
143
|
-
let
|
|
144
|
-
if (!
|
|
145
|
-
let
|
|
146
|
-
if (
|
|
147
|
-
let
|
|
148
|
-
for (let e of
|
|
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 +
|
|
152
|
+
line: e.origin.line + c
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
|
-
return
|
|
156
|
+
return i;
|
|
157
157
|
}
|
|
158
|
-
function m(t, r) {
|
|
159
|
-
let
|
|
160
|
-
if (
|
|
161
|
-
let t = e(
|
|
162
|
-
for (let e of t)
|
|
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
|
|
170
|
-
for (let e of
|
|
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 (
|
|
179
|
-
let t = e(
|
|
180
|
-
for (let e of t)
|
|
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 (
|
|
189
|
-
let t = e(
|
|
190
|
-
for (let e of t)
|
|
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
|
|
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)
|