@npm-questionpro/wick-ui-i18n 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ Here is the stripped-down, one-liner style README with the logic map included.
2
+
3
+ # Wick UI Auto I18n
4
+
5
+ **Build-time extractor that wraps static strings in `<WuTranslate>` and generates a JSON dictionary.**
6
+
7
+ ---
8
+
9
+ ### Quick Start
10
+
11
+ ```javascript
12
+ import wickuiI18nPlugin from "./plugins/wickui-i18n";
13
+ export default {
14
+ plugins: [wickuiI18nPlugin({ debug: true })],
15
+ };
16
+ ```
17
+
18
+ ---
19
+
20
+ ### 🛠 Attributes
21
+
22
+ - **`data-i18n-wrapper`**: Forces extraction on non-Wu components (e.g., `<div data-i18n-wrapper>Text</div>`).
23
+ - **`data-i18n-key="custom.key"`**: Overrides the default "text-as-key" behavior.
24
+ - **`data-i18n-skip`**: Prevents the plugin from touching the element or its children.
25
+
26
+ ---
27
+
28
+ ### 📖 Rules & Logic
29
+
30
+ - **Auto-Detection**: Targets all `Wu*` prefixed components by default.
31
+ - **Supported**: Static `JSXText`, `{"Strings"}`, and non-dynamic `{`Template Literals`}`.
32
+ - **Ignored**: Any expression containing variables (e.g., `{`Hello ${name}`}`).
33
+ - **Output**: Generates `dist/wick_extracted_strings.json` during bundle phase.
34
+
35
+ ---
36
+
37
+ ### ⚙️ Config Options
38
+
39
+ | Option | Type | Default | Description |
40
+ | ------------------ | ---------- | --------------- | -------------------------------------------- | ----------------- |
41
+ | `components` | `string[]` | `[]` | Component names to scan (empty = all `Wu*`). |
42
+ | `ignoreComponents` | `string[]` | `["WuIcon"...]` | Component names to explicitly skip. |
43
+ | `include` | `RegExp[]` | `[/\.(jsx | tsx)$/]` | Files to process. |
44
+
45
+ ---
46
+
47
+ ### 🔄 Logic Map (Pseudo-code)
48
+
49
+ ```text
50
+ FOR each File in the Project:
51
+ IF File matches IncludeFilter AND contains "Wu":
52
+ PARSE File into an AST (Abstract Syntax Tree)
53
+ INITIALIZE MagicString (for non-destructive editing)
54
+
55
+ TRAVERSE the AST:
56
+
57
+ // 1. IMPORT CHECK
58
+ IF Node is an Import from '@wick-ui-lib':
59
+ MARK 'WuTranslate' as already imported if found
60
+
61
+ // 2. STRING DISCOVERY
62
+ IF Node is JSXText OR (JSXExpressionContainer WITH StaticString):
63
+ SET CandidateText = Node.Value
64
+
65
+ // 3. HIERARCHY EVALUATION (The "shouldTranslate" logic)
66
+ WALK UP from Node to Parents:
67
+ IF Parent has [data-skip] attribute:
68
+ ABORT (Don't translate this node)
69
+
70
+ IF Parent has [data-i18n-wrapper] attribute:
71
+ MARK as "Valid Target" and STOP walking up
72
+
73
+ IF Parent.Name starts with "Wu" OR is in CustomList:
74
+ IF Parent.Name is NOT in IgnoreList:
75
+ MARK as "Valid Target" and STOP walking up
76
+
77
+ // 4. TRANSFORMATION
78
+ IF "Valid Target" was found:
79
+ GET ExplicitKey from [data-i18n-key] OR USE CandidateText
80
+ STORE { Key, CandidateText } in GlobalDictionary
81
+ OVERWRITE original code with:
82
+ `<WuTranslate __i18nKey="Key">OriginalText</WuTranslate>`
83
+ SET NeedsImport = True
84
+
85
+ // 5. FINAL ASSEMBLY
86
+ IF NeedsImport AND NOT 'WuTranslate' Imported:
87
+ PREPEND import statement to top of file
88
+
89
+ RETURN Modified Code + Source Maps
90
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export interface AutoTranslateOptions {
2
+ /**
3
+ * List of components to extract text from.
4
+ * If empty, matches all Wu* components automatically.
5
+ */
6
+ components?: string[];
7
+
8
+ /**
9
+ * Files to include in the transformation.
10
+ * Can be string, RegExp, or array of string/RegExp.
11
+ */
12
+ include?: any;
13
+
14
+ /**
15
+ * Enable debug logging for extraction process.
16
+ */
17
+ debug?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Vite plugin that extracts text from Wick UI components and wraps them in WuTranslate.
22
+ */
23
+ export default function autoTranslate(options?: AutoTranslateOptions): any;
package/index.js ADDED
@@ -0,0 +1,172 @@
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 || ["WuButton"]);
11
+ this.ignoreComponents = new Set(
12
+ options.ignoreComponents || ["WuIcon", "WuTranslateProvider"],
13
+ );
14
+ this.dictionary = new Map();
15
+ this.debugEnabled = options.debug || false;
16
+ }
17
+
18
+ log(...args) {
19
+ if (this.debugEnabled) console.log("[wick-i18n]", ...args);
20
+ }
21
+
22
+ record(key, text, file) {
23
+ if (this.dictionary.has(key) && this.dictionary.get(key) !== text) {
24
+ console.warn(
25
+ `[wick-i18n] Collision in ${file}\nKey: "${key}"\nNew: "${text}"`,
26
+ );
27
+ return;
28
+ }
29
+ this.dictionary.set(key, text);
30
+ }
31
+
32
+ shouldTranslate(path) {
33
+ let isIgnored = false;
34
+ let targetFound = false;
35
+
36
+ path.findParent((p) => {
37
+ if (!p.isJSXElement()) return false;
38
+
39
+ const name =
40
+ p.node.openingElement.name.name ||
41
+ p.node.openingElement.name.property?.name;
42
+ const attrs = p.node.openingElement.attributes || [];
43
+
44
+ if (
45
+ attrs.some((a) =>
46
+ ["data-skip", "data-i18n-skip"].includes(a.name?.name),
47
+ )
48
+ ) {
49
+ isIgnored = true;
50
+ return true;
51
+ }
52
+
53
+ if (this.ignoreComponents.has(name)) {
54
+ isIgnored = true;
55
+ return true;
56
+ }
57
+
58
+ const hasWrapper = attrs.some(
59
+ (a) => a.name?.name === "data-i18n-wrapper",
60
+ );
61
+ // const isTarget = this.components.has(name) || name?.startsWith("Wu");
62
+ const isTarget = this.components.has(name);
63
+
64
+ if (hasWrapper || isTarget) {
65
+ targetFound = true;
66
+ return true;
67
+ }
68
+
69
+ return false;
70
+ });
71
+
72
+ return targetFound && !isIgnored;
73
+ }
74
+
75
+ getExplicitKey(path) {
76
+ const parent = path.findParent((p) => p.isJSXElement());
77
+ const attr = parent?.node.openingElement.attributes.find(
78
+ (a) => a.name?.name === "data-i18n-key",
79
+ );
80
+ if (!attr) return null;
81
+ return attr.value.type === "StringLiteral"
82
+ ? attr.value.value
83
+ : attr.value.expression?.value;
84
+ }
85
+ }
86
+
87
+ export default function wickuiI18nPlugin(options = {}) {
88
+ const processor = new TranslationProcessor(options);
89
+ const filter = createFilter(options.include || [/\.(jsx|tsx)$/]);
90
+
91
+ return {
92
+ name: "wick-ui-i18n",
93
+ enforce: "pre",
94
+
95
+ transform(code, id) {
96
+ if (!filter(id) || !code.includes("Wu")) return null;
97
+
98
+ const ast = parse(code, {
99
+ sourceType: "module",
100
+ plugins: ["jsx", "typescript"],
101
+ });
102
+ const ms = new MagicString(code);
103
+ let [needsImport, hasImport] = [false, false];
104
+
105
+ const handleCapture = (path, text, start, end) => {
106
+ const cleanText = text.trim();
107
+ if (!cleanText || !processor.shouldTranslate(path)) return;
108
+
109
+ const key = processor.getExplicitKey(path) || cleanText;
110
+ processor.record(key, cleanText, id);
111
+
112
+ const original = ms.slice(start, end);
113
+ ms.overwrite(
114
+ start,
115
+ end,
116
+ `<WuTranslate __i18nKey=${JSON.stringify(key)}>${original}</WuTranslate>`,
117
+ );
118
+ needsImport = true;
119
+ };
120
+
121
+ traverse(ast, {
122
+ ImportDeclaration(path) {
123
+ if (path.node.source.value.includes("wick-ui-lib")) {
124
+ hasImport = path.node.specifiers.some(
125
+ (s) => s.imported?.name === "WuTranslate",
126
+ );
127
+ }
128
+ },
129
+ JSXText(path) {
130
+ const text = path.node.value;
131
+ const trimmed = text.trim();
132
+ const start = path.node.start + text.indexOf(trimmed);
133
+ handleCapture(path, trimmed, start, start + trimmed.length);
134
+ },
135
+ JSXExpressionContainer(path) {
136
+ const expr = path.node.expression;
137
+ let text = null;
138
+ if (expr.type === "StringLiteral") text = expr.value;
139
+ else if (
140
+ expr.type === "TemplateLiteral" &&
141
+ !expr.expressions.length
142
+ ) {
143
+ text = expr.quasis[0].value.cooked;
144
+ }
145
+ if (text) handleCapture(path, text, path.node.start, path.node.end);
146
+ },
147
+ });
148
+
149
+ if (needsImport && !hasImport) {
150
+ ms.prepend(
151
+ `import { WuTranslate } from '@npm-questionpro/wick-ui-lib';\n`,
152
+ );
153
+ }
154
+
155
+ return needsImport
156
+ ? { code: ms.toString(), map: ms.generateMap({ hires: true }) }
157
+ : null;
158
+ },
159
+
160
+ generateBundle() {
161
+ this.emitFile({
162
+ type: "asset",
163
+ fileName: "wick_extracted_strings.json",
164
+ source: JSON.stringify(
165
+ Object.fromEntries(processor.dictionary),
166
+ null,
167
+ 2,
168
+ ),
169
+ });
170
+ },
171
+ };
172
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@npm-questionpro/wick-ui-i18n",
3
+ "version": "0.1.0",
4
+ "license": "ISC",
5
+ "description": "Auto-translation AST wrapper for Wick UI",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "types": "index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "default": "./index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "test": "vitest run"
17
+ },
18
+ "peerDependencies": {
19
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/babel__traverse": "^7.28.0",
23
+ "vite": "^7.3.1",
24
+ "vitest": "^4.0.18"
25
+ },
26
+ "dependencies": {
27
+ "@babel/parser": "^7.29.0",
28
+ "@babel/traverse": "^7.29.0",
29
+ "magic-string": "^0.30.21"
30
+ }
31
+ }
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import wickuiI18nPlugin from "./index.js";
3
+
4
+ // Helper to simulate Vite running the transform hook
5
+ function transform(code, options = {}) {
6
+ const plugin = wickuiI18nPlugin(options);
7
+ const result = plugin.transform(code, "TestFile.jsx");
8
+ return result ? result.code : code;
9
+ }
10
+
11
+ describe("Wick UI i18n Vite Plugin", () => {
12
+ it("1. Translates basic Wu* components and injects import", () => {
13
+ const code = `<WuButton>Submit</WuButton>`;
14
+ const result = transform(code);
15
+ expect(result).toContain(`import { WuTranslate }`);
16
+ expect(result).toContain(
17
+ `<WuTranslate __i18nKey="Submit">Submit</WuTranslate>`,
18
+ );
19
+ });
20
+
21
+ it("2. Ignores components in the ignoreComponents list", () => {
22
+ const code = `<WuIcon>star</WuIcon>`;
23
+ const result = transform(code);
24
+ expect(result).toBe(code);
25
+ });
26
+
27
+ it("3. Handles smart nested ignored components (Ignored inside Target)", () => {
28
+ const code = `<WuButton>Click <WuIcon>star</WuIcon></WuButton>`;
29
+ const result = transform(code);
30
+ expect(result).toContain(
31
+ `<WuTranslate __i18nKey="Click">Click</WuTranslate>`,
32
+ );
33
+ expect(result).toContain(`<WuIcon>star</WuIcon>`);
34
+ });
35
+
36
+ it("4. Handles smart nested target components (Target inside regular HTML)", () => {
37
+ const code = `<WuProvider><div><WuButton>Save</WuButton></div></WuProvider>`;
38
+ const result = transform(code);
39
+ expect(result).toContain(
40
+ `<WuTranslate __i18nKey="Save">Save</WuTranslate>`,
41
+ );
42
+ });
43
+
44
+ it("5. Respects data-skip and data-i18n-skip flags", () => {
45
+ const code1 = `<WuButton data-skip>Ignored</WuButton>`;
46
+ const code2 = `<WuButton data-i18n-skip>Ignored</WuButton>`;
47
+ expect(transform(code1)).toBe(code1);
48
+ expect(transform(code2)).toBe(code2);
49
+ });
50
+
51
+ it("6. Uses explicit keys when data-i18n-key is provided", () => {
52
+ const code = `<WuButton data-i18n-key="btn_login">Log In</WuButton>`;
53
+ const result = transform(code);
54
+ expect(result).toContain(
55
+ `<WuTranslate __i18nKey="btn_login">Log In</WuTranslate>`,
56
+ );
57
+ });
58
+
59
+ it("7. Translates JSX Expression Containers (Strings in braces)", () => {
60
+ const code = `<WuButton>{"Hello World"}</WuButton>`;
61
+ const result = transform(code);
62
+ expect(result).toContain(
63
+ `<WuTranslate __i18nKey="Hello World">{"Hello World"}</WuTranslate>`,
64
+ );
65
+ });
66
+
67
+ // it("8. Supports custom components via options", () => {
68
+ // const code = `<CustomCard>Welcome</CustomCard>`;
69
+ // const result = transform(code, { components: ["CustomCard"] });
70
+ // expect(result).toContain(
71
+ // `<WuTranslate __i18nKey="Welcome">Welcome</WuTranslate>`,
72
+ // );
73
+ // });
74
+ //
75
+ it("9. Supports data-i18n-wrapper on non-target tags", () => {
76
+ const code = `<span data-i18n-wrapper>Wrapped Text</span>`;
77
+ const triggerCode = `import { WuPlaceholder } from 'lib';\n` + code;
78
+ const result = transform(triggerCode);
79
+ expect(result).toContain(
80
+ `<WuTranslate __i18nKey="Wrapped Text">Wrapped Text</WuTranslate>`,
81
+ );
82
+ });
83
+ });