@npm-questionpro/wick-ui-i18n 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -6,10 +6,15 @@ export interface AutoTranslateOptions {
6
6
  components?: string[];
7
7
 
8
8
  /**
9
- * Files to include in the transformation.
10
- * Can be string, RegExp, or array of string/RegExp.
9
+ * List of components to ignore text from.
11
10
  */
12
- include?: any;
11
+ ignoreComponents?: string[];
12
+
13
+ // /**
14
+ // * Files to include in the transformation.
15
+ // * Can be string, RegExp, or array of string/RegExp.
16
+ // */
17
+ // include?: any;
13
18
 
14
19
  /**
15
20
  * Enable debug logging for extraction process.
package/index.js CHANGED
@@ -1,180 +1,83 @@
1
- import { createFilter } from "vite";
2
- import MagicString from "magic-string";
3
- import { parse } from "@babel/parser";
4
- import _traverse from "@babel/traverse";
5
-
6
- const traverse = _traverse.default || _traverse;
7
-
8
- class TranslationProcessor {
9
- constructor(options) {
10
- this.components = new Set(options.components);
11
- this.ignoreComponents = new Set(
12
- options.ignoreComponents || [
13
- "WuIcon",
14
- "WuTranslateProvider",
15
- "WuHelpButton",
16
- "WuActivityLog",
17
- "WuAppHeader",
18
- "WuCopyToClipboard",
19
- "WuMenuIcon",
20
- "WuScrollArea",
21
- "WuDrawer",
22
- "WuLoader",
23
- "WuContentEditor",
24
- ],
25
- );
26
- this.dictionary = new Map();
27
- this.debugEnabled = options.debug || false;
28
- }
29
-
30
- log(...args) {
31
- if (this.debugEnabled) console.log("[wick-i18n]", ...args);
32
- }
33
-
34
- record(key, text, file) {
35
- if (this.dictionary.has(key) && this.dictionary.get(key) !== text) {
36
- console.warn(
37
- `[wick-i18n] Collision in ${file}\nKey: "${key}"\nNew: "${text}"`,
38
- );
39
- return;
40
- }
41
- this.dictionary.set(key, text);
42
- }
43
-
44
- shouldTranslate(path) {
45
- let isIgnored = false;
46
- let targetFound = false;
47
-
48
- path.findParent((p) => {
49
- if (!p.isJSXElement()) return false;
50
-
51
- const name =
52
- p.node.openingElement.name.name ||
53
- p.node.openingElement.name.property?.name;
54
- const attrs = p.node.openingElement.attributes || [];
55
-
56
- if (
57
- attrs.some((a) =>
58
- ["data-skip", "data-i18n-skip"].includes(a.name?.name),
59
- )
60
- ) {
61
- isIgnored = true;
62
- return true;
63
- }
64
-
65
- if (this.ignoreComponents.has(name)) {
66
- isIgnored = true;
67
- return true;
68
- }
69
-
70
- const hasWrapper = attrs.some(
71
- (a) => a.name?.name === "data-i18n-wrapper",
72
- );
73
- const isTarget = this.components.has(name) || name?.startsWith("Wu");
74
- // const isTarget = this.components.has(name);
75
-
76
- if (hasWrapper || isTarget) {
77
- targetFound = true;
78
- return true;
79
- }
80
-
81
- return false;
82
- });
83
-
84
- return targetFound && !isIgnored;
85
- }
86
-
87
- getExplicitKey(path) {
88
- const parent = path.findParent((p) => p.isJSXElement());
89
- const attr = parent?.node.openingElement.attributes.find(
90
- (a) => a.name?.name === "data-i18n-key",
91
- );
92
- if (!attr) return null;
93
- return attr.value.type === "StringLiteral"
94
- ? attr.value.value
95
- : attr.value.expression?.value;
96
- }
97
- }
1
+ /**
2
+ * @fileoverview wick-ui-i18n — Vite plugin that automatically wraps static JSX
3
+ * text inside Wick UI components with `<WuTranslate>` at build/dev time and
4
+ * emits a `wick-ui-i18n.json` translation dictionary.
5
+ *
6
+ * @example
7
+ * // vite.config.js
8
+ * import wickI18n from '@npm-questionpro/wick-ui-i18n';
9
+ *
10
+ * export default {
11
+ * plugins: [
12
+ * wickI18n({
13
+ * components: ['MyWidget'],
14
+ * ignoreComponents: ['MyRawHtml'],
15
+ * debug: true,
16
+ * }),
17
+ * ],
18
+ * };
19
+ */
98
20
 
21
+ import { createFilter } from "vite";
22
+ import { TranslationProcessor } from "./src/processor.js";
23
+ import { transformFile } from "./src/transform.js";
24
+ import { printReport } from "./src/debug.js";
25
+
26
+ /**
27
+ * @typedef {object} WickI18nOptions
28
+ * @property {string[]} [components] - Extra component names that trigger translation.
29
+ * @property {string[]} [ignoreComponents] - Component names to exclude from translation.
30
+ * @property {string|string[]|RegExp} [excludeFiles] - Files to skip (passed as `exclude` to Vite's createFilter).
31
+ * @property {boolean} [debug] - Log transform activity to the console.
32
+ */
33
+
34
+ /**
35
+ * Vite plugin factory for wick-ui-i18n.
36
+ *
37
+ * @param {WickI18nOptions} [options]
38
+ * @returns {import('vite').Plugin}
39
+ */
99
40
  export default function wickuiI18nPlugin(options = {}) {
100
- const processor = new TranslationProcessor(options);
101
- const filter = createFilter(options.include || [/\.(jsx|tsx)$/]);
41
+ const processor = new TranslationProcessor({
42
+ components: options.components || [],
43
+ ignoreComponents: options.ignoreComponents,
44
+ debug: options.debug,
45
+ });
46
+
47
+ const filter = createFilter([/\.(jsx|tsx)$/], options.excludeFiles);
102
48
 
103
49
  return {
104
50
  name: "wick-ui-i18n",
105
51
  enforce: "pre",
106
52
 
107
- transform(code, id) {
108
- if (!filter(id) || !code.includes("Wu")) return null;
109
-
110
- const ast = parse(code, {
111
- sourceType: "module",
112
- plugins: ["jsx", "typescript"],
113
- });
114
- const ms = new MagicString(code);
115
- let [needsImport, hasImport] = [false, false];
116
-
117
- const handleCapture = (path, text, start, end) => {
118
- const cleanText = text.trim();
119
- if (!cleanText || !processor.shouldTranslate(path)) return;
120
-
121
- const key = processor.getExplicitKey(path) || cleanText;
122
- processor.record(key, cleanText, id);
123
-
124
- const original = ms.slice(start, end);
125
- ms.overwrite(
126
- start,
127
- end,
128
- `<WuTranslate __i18nKey=${JSON.stringify(key)}></WuTranslate>`,
129
- );
130
- needsImport = true;
131
- };
132
-
133
- traverse(ast, {
134
- ImportDeclaration(path) {
135
- if (path.node.source.value.includes("wick-ui-lib")) {
136
- hasImport = path.node.specifiers.some(
137
- (s) => s.imported?.name === "WuTranslate",
138
- );
139
- }
140
- },
141
- JSXText(path) {
142
- const text = path.node.value;
143
- const trimmed = text.trim();
144
- const start = path.node.start + text.indexOf(trimmed);
145
- handleCapture(path, trimmed, start, start + trimmed.length);
146
- },
147
- JSXExpressionContainer(path) {
148
- const expr = path.node.expression;
149
- let text = null;
150
- if (expr.type === "StringLiteral") text = expr.value;
151
- else if (
152
- expr.type === "TemplateLiteral" &&
153
- !expr.expressions.length
154
- ) {
155
- text = expr.quasis[0].value.cooked;
156
- }
157
- if (text) handleCapture(path, text, path.node.start, path.node.end);
158
- },
159
- });
160
-
161
- if (needsImport && !hasImport) {
162
- ms.prepend(
163
- `import { WuTranslate } from '@npm-questionpro/wick-ui-lib';\n`,
164
- );
165
- }
166
-
167
- return needsImport
168
- ? { code: ms.toString(), map: ms.generateMap({ hires: true }) }
169
- : null;
170
- },
171
-
53
+ /**
54
+ * Clear state on every build so stale keys don't accumulate across
55
+ * watch-mode rebuilds.
56
+ */
172
57
  buildStart() {
173
58
  processor.dictionary.clear();
59
+ processor.entries = [];
174
60
  },
175
61
 
62
+ /**
63
+ * Transform a single file: skip fast if it has no "Wu" tokens, then run
64
+ * the full AST rewrite.
65
+ *
66
+ * @param {string} code
67
+ * @param {string} id
68
+ */
69
+ transform(code, id) {
70
+ if (!filter(id) || !code.includes("Wu")) return null;
71
+ return transformFile(code, id, processor);
72
+ },
73
+
74
+ /**
75
+ * Expose the collected dictionary at `GET /wick-ui-i18n.json` during dev.
76
+ *
77
+ * @param {import('vite').ViteDevServer} server
78
+ */
176
79
  configureServer(server) {
177
- server.middlewares.use("/wick-ui-i18n.json", (req, res) => {
80
+ server.middlewares.use("/wick-ui-i18n.json", (_req, res) => {
178
81
  res.setHeader("Content-Type", "application/json");
179
82
  res.end(
180
83
  JSON.stringify(Object.fromEntries(processor.dictionary), null, 2),
@@ -182,6 +85,7 @@ export default function wickuiI18nPlugin(options = {}) {
182
85
  });
183
86
  },
184
87
 
88
+ /** Emit the translation dictionary as a build asset and print debug table. */
185
89
  generateBundle() {
186
90
  this.emitFile({
187
91
  type: "asset",
@@ -192,6 +96,8 @@ export default function wickuiI18nPlugin(options = {}) {
192
96
  2,
193
97
  ),
194
98
  });
99
+
100
+ printReport(processor.entries);
195
101
  },
196
102
  };
197
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npm-questionpro/wick-ui-i18n",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "license": "ISC",
5
5
  "description": "Auto-translation AST wrapper for Wick UI",
6
6
  "type": "module",
package/src/debug.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @fileoverview Build-time debug reporter — prints a formatted table of every
3
+ * captured translation entry (text, source file, JSX component tree) to stdout
4
+ * after the bundle is generated.
5
+ */
6
+
7
+ import { basename } from "node:path";
8
+
9
+ /**
10
+ * Walk up the Babel path and collect JSX element names from outermost inward.
11
+ *
12
+ * @param {import('@babel/traverse').NodePath} path
13
+ * @returns {string} e.g. `"WuProvider > div > WuButton"`
14
+ */
15
+ export function getComponentTree(path) {
16
+ const parts = [];
17
+ path.findParent((p) => {
18
+ if (p.isJSXElement()) {
19
+ const name =
20
+ p.node.openingElement.name.name ||
21
+ p.node.openingElement.name.property?.name;
22
+ if (name) parts.unshift(name);
23
+ }
24
+ return false;
25
+ });
26
+ return parts.join(" > ") || "(root)";
27
+ }
28
+
29
+ /**
30
+ * @typedef {object} DebugEntry
31
+ * @property {string} key - i18n key used in WuTranslate.
32
+ * @property {string} text - Original source text.
33
+ * @property {string} file - Absolute path of the source file.
34
+ * @property {string} componentTree - Ancestor JSX chain, outermost first.
35
+ */
36
+
37
+ /**
38
+ * Print a box-drawing ASCII table of all captured translation entries.
39
+ * Columns are auto-sized to content.
40
+ *
41
+ * @param {DebugEntry[]} entries
42
+ */
43
+ export function printReport(entries) {
44
+ if (!entries.length) {
45
+ console.log("\n[wick-i18n] No translations captured.\n");
46
+ return;
47
+ }
48
+
49
+ const headers = ["Text", "File", "Component Tree"];
50
+
51
+ const rows = entries.map((e) => [e.text, basename(e.file), e.componentTree]);
52
+
53
+ const cols = headers.length;
54
+ const widths = headers.map((h, i) =>
55
+ Math.max(h.length, ...rows.map((r) => r[i].length)),
56
+ );
57
+
58
+ const pad = (str, w) => str.padEnd(w);
59
+ const sep = (l, m, r, fill) =>
60
+ l + widths.map((w) => fill.repeat(w + 2)).join(m) + r;
61
+
62
+ const top = sep("┌", "┬", "┐", "─");
63
+ const mid = sep("├", "┼", "┤", "─");
64
+ const bottom = sep("└", "┴", "┘", "─");
65
+ const row = (cells) =>
66
+ "│" + cells.map((c, i) => ` ${pad(c, widths[i])} `).join("│") + "│";
67
+
68
+ const lines = [
69
+ "",
70
+ `[wick-i18n] Build report — ${entries.length} translation(s) captured`,
71
+ top,
72
+ row(headers),
73
+ mid,
74
+ ...rows.map(row),
75
+ bottom,
76
+ "",
77
+ ];
78
+
79
+ console.log(lines.join("\n"));
80
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @fileoverview TranslationProcessor — maintains the translation dictionary and
3
+ * decides which JSX nodes should be wrapped with WuTranslate.
4
+ */
5
+
6
+ /** Components always excluded from translation regardless of user config. */
7
+ const DEFAULT_IGNORE = [
8
+ "WuIcon",
9
+ "WuTranslateProvider",
10
+ "WuHelpButton",
11
+ "WuActivityLog",
12
+ "WuAppHeader",
13
+ "WuAPpHeadeMenu",
14
+ "WuCopyToClipboard",
15
+ "WuMenuIcon",
16
+ "WuScrollArea",
17
+ "WuDrawer",
18
+ "WuLoader",
19
+ "WuContentEditor",
20
+ ];
21
+
22
+ export class TranslationProcessor {
23
+ /**
24
+ * @param {object} options
25
+ * @param {string[]} options.components - Component names that trigger translation.
26
+ * @param {string[]} [options.ignoreComponents] - Extra components to exclude.
27
+ * @param {boolean} [options.debug] - Enable verbose logging.
28
+ */
29
+ constructor(options) {
30
+ this.components = new Set(options.components);
31
+ this.ignoreComponents = new Set(
32
+ DEFAULT_IGNORE.concat(options.ignoreComponents || []),
33
+ );
34
+ /** @type {Map<string, string>} key → original text */
35
+ this.dictionary = new Map();
36
+ /** @type {import('./debug.js').DebugEntry[]} */
37
+ this.entries = [];
38
+ this.debugEnabled = options.debug || false;
39
+ }
40
+
41
+ /**
42
+ * Conditional logger — only emits when `debug: true`.
43
+ * @param {...unknown} args
44
+ */
45
+ log(...args) {
46
+ if (this.debugEnabled) console.log("[wick-i18n]", ...args);
47
+ }
48
+
49
+ /**
50
+ * Store a key→text pair and append a debug entry.
51
+ * Warns on collision (same key, different text).
52
+ *
53
+ * @param {string} key
54
+ * @param {string} text
55
+ * @param {string} file - Source file path.
56
+ * @param {string} componentTree - JSX ancestor chain for the debug report.
57
+ */
58
+ record(key, text, file, componentTree) {
59
+ if (this.dictionary.has(key) && this.dictionary.get(key) !== text) {
60
+ console.warn(
61
+ `[wick-i18n] Collision in ${file}\nKey: "${key}"\nNew: "${text}"`,
62
+ );
63
+ return;
64
+ }
65
+ this.dictionary.set(key, text);
66
+ this.entries.push({ key, text, file, componentTree });
67
+ }
68
+
69
+ /**
70
+ * Walk up the JSX tree from `path` to determine if the node is inside a
71
+ * component that should be translated.
72
+ *
73
+ * Rules (first match wins, walking outward):
74
+ * - `data-skip` / `data-i18n-skip` attr → ignored
75
+ * - component in `ignoreComponents` → ignored
76
+ * - `data-i18n-wrapper` attr OR component starts with "Wu" / is in `components` → translate
77
+ *
78
+ * @param {import('@babel/traverse').NodePath} path
79
+ * @returns {boolean}
80
+ */
81
+ shouldTranslate(path) {
82
+ let isIgnored = false;
83
+ let targetFound = false;
84
+
85
+ path.findParent((p) => {
86
+ if (!p.isJSXElement()) return false;
87
+
88
+ const name =
89
+ p.node.openingElement.name.name ||
90
+ p.node.openingElement.name.property?.name;
91
+ const attrs = p.node.openingElement.attributes || [];
92
+
93
+ if (
94
+ attrs.some((a) =>
95
+ ["data-skip", "data-i18n-skip"].includes(a.name?.name),
96
+ )
97
+ ) {
98
+ isIgnored = true;
99
+ return true;
100
+ }
101
+
102
+ if (this.ignoreComponents.has(name)) {
103
+ isIgnored = true;
104
+ return true;
105
+ }
106
+
107
+ const hasWrapper = attrs.some(
108
+ (a) => a.name?.name === "data-i18n-wrapper",
109
+ );
110
+ const isTarget = this.components.has(name) || name?.startsWith("Wu");
111
+
112
+ if (hasWrapper || isTarget) {
113
+ targetFound = true;
114
+ return true;
115
+ }
116
+
117
+ return false;
118
+ });
119
+
120
+ return targetFound && !isIgnored;
121
+ }
122
+
123
+ /**
124
+ * Return the explicit i18n key from the nearest ancestor JSX element's
125
+ * `data-i18n-key` attribute, or `null` if absent.
126
+ *
127
+ * @param {import('@babel/traverse').NodePath} path
128
+ * @returns {string|null}
129
+ */
130
+ getExplicitKey(path) {
131
+ let result = null;
132
+ path.findParent((p) => {
133
+ if (!p.isJSXElement()) return false;
134
+ const attr = p.node.openingElement.attributes.find(
135
+ (a) => a.name?.name === "data-i18n-key",
136
+ );
137
+ if (!attr) return false;
138
+ const val =
139
+ attr.value.type === "StringLiteral"
140
+ ? attr.value.value
141
+ : attr.value.expression?.value;
142
+ if (!val) {
143
+ console.warn(
144
+ `[wick-i18n] data-i18n-key on <${p.node.openingElement.name.name}> is dynamic or empty — falling back to text content.`,
145
+ );
146
+ return true;
147
+ }
148
+ result = val;
149
+ return true;
150
+ });
151
+ return result;
152
+ }
153
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @fileoverview AST transform — parses a JSX/TSX file, rewrites translatable
3
+ * text nodes to `<WuTranslate>`, and prepends the import if needed.
4
+ */
5
+
6
+ import MagicString from "magic-string";
7
+ import { parse } from "@babel/parser";
8
+ import _traverse from "@babel/traverse";
9
+ import { getComponentTree } from "./debug.js";
10
+
11
+ const traverse = _traverse.default || _traverse;
12
+
13
+ /** Babel parser plugins applied to every file. */
14
+ const BABEL_PLUGINS = ["jsx", "typescript"];
15
+
16
+ /** @param {import('@babel/types').Node} node @returns {string|null} */
17
+ function getStaticString(node) {
18
+ if (node.type === "StringLiteral") return node.value;
19
+ if (node.type === "TemplateLiteral" && !node.expressions.length)
20
+ return node.quasis[0].value.cooked;
21
+ return null;
22
+ }
23
+
24
+ /**
25
+ * Replace a translatable text node with a `<WuTranslate>` element and record
26
+ * the key in the processor's dictionary.
27
+ *
28
+ * @param {import('@babel/traverse').NodePath} path - Path of the text node.
29
+ * @param {string} text - Trimmed text content.
30
+ * @param {number} start - Absolute start offset in source.
31
+ * @param {number} end - Absolute end offset in source.
32
+ * @param {MagicString} ms - Mutable source string.
33
+ * @param {import('./processor.js').TranslationProcessor} processor
34
+ * @param {string} id - File path (for collision warnings).
35
+ * @returns {boolean} `true` when replacement was made.
36
+ */
37
+ function handleCapture(path, text, start, end, ms, processor, id, skipExplicitKey = false) {
38
+ const cleanText = text.trim().replace(/\n/g, " ").replace(/\s{2,}/g, " ");
39
+ if (!cleanText || !processor.shouldTranslate(path)) return false;
40
+
41
+ const key = (!skipExplicitKey && processor.getExplicitKey(path)) || cleanText;
42
+ processor.record(key, cleanText, id, getComponentTree(path));
43
+
44
+ ms.overwrite(
45
+ start,
46
+ end,
47
+ `<WuTranslate __i18nKey=${JSON.stringify(key)}></WuTranslate>`,
48
+ );
49
+ return true;
50
+ }
51
+
52
+ /**
53
+ * Parse `code`, replace all translatable JSX text nodes, and prepend the
54
+ * `WuTranslate` import when necessary.
55
+ *
56
+ * @param {string} code - Raw source of the file.
57
+ * @param {string} id - File path.
58
+ * @param {import('./processor.js').TranslationProcessor} processor
59
+ * @returns {{ code: string, map: object } | null} Transformed result, or
60
+ * `null` when no changes were made.
61
+ */
62
+ export function transformFile(code, id, processor) {
63
+ const ast = parse(code, {
64
+ sourceType: "module",
65
+ plugins: BABEL_PLUGINS,
66
+ });
67
+
68
+ const ms = new MagicString(code);
69
+ let needsImport = false;
70
+ let hasImport = false;
71
+
72
+ traverse(ast, {
73
+ /**
74
+ * Track whether `WuTranslate` is already imported from wick-ui-lib so we
75
+ * don't duplicate the import statement.
76
+ */
77
+ ImportDeclaration(path) {
78
+ if (path.node.source.value.includes("wick-ui-lib")) {
79
+ hasImport = path.node.specifiers.some(
80
+ (s) => s.imported?.name === "WuTranslate",
81
+ );
82
+ }
83
+ },
84
+
85
+ /** Plain JSX text: `<Foo>Hello world</Foo>` */
86
+ JSXText(path) {
87
+ const text = path.node.value;
88
+ const trimmed = text.trim();
89
+ const start = path.node.start + text.indexOf(trimmed);
90
+ if (
91
+ handleCapture(
92
+ path,
93
+ trimmed,
94
+ start,
95
+ start + trimmed.length,
96
+ ms,
97
+ processor,
98
+ id,
99
+ )
100
+ ) {
101
+ needsImport = true;
102
+ }
103
+ },
104
+
105
+ /**
106
+ * JSX expression containers with a static string value:
107
+ * `<Foo>{"Hello"}</Foo>` or `<Foo>{\`Hello\`}</Foo>`
108
+ *
109
+ * Attribute values (`variant={'secondary'}`) are skipped — their parent is
110
+ * a JSXAttribute, not a JSXElement child position.
111
+ */
112
+ JSXExpressionContainer(path) {
113
+ if (path.parent.type === "JSXAttribute") return;
114
+
115
+ const expr = path.node.expression;
116
+ let text = null;
117
+
118
+ if (expr.type === "StringLiteral") {
119
+ text = expr.value;
120
+ } else if (expr.type === "TemplateLiteral" && !expr.expressions.length) {
121
+ text = expr.quasis[0].value.cooked;
122
+ } else if (expr.type === "ConditionalExpression") {
123
+ const consText = getStaticString(expr.consequent);
124
+ const altText = getStaticString(expr.alternate);
125
+ let changed = false;
126
+ if (consText !== null)
127
+ changed =
128
+ handleCapture(
129
+ path,
130
+ consText,
131
+ expr.consequent.start,
132
+ expr.consequent.end,
133
+ ms,
134
+ processor,
135
+ id,
136
+ true,
137
+ ) || changed;
138
+ if (altText !== null)
139
+ changed =
140
+ handleCapture(
141
+ path,
142
+ altText,
143
+ expr.alternate.start,
144
+ expr.alternate.end,
145
+ ms,
146
+ processor,
147
+ id,
148
+ true,
149
+ ) || changed;
150
+ if (changed) needsImport = true;
151
+ return;
152
+ }
153
+
154
+ if (
155
+ text &&
156
+ handleCapture(
157
+ path,
158
+ text,
159
+ path.node.start,
160
+ path.node.end,
161
+ ms,
162
+ processor,
163
+ id,
164
+ )
165
+ ) {
166
+ needsImport = true;
167
+ }
168
+ },
169
+ });
170
+
171
+ if (!needsImport) return null;
172
+
173
+ if (!hasImport) {
174
+ ms.prepend(`import { WuTranslate } from '@npm-questionpro/wick-ui-lib';\n`);
175
+ }
176
+
177
+ return { code: ms.toString(), map: ms.generateMap({ hires: true }) };
178
+ }
@@ -9,6 +9,8 @@ function transform(code, options = {}) {
9
9
  }
10
10
 
11
11
  describe("Wick UI i18n Vite Plugin", () => {
12
+ // ─── Core translation ──────────────────────────────────────────────────────
13
+
12
14
  it("1. Translates basic Wu* components and injects import", () => {
13
15
  const code = `<WuButton>Submit</WuButton>`;
14
16
  const result = transform(code);
@@ -50,6 +52,21 @@ describe("Wick UI i18n Vite Plugin", () => {
50
52
  );
51
53
  });
52
54
 
55
+ it("6b. data-i18n-key propagates through nested elements", () => {
56
+ const code = `<WuButton data-i18n-key="btn_login"><span>Log In</span></WuButton>`;
57
+ const result = transform(code);
58
+ expect(result).toContain(`<WuTranslate __i18nKey="btn_login"></WuTranslate>`);
59
+ expect(result).not.toContain(`__i18nKey="Log In"`);
60
+ });
61
+
62
+ it("6c. data-i18n-key on ternary parent is ignored — each branch uses its own text as key", () => {
63
+ const code = `<WuButton data-i18n-key="btn">{flag ? "Yes" : "No"}</WuButton>`;
64
+ const result = transform(code);
65
+ expect(result).toContain(`<WuTranslate __i18nKey="Yes"></WuTranslate>`);
66
+ expect(result).toContain(`<WuTranslate __i18nKey="No"></WuTranslate>`);
67
+ expect(result).not.toContain(`__i18nKey="btn"`);
68
+ });
69
+
53
70
  it("7. Translates JSX Expression Containers (Strings in braces)", () => {
54
71
  const code = `<WuButton>{"Hello World"}</WuButton>`;
55
72
  const result = transform(code);
@@ -58,14 +75,13 @@ describe("Wick UI i18n Vite Plugin", () => {
58
75
  );
59
76
  });
60
77
 
61
- // it("8. Supports custom components via options", () => {
62
- // const code = `<CustomCard>Welcome</CustomCard>`;
63
- // const result = transform(code, { components: ["CustomCard"] });
64
- // expect(result).toContain(
65
- // `<WuTranslate __i18nKey="Welcome">Welcome</WuTranslate>`,
66
- // );
67
- // });
68
- //
78
+ it("8. Does not translate JSX attribute string values", () => {
79
+ const code = `<WuButton variant={'secondary'}>Submit</WuButton>`;
80
+ const result = transform(code);
81
+ expect(result).not.toContain(`__i18nKey="secondary"`);
82
+ expect(result).toContain(`<WuTranslate __i18nKey="Submit"></WuTranslate>`);
83
+ });
84
+
69
85
  it("9. Supports data-i18n-wrapper on non-target tags", () => {
70
86
  const code = `<span data-i18n-wrapper>Wrapped Text</span>`;
71
87
  const triggerCode = `import { WuPlaceholder } from 'lib';\n` + code;
@@ -74,4 +90,98 @@ describe("Wick UI i18n Vite Plugin", () => {
74
90
  `<WuTranslate __i18nKey="Wrapped Text"></WuTranslate>`,
75
91
  );
76
92
  });
93
+
94
+ it("10. Skips files matching excludeFiles", () => {
95
+ const code = `<WuButton>Submit</WuButton>`;
96
+ const plugin = wickuiI18nPlugin({ excludeFiles: ["**/ignored/**"] });
97
+ const hit = plugin.transform(code, "src/components/Form.jsx");
98
+ const miss = plugin.transform(code, "src/ignored/Form.jsx");
99
+ expect(hit?.code).toContain("WuTranslate");
100
+ expect(miss).toBeNull();
101
+ });
102
+
103
+ // ─── Newline stripping ─────────────────────────────────────────────────────
104
+
105
+ it("11. Strips newline from JSX text node", () => {
106
+ const code = `<WuButton>Hello\nWorld</WuButton>`;
107
+ const result = transform(code);
108
+ expect(result).toContain(
109
+ `<WuTranslate __i18nKey="Hello World"></WuTranslate>`,
110
+ );
111
+ });
112
+
113
+ it("11b. Collapses multiple newlines/spaces into a single space", () => {
114
+ const code = `<WuButton>Hello\n\n World</WuButton>`;
115
+ const result = transform(code);
116
+ expect(result).toContain(
117
+ `<WuTranslate __i18nKey="Hello World"></WuTranslate>`,
118
+ );
119
+ });
120
+
121
+ // ─── Ternary expressions ───────────────────────────────────────────────────
122
+
123
+ it("12. Translates both static branches of a ternary", () => {
124
+ const code = `<WuButton>{isActive ? "Deactivate" : "Activate"}</WuButton>`;
125
+ const result = transform(code);
126
+ expect(result).toContain(
127
+ `<WuTranslate __i18nKey="Deactivate"></WuTranslate>`,
128
+ );
129
+ expect(result).toContain(
130
+ `<WuTranslate __i18nKey="Activate"></WuTranslate>`,
131
+ );
132
+ expect(result).toContain(`isActive ?`);
133
+ expect(result).toContain(`import { WuTranslate }`);
134
+ });
135
+
136
+ it("12b. Translates only the static branch when one side is dynamic", () => {
137
+ const code = `<WuButton>{isActive ? "Deactivate" : dynamicLabel}</WuButton>`;
138
+ const result = transform(code);
139
+ expect(result).toContain(
140
+ `<WuTranslate __i18nKey="Deactivate"></WuTranslate>`,
141
+ );
142
+ expect(result).toContain(`dynamicLabel`);
143
+ expect(result).not.toContain(`__i18nKey="dynamicLabel"`);
144
+ });
145
+
146
+ it("12c. Skips ternary when both branches are dynamic", () => {
147
+ const code = `<WuButton>{isActive ? labelA : labelB}</WuButton>`;
148
+ const result = transform(code);
149
+ expect(result).not.toContain(`WuTranslate`);
150
+ expect(result).toContain(`labelA`);
151
+ expect(result).toContain(`labelB`);
152
+ });
153
+
154
+ it("12d. Translates ternary with template literal branches", () => {
155
+ const code = "<WuButton>{flag ? `Yes` : `No`}</WuButton>";
156
+ const result = transform(code);
157
+ expect(result).toContain(`<WuTranslate __i18nKey="Yes"></WuTranslate>`);
158
+ expect(result).toContain(`<WuTranslate __i18nKey="No"></WuTranslate>`);
159
+ });
160
+
161
+ it("12e. Skips ternary inside ignored component", () => {
162
+ const code = `<WuIcon>{flag ? "A" : "B"}</WuIcon>`;
163
+ const result = transform(code);
164
+ expect(result).not.toContain(`WuTranslate`);
165
+ });
166
+
167
+ it("12f. Respects data-skip on parent of ternary", () => {
168
+ const code = `<WuButton data-skip>{flag ? "A" : "B"}</WuButton>`;
169
+ const result = transform(code);
170
+ expect(result).not.toContain(`WuTranslate`);
171
+ });
172
+
173
+ it("12g. Nested ternary: translates first branch, skips inner ternary", () => {
174
+ // {a ? "A" : b ? "B" : "C"} — alternate is ConditionalExpression, not a static string
175
+ const code = `<WuButton>{a ? "A" : b ? "B" : "C"}</WuButton>`;
176
+ const result = transform(code);
177
+ expect(result).toContain(`<WuTranslate __i18nKey="A"></WuTranslate>`);
178
+ // inner ternary branches are not translated (known v1 limitation)
179
+ expect(result).toContain(`b ? "B" : "C"`);
180
+ });
181
+
182
+ it("12h. Ternary outside any Wu/wrapper component is not translated", () => {
183
+ const code = `<div>{flag ? "A" : "B"}</div>`;
184
+ const result = transform(code);
185
+ expect(result).not.toContain(`WuTranslate`);
186
+ });
77
187
  });