@npm-questionpro/wick-ui-i18n 0.10.0 → 0.14.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 +96 -70
- package/index.d.ts +26 -17
- package/index.js +13 -2
- package/package.json +1 -1
- package/src/processor.js +36 -4
- package/src/transform.js +114 -52
- package/wickuii18n.test.js +282 -4
package/README.md
CHANGED
|
@@ -1,78 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wick-ui-i18n
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vite plugin — wraps JSX text in Wu components with `<WuTranslate>`, rewrites
|
|
4
|
+
translatable props to `{wt("...")}`, and emits `wick-ui-i18n.json`.
|
|
4
5
|
|
|
5
6
|
---
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
8
|
+
## JSX text
|
|
9
|
+
|
|
10
|
+
| Input | Output |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `<WuButton>Hello</WuButton>` | ✅ `<WuTranslate __i18nKey="Hello" />` |
|
|
13
|
+
| `<WuIcon>star</WuIcon>` | ❌ |
|
|
14
|
+
| `<div>Hello</div>` | ❌ |
|
|
15
|
+
| `<WuButton data-skip>Hello</WuButton>` | ❌ |
|
|
16
|
+
| `<span data-i18n-wrapper>Hello</span>` | ✅ `<WuTranslate __i18nKey="Hello" />` |
|
|
17
|
+
| `<WuButton data-i18n-key="k">Hello</WuButton>` | ✅ `<WuTranslate __i18nKey="k" />` |
|
|
18
|
+
| `<WuButton>&</WuButton>` | ❌ |
|
|
19
|
+
| `<WuButton>Hello & World</WuButton>` | ✅ `<WuTranslate __i18nKey="Hello" /> & <WuTranslate __i18nKey="World" />` |
|
|
20
|
+
|
|
21
|
+
## JSX string expressions
|
|
22
|
+
|
|
23
|
+
| Input | Output |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `<WuButton>{"Hello"}</WuButton>` | ✅ `<WuTranslate __i18nKey="Hello" />` |
|
|
26
|
+
| `` <WuButton>{`Hello`}</WuButton> `` | ✅ `<WuTranslate __i18nKey="Hello" />` |
|
|
27
|
+
| `<WuButton>{variable}</WuButton>` | ❌ |
|
|
28
|
+
|
|
29
|
+
## JSX ternaries
|
|
30
|
+
|
|
31
|
+
| Input | Output |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `<WuButton>{flag ? "Yes" : "No"}</WuButton>` | ✅ `{flag ? <WuTranslate __i18nKey="Yes" /> : <WuTranslate __i18nKey="No" />}` |
|
|
34
|
+
| `<WuButton>{flag ? "Yes" : variable}</WuButton>` | ✅ `{flag ? <WuTranslate __i18nKey="Yes" /> : variable}` |
|
|
35
|
+
| `<WuButton>{flag ? variable : variable}</WuButton>` | ❌ |
|
|
36
|
+
| `<WuButton>{a ? "A" : b ? "B" : "C"}</WuButton>` | ✅ `{a ? <WuTranslate __i18nKey="A" /> : b ? <WuTranslate __i18nKey="B" /> : <WuTranslate __i18nKey="C" />}` |
|
|
37
|
+
|
|
38
|
+
## JSX template literals with expressions
|
|
39
|
+
|
|
40
|
+
| Input | Output |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `` <WuButton>{`Hello ${name}`}</WuButton> `` | ✅ `<><WuTranslate __i18nKey="Hello" /> {name}</>` |
|
|
43
|
+
| `` <WuButton>{`${name} world`}</WuButton> `` | ✅ `<>{name} <WuTranslate __i18nKey="world" /></>` |
|
|
44
|
+
| `` <WuButton>{`Hello ${a} and ${b}`}</WuButton> `` | ✅ `<><WuTranslate __i18nKey="Hello" /> {a} <WuTranslate __i18nKey="and" /> {b}</>` |
|
|
45
|
+
| `` <WuButton>{`${a}${b}`}</WuButton> `` | ❌ |
|
|
46
|
+
|
|
47
|
+
## JSX mixed children
|
|
48
|
+
|
|
49
|
+
| Input | Output |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `<WuButton>Hello {name}</WuButton>` | ✅ `<WuTranslate __i18nKey="Hello" /> {name}` |
|
|
52
|
+
| `<WuButton>{a} and {b}</WuButton>` | ✅ `{a} <WuTranslate __i18nKey="and" /> {b}` |
|
|
53
|
+
| `<WuButton>{a} {b}</WuButton>` | ❌ |
|
|
54
|
+
|
|
55
|
+
## JSX props
|
|
56
|
+
|
|
57
|
+
Defaults: `Label`, `placeholder`, `title`, `aria-label`, `aria-placeholder`.
|
|
58
|
+
|
|
59
|
+
| Input | Output |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `<WuField Label="First name" />` | ✅ `Label={wt("First name")}` |
|
|
62
|
+
| `<WuInput placeholder="Enter name" />` | ✅ `placeholder={wt("Enter name")}` |
|
|
63
|
+
| `<WuDialog title="Confirm?" />` | ✅ `title={wt("Confirm?")}` |
|
|
64
|
+
| `<WuField Label={variable} />` | ❌ |
|
|
65
|
+
| `<WuField Label="" />` | ❌ |
|
|
66
|
+
| `<WuIcon Label="x" />` | ❌ |
|
|
67
|
+
| `<input placeholder="x" />` | ❌ |
|
|
68
|
+
| `<WuField data-skip Label="x" />` | ❌ |
|
|
69
|
+
|
|
70
|
+
## `wt()` calls
|
|
71
|
+
|
|
72
|
+
Plugin records static args into `wick-ui-i18n.json`. No code rewrite unless template literal with expressions.
|
|
73
|
+
|
|
74
|
+
| Input | Dictionary | Code output |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `wt("Hello")` | ✅ `"Hello"` | `wt("Hello")` |
|
|
77
|
+
| `` wt(`Hello`) `` | ✅ `"Hello"` | `` wt(`Hello`) `` |
|
|
78
|
+
| `` wt(`Hello ${name}`) `` | ✅ `"Hello"` | `` `${wt("Hello")} ${name}` `` |
|
|
79
|
+
| `` wt(`Hello ${a} and ${b}`) `` | ✅ `"Hello"`, `"and"` | `` `${wt("Hello")} ${a} ${wt("and")} ${b}` `` |
|
|
80
|
+
| `wt(variable)` | ❌ | — |
|
|
81
|
+
| `` wt(`${a}${b}`) `` | ❌ | — |
|
|
82
|
+
|
|
83
|
+
## Data files (`extractFromKeys` option)
|
|
84
|
+
|
|
85
|
+
No code rewrite — keys only recorded in `wick-ui-i18n.json`.
|
|
86
|
+
|
|
87
|
+
| Input | Dictionary |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `{ label: 'Analytics' }` + `extractFromKeys: ['label']` | ✅ `"Analytics"` |
|
|
90
|
+
| `{ label: variable }` | ❌ |
|
|
91
|
+
| `{ label: '' }` | ❌ |
|
|
32
92
|
|
|
33
93
|
---
|
|
34
94
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```text
|
|
38
|
-
FOR each File in the Project:
|
|
39
|
-
IF File matches IncludeFilter AND contains "Wu":
|
|
40
|
-
PARSE File into an AST (Abstract Syntax Tree)
|
|
41
|
-
INITIALIZE MagicString (for non-destructive editing)
|
|
42
|
-
|
|
43
|
-
TRAVERSE the AST:
|
|
44
|
-
|
|
45
|
-
// 1. IMPORT CHECK
|
|
46
|
-
IF Node is an Import from '@wick-ui-lib':
|
|
47
|
-
MARK 'WuTranslate' as already imported if found
|
|
48
|
-
|
|
49
|
-
// 2. STRING DISCOVERY
|
|
50
|
-
IF Node is JSXText OR (JSXExpressionContainer WITH StaticString):
|
|
51
|
-
SET CandidateText = Node.Value
|
|
52
|
-
|
|
53
|
-
// 3. HIERARCHY EVALUATION (The "shouldTranslate" logic)
|
|
54
|
-
WALK UP from Node to Parents:
|
|
55
|
-
IF Parent has [data-skip] attribute:
|
|
56
|
-
ABORT (Don't translate this node)
|
|
57
|
-
|
|
58
|
-
IF Parent has [data-i18n-wrapper] attribute:
|
|
59
|
-
MARK as "Valid Target" and STOP walking up
|
|
60
|
-
|
|
61
|
-
IF Parent.Name starts with "Wu" OR is in CustomList:
|
|
62
|
-
IF Parent.Name is NOT in IgnoreList:
|
|
63
|
-
MARK as "Valid Target" and STOP walking up
|
|
64
|
-
|
|
65
|
-
// 4. TRANSFORMATION
|
|
66
|
-
IF "Valid Target" was found:
|
|
67
|
-
GET ExplicitKey from [data-i18n-key] OR USE CandidateText
|
|
68
|
-
STORE { Key, CandidateText } in GlobalDictionary
|
|
69
|
-
OVERWRITE original code with:
|
|
70
|
-
`<WuTranslate __i18nKey="Key">OriginalText</WuTranslate>`
|
|
71
|
-
SET NeedsImport = True
|
|
72
|
-
|
|
73
|
-
// 5. FINAL ASSEMBLY
|
|
74
|
-
IF NeedsImport AND NOT 'WuTranslate' Imported:
|
|
75
|
-
PREPEND import statement to top of file
|
|
95
|
+
## Options
|
|
76
96
|
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
| Option | Default | Description |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `components` | `[]` | Extra components treated like Wu* |
|
|
100
|
+
| `ignoreComponents` | `[]` | Extra components never translated |
|
|
101
|
+
| `translatableProps` | `['Label','placeholder','title','aria-label','aria-placeholder']` | Props rewritten to `wt()` |
|
|
102
|
+
| `extractFromKeys` | `[]` | Object keys extracted into dictionary |
|
|
103
|
+
| `excludeFiles` | — | Files skipped entirely |
|
|
104
|
+
| `debug` | `false` | Log transforms to console |
|
package/index.d.ts
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
import type {Plugin} from 'vite'
|
|
2
|
+
|
|
3
|
+
export interface WickI18nOptions {
|
|
4
|
+
/** Extra component names to translate (in addition to Wu* components). */
|
|
5
|
+
components?: string[]
|
|
6
|
+
|
|
7
|
+
/** Component names to exclude from translation. */
|
|
8
|
+
ignoreComponents?: string[]
|
|
9
|
+
|
|
2
10
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
11
|
+
* JSX prop names rewritten to `{wt("...")}` on Wu* components.
|
|
12
|
+
* @default ['Label', 'placeholder', 'title', 'aria-label', 'aria-placeholder']
|
|
5
13
|
*/
|
|
6
|
-
|
|
14
|
+
translatableProps?: string[]
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
|
-
*
|
|
17
|
+
* Object property names whose string values are extracted into
|
|
18
|
+
* `wick-ui-i18n.json` without rewriting code. Use for data files
|
|
19
|
+
* (nav items, option lists, etc.) paired with `wt(item.label)` at render time.
|
|
10
20
|
*/
|
|
11
|
-
|
|
21
|
+
extractFromKeys?: string[]
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// * Can be string, RegExp, or array of string/RegExp.
|
|
16
|
-
// */
|
|
17
|
-
// include?: any;
|
|
23
|
+
/** Files to skip entirely. Passed to Vite's `createFilter` as `exclude`. */
|
|
24
|
+
excludeFiles?: string | RegExp | Array<string | RegExp>
|
|
18
25
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
debug?: boolean;
|
|
26
|
+
/** Log every transform to the console. */
|
|
27
|
+
debug?: boolean
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
|
-
* Vite plugin that
|
|
31
|
+
* Vite plugin that automatically translates Wick UI components.
|
|
32
|
+
*
|
|
33
|
+
* - JSX text content inside Wu* → `<WuTranslate __i18nKey="..." />`
|
|
34
|
+
* - Translatable props (placeholder, title, Label, …) → `{wt("...")}`
|
|
35
|
+
* - Emits `wick-ui-i18n.json` with all extracted keys
|
|
27
36
|
*/
|
|
28
|
-
export default function
|
|
37
|
+
export default function wickuiI18nPlugin(options?: WickI18nOptions): Plugin
|
package/index.js
CHANGED
|
@@ -27,6 +27,8 @@ import { printReport } from "./src/debug.js";
|
|
|
27
27
|
* @typedef {object} WickI18nOptions
|
|
28
28
|
* @property {string[]} [components] - Extra component names that trigger translation.
|
|
29
29
|
* @property {string[]} [ignoreComponents] - Component names to exclude from translation.
|
|
30
|
+
* @property {string[]} [translatableProps] - JSX prop names rewritten to `{wt("...")}`. Defaults to `['Label','placeholder','title','aria-label','aria-placeholder']`.
|
|
31
|
+
* @property {string[]} [extractFromKeys] - Object property names whose string values are extracted as translation keys (e.g. ['label']).
|
|
30
32
|
* @property {string|string[]|RegExp} [excludeFiles] - Files to skip (passed as `exclude` to Vite's createFilter).
|
|
31
33
|
* @property {boolean} [debug] - Log transform activity to the console.
|
|
32
34
|
*/
|
|
@@ -41,10 +43,12 @@ export default function wickuiI18nPlugin(options = {}) {
|
|
|
41
43
|
const processor = new TranslationProcessor({
|
|
42
44
|
components: options.components || [],
|
|
43
45
|
ignoreComponents: options.ignoreComponents,
|
|
46
|
+
translatableProps: options.translatableProps,
|
|
47
|
+
extractFromKeys: options.extractFromKeys,
|
|
44
48
|
debug: options.debug,
|
|
45
49
|
});
|
|
46
50
|
|
|
47
|
-
const filter = createFilter([/\.(jsx|tsx)$/], options.excludeFiles);
|
|
51
|
+
const filter = createFilter([/\.(jsx|tsx|ts)$/], options.excludeFiles);
|
|
48
52
|
|
|
49
53
|
let base = "/";
|
|
50
54
|
|
|
@@ -79,7 +83,14 @@ export default function wickuiI18nPlugin(options = {}) {
|
|
|
79
83
|
* @param {string} id
|
|
80
84
|
*/
|
|
81
85
|
transform(code, id) {
|
|
82
|
-
|
|
86
|
+
const hasAnyTarget =
|
|
87
|
+
code.includes("Wu") ||
|
|
88
|
+
/\bwt\(/.test(code) ||
|
|
89
|
+
(processor.components.size > 0 &&
|
|
90
|
+
[...processor.components].some(c => code.includes(c))) ||
|
|
91
|
+
(processor.extractFromKeys.size > 0 &&
|
|
92
|
+
[...processor.extractFromKeys].some(k => code.includes(k)));
|
|
93
|
+
if (!filter(id) || !hasAnyTarget) return null;
|
|
83
94
|
return transformFile(code, id, processor);
|
|
84
95
|
},
|
|
85
96
|
|
package/package.json
CHANGED
package/src/processor.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* decides which JSX nodes should be wrapped with WuTranslate.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/** Prop names translated by default on Wu* components. Pass `translatableProps` to override. */
|
|
7
|
+
const DEFAULT_TRANSLATABLE_PROPS = ['Label', 'placeholder', 'title', 'aria-label', 'aria-placeholder']
|
|
8
|
+
|
|
6
9
|
/** Components always excluded from translation regardless of user config. */
|
|
7
10
|
const DEFAULT_IGNORE = [
|
|
8
11
|
"WuIcon",
|
|
@@ -21,16 +24,24 @@ const DEFAULT_IGNORE = [
|
|
|
21
24
|
|
|
22
25
|
export class TranslationProcessor {
|
|
23
26
|
/**
|
|
24
|
-
* @param {object}
|
|
25
|
-
* @param {string[]} options.components
|
|
26
|
-
* @param {string[]} [options.ignoreComponents]
|
|
27
|
-
* @param {
|
|
27
|
+
* @param {object} options
|
|
28
|
+
* @param {string[]} options.components - Component names that trigger translation.
|
|
29
|
+
* @param {string[]} [options.ignoreComponents] - Extra components to exclude.
|
|
30
|
+
* @param {string[]} [options.translatableProps] - JSX prop names to rewrite to wt(). Overrides defaults.
|
|
31
|
+
* @param {string[]} [options.extractFromKeys] - Object property names whose string values are recorded.
|
|
32
|
+
* @param {boolean} [options.debug] - Enable verbose logging.
|
|
28
33
|
*/
|
|
29
34
|
constructor(options) {
|
|
30
35
|
this.components = new Set(options.components);
|
|
31
36
|
this.ignoreComponents = new Set(
|
|
32
37
|
DEFAULT_IGNORE.concat(options.ignoreComponents || []),
|
|
33
38
|
);
|
|
39
|
+
/** @type {Set<string>} JSX prop names that should be translated. */
|
|
40
|
+
this.translatableProps = new Set(
|
|
41
|
+
options.translatableProps ?? DEFAULT_TRANSLATABLE_PROPS,
|
|
42
|
+
);
|
|
43
|
+
/** @type {Set<string>} Object property key names whose string values are extracted (e.g. 'label'). */
|
|
44
|
+
this.extractFromKeys = new Set(options.extractFromKeys || []);
|
|
34
45
|
/** @type {Map<string, string>} key → original text */
|
|
35
46
|
this.dictionary = new Map();
|
|
36
47
|
/** @type {import('./debug.js').DebugEntry[]} */
|
|
@@ -120,6 +131,27 @@ export class TranslationProcessor {
|
|
|
120
131
|
return targetFound && !isIgnored;
|
|
121
132
|
}
|
|
122
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Return `true` when `propName` is in the translatable-props set and the
|
|
136
|
+
* immediate parent JSX element is a Wu* component or in `components`, and
|
|
137
|
+
* is not in `ignoreComponents`. Matched props are rewritten to `{wt("...")}`. *
|
|
138
|
+
* @param {string} propName
|
|
139
|
+
* @param {import('@babel/traverse').NodePath} path - JSXAttribute path.
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
shouldTranslateProp(propName, path) {
|
|
143
|
+
if (!this.translatableProps.has(propName)) return false;
|
|
144
|
+
// path.parent is the JSXOpeningElement node
|
|
145
|
+
const openingEl = path.parent;
|
|
146
|
+
const name = openingEl.name?.name || openingEl.name?.property?.name;
|
|
147
|
+
if (!name) return false;
|
|
148
|
+
if (this.ignoreComponents.has(name)) return false;
|
|
149
|
+
// Respect data-skip / data-i18n-skip on the same element
|
|
150
|
+
const attrs = openingEl.attributes || [];
|
|
151
|
+
if (attrs.some(a => ['data-skip', 'data-i18n-skip'].includes(a.name?.name))) return false;
|
|
152
|
+
return name.startsWith('Wu') || this.components.has(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
123
155
|
/**
|
|
124
156
|
* Return the explicit i18n key from the nearest ancestor JSX element's
|
|
125
157
|
* `data-i18n-key` attribute, or `null` if absent.
|
package/src/transform.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview AST transform — parses a JSX/TSX file
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview AST transform — parses a JSX/TSX/TS file and:
|
|
3
|
+
* - Rewrites JSX text content inside Wu* components to `<WuTranslate>`
|
|
4
|
+
* - Rewrites translatable props (placeholder, title, …) to `{wt("…")}`
|
|
5
|
+
* - Prepends the necessary imports when added.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import MagicString from "magic-string";
|
|
@@ -9,7 +11,10 @@ import _traverse from "@babel/traverse";
|
|
|
9
11
|
import { getComponentTree } from "./debug.js";
|
|
10
12
|
import { transformTemplateLiteralExpression } from "./transformTemplateLiteral.js";
|
|
11
13
|
import { transformJSXTextWithEntities } from "./transformJSXTextWithEntities.js";
|
|
12
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
recordWtCall,
|
|
16
|
+
transformWtTemplateLiteral,
|
|
17
|
+
} from "./transformWtCalls.js";
|
|
13
18
|
|
|
14
19
|
const traverse = _traverse.default || _traverse;
|
|
15
20
|
|
|
@@ -32,6 +37,34 @@ function getStaticString(node) {
|
|
|
32
37
|
return null;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Recursively handle a ConditionalExpression, wrapping every static string
|
|
42
|
+
* branch (at any nesting depth) with WuTranslate.
|
|
43
|
+
*
|
|
44
|
+
* @param {import('@babel/types').ConditionalExpression} expr
|
|
45
|
+
* @param {import('@babel/traverse').NodePath} path
|
|
46
|
+
* @param {MagicString} ms
|
|
47
|
+
* @param {import('./processor.js').TranslationProcessor} processor
|
|
48
|
+
* @param {string} id
|
|
49
|
+
* @returns {boolean} true when at least one branch was captured
|
|
50
|
+
*/
|
|
51
|
+
function handleConditional(expr, path, ms, processor, id) {
|
|
52
|
+
let changed = false;
|
|
53
|
+
|
|
54
|
+
for (const branch of [expr.consequent, expr.alternate]) {
|
|
55
|
+
const text = getStaticString(branch);
|
|
56
|
+
if (text !== null) {
|
|
57
|
+
changed =
|
|
58
|
+
handleCapture(path, text, branch.start, branch.end, ms, processor, id, true) ||
|
|
59
|
+
changed;
|
|
60
|
+
} else if (branch.type === "ConditionalExpression") {
|
|
61
|
+
changed = handleConditional(branch, path, ms, processor, id) || changed;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return changed;
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
/**
|
|
36
69
|
* Replace a translatable text node with a `<WuTranslate>` element and record
|
|
37
70
|
* the key in the processor's dictionary.
|
|
@@ -45,8 +78,20 @@ function getStaticString(node) {
|
|
|
45
78
|
* @param {string} id - File path (for collision warnings).
|
|
46
79
|
* @returns {boolean} `true` when replacement was made.
|
|
47
80
|
*/
|
|
48
|
-
function handleCapture(
|
|
49
|
-
|
|
81
|
+
function handleCapture(
|
|
82
|
+
path,
|
|
83
|
+
text,
|
|
84
|
+
start,
|
|
85
|
+
end,
|
|
86
|
+
ms,
|
|
87
|
+
processor,
|
|
88
|
+
id,
|
|
89
|
+
skipExplicitKey = false,
|
|
90
|
+
) {
|
|
91
|
+
const cleanText = text
|
|
92
|
+
.trim()
|
|
93
|
+
.replace(/\n/g, " ")
|
|
94
|
+
.replace(/\s{2,}/g, " ");
|
|
50
95
|
if (!cleanText || !processor.shouldTranslate(path)) return false;
|
|
51
96
|
// For StringLiteral / TemplateLiteral quasis / ternary branches: entities are
|
|
52
97
|
// not decoded by the JS parser, so cleanText still contains "&" etc.
|
|
@@ -55,11 +100,7 @@ function handleCapture(path, text, start, end, ms, processor, id, skipExplicitKe
|
|
|
55
100
|
const key = (!skipExplicitKey && processor.getExplicitKey(path)) || cleanText;
|
|
56
101
|
processor.record(key, cleanText, id, getComponentTree(path));
|
|
57
102
|
|
|
58
|
-
ms.overwrite(
|
|
59
|
-
start,
|
|
60
|
-
end,
|
|
61
|
-
`<WuTranslate __i18nKey=${JSON.stringify(key)}></WuTranslate>`,
|
|
62
|
-
);
|
|
103
|
+
ms.overwrite(start, end, `<WuTranslate __i18nKey=${JSON.stringify(key)}></WuTranslate>`);
|
|
63
104
|
return true;
|
|
64
105
|
}
|
|
65
106
|
|
|
@@ -83,14 +124,11 @@ export function transformFile(code, id, processor) {
|
|
|
83
124
|
let needsImport = false;
|
|
84
125
|
let hasImport = false;
|
|
85
126
|
let hasWtTransform = false;
|
|
127
|
+
let needsWtImport = false;
|
|
128
|
+
let hasWtImport = false;
|
|
86
129
|
|
|
87
130
|
traverse(ast, {
|
|
88
|
-
/**
|
|
89
|
-
* Track whether `WuTranslate` is already imported from wick-ui-lib so we
|
|
90
|
-
* don't duplicate the import statement.
|
|
91
|
-
*/
|
|
92
|
-
/**
|
|
93
|
-
* wt("static string") call — record the key in the dictionary.
|
|
131
|
+
/** wt("static string") call — record the key in the dictionary.
|
|
94
132
|
* No code transformation; wt() handles runtime lookup.
|
|
95
133
|
*/
|
|
96
134
|
CallExpression(path) {
|
|
@@ -100,14 +138,63 @@ export function transformFile(code, id, processor) {
|
|
|
100
138
|
}
|
|
101
139
|
},
|
|
102
140
|
|
|
141
|
+
/** Track whether WuTranslate / wt are already imported so we don't duplicate them. */
|
|
103
142
|
ImportDeclaration(path) {
|
|
104
143
|
if (path.node.source.value.includes("wick-ui-lib")) {
|
|
105
|
-
|
|
106
|
-
(s
|
|
107
|
-
|
|
144
|
+
for (const s of path.node.specifiers) {
|
|
145
|
+
if (s.imported?.name === "WuTranslate") hasImport = true;
|
|
146
|
+
if (s.imported?.name === "wt") hasWtImport = true;
|
|
147
|
+
}
|
|
108
148
|
}
|
|
109
149
|
},
|
|
110
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Data-file key extraction: records string values of configured object
|
|
153
|
+
* property names (e.g. `label: 'Analytics'`) without rewriting code.
|
|
154
|
+
* Enabled via `extractFromKeys` option.
|
|
155
|
+
*/
|
|
156
|
+
ObjectProperty(path) {
|
|
157
|
+
if (!processor.extractFromKeys.size) return;
|
|
158
|
+
const keyNode = path.node.key;
|
|
159
|
+
const keyName = keyNode.name ?? keyNode.value;
|
|
160
|
+
if (!keyName || !processor.extractFromKeys.has(keyName)) return;
|
|
161
|
+
const value = path.node.value;
|
|
162
|
+
if (value.type !== "StringLiteral") return;
|
|
163
|
+
const text = value.value
|
|
164
|
+
.trim()
|
|
165
|
+
.replace(/\n/g, " ")
|
|
166
|
+
.replace(/\s{2,}/g, " ");
|
|
167
|
+
if (!text) return;
|
|
168
|
+
if (HTML_ENTITY_RE.test(text)) return;
|
|
169
|
+
processor.record(text, text, id, "(data)");
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Translatable props (Label, placeholder, title, aria-label, ...)
|
|
174
|
+
* rewritten to `{wt("foo")}` on Wu* components.
|
|
175
|
+
*/
|
|
176
|
+
JSXAttribute(path) {
|
|
177
|
+
const propName = path.node.name.name;
|
|
178
|
+
if (typeof propName !== "string") return;
|
|
179
|
+
const value = path.node.value;
|
|
180
|
+
if (!value || value.type !== "StringLiteral") return;
|
|
181
|
+
|
|
182
|
+
const rawValue = code.slice(value.start, value.end);
|
|
183
|
+
if (HTML_ENTITY_RE.test(rawValue)) return;
|
|
184
|
+
|
|
185
|
+
const text = value.value
|
|
186
|
+
.trim()
|
|
187
|
+
.replace(/\n/g, " ")
|
|
188
|
+
.replace(/\s{2,}/g, " ");
|
|
189
|
+
if (!text) return;
|
|
190
|
+
|
|
191
|
+
if (!processor.shouldTranslateProp(propName, path)) return;
|
|
192
|
+
|
|
193
|
+
processor.record(text, text, id, getComponentTree(path));
|
|
194
|
+
ms.overwrite(value.start, value.end, `{wt(${JSON.stringify(text)})}`);
|
|
195
|
+
needsWtImport = true;
|
|
196
|
+
},
|
|
197
|
+
|
|
111
198
|
/** Plain JSX text: `<Foo>Hello world</Foo>` */
|
|
112
199
|
JSXText(path) {
|
|
113
200
|
const text = path.node.value;
|
|
@@ -157,43 +244,14 @@ export function transformFile(code, id, processor) {
|
|
|
157
244
|
expr.type === "TemplateLiteral" &&
|
|
158
245
|
expr.expressions.length > 0
|
|
159
246
|
) {
|
|
160
|
-
if (
|
|
161
|
-
transformTemplateLiteralExpression(path, code, ms, processor, id)
|
|
162
|
-
) {
|
|
247
|
+
if (transformTemplateLiteralExpression(path, code, ms, processor, id)) {
|
|
163
248
|
needsImport = true;
|
|
164
249
|
}
|
|
165
250
|
return;
|
|
166
251
|
} else if (expr.type === "TemplateLiteral" && !expr.expressions.length) {
|
|
167
252
|
text = expr.quasis[0].value.cooked;
|
|
168
253
|
} else if (expr.type === "ConditionalExpression") {
|
|
169
|
-
|
|
170
|
-
const altText = getStaticString(expr.alternate);
|
|
171
|
-
let changed = false;
|
|
172
|
-
if (consText !== null)
|
|
173
|
-
changed =
|
|
174
|
-
handleCapture(
|
|
175
|
-
path,
|
|
176
|
-
consText,
|
|
177
|
-
expr.consequent.start,
|
|
178
|
-
expr.consequent.end,
|
|
179
|
-
ms,
|
|
180
|
-
processor,
|
|
181
|
-
id,
|
|
182
|
-
true,
|
|
183
|
-
) || changed;
|
|
184
|
-
if (altText !== null)
|
|
185
|
-
changed =
|
|
186
|
-
handleCapture(
|
|
187
|
-
path,
|
|
188
|
-
altText,
|
|
189
|
-
expr.alternate.start,
|
|
190
|
-
expr.alternate.end,
|
|
191
|
-
ms,
|
|
192
|
-
processor,
|
|
193
|
-
id,
|
|
194
|
-
true,
|
|
195
|
-
) || changed;
|
|
196
|
-
if (changed) needsImport = true;
|
|
254
|
+
if (handleConditional(expr, path, ms, processor, id)) needsImport = true;
|
|
197
255
|
return;
|
|
198
256
|
}
|
|
199
257
|
|
|
@@ -214,9 +272,13 @@ export function transformFile(code, id, processor) {
|
|
|
214
272
|
},
|
|
215
273
|
});
|
|
216
274
|
|
|
217
|
-
if (!needsImport && !hasWtTransform) return null;
|
|
275
|
+
if (!needsImport && !hasWtTransform && !needsWtImport) return null;
|
|
276
|
+
|
|
277
|
+
if (needsWtImport && !hasWtImport) {
|
|
278
|
+
ms.prepend(`import { wt } from '@npm-questionpro/wick-ui-lib';\n`);
|
|
279
|
+
}
|
|
218
280
|
|
|
219
|
-
if (!hasImport) {
|
|
281
|
+
if (needsImport && !hasImport) {
|
|
220
282
|
ms.prepend(`import { WuTranslate } from '@npm-questionpro/wick-ui-lib';\n`);
|
|
221
283
|
}
|
|
222
284
|
|
package/wickuii18n.test.js
CHANGED
|
@@ -7,6 +7,12 @@ function transform(code, options = {}) {
|
|
|
7
7
|
return result ? result.code : code;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function transformTs(code, options = {}) {
|
|
11
|
+
const plugin = wickuiI18nPlugin(options);
|
|
12
|
+
const result = plugin.transform(code, "TestFile.ts");
|
|
13
|
+
return result ? result.code : code;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* Run the full plugin lifecycle on `code` and return the emitted dictionary.
|
|
12
18
|
* Needed for wt() tests since those record keys without transforming code.
|
|
@@ -182,13 +188,30 @@ describe("Wick UI i18n Vite Plugin", () => {
|
|
|
182
188
|
expect(result).not.toContain(`WuTranslate`);
|
|
183
189
|
});
|
|
184
190
|
|
|
185
|
-
it("12g. Nested ternary:
|
|
186
|
-
// {a ? "A" : b ? "B" : "C"} — alternate is ConditionalExpression, not a static string
|
|
191
|
+
it("12g. Nested ternary: all static branches translated recursively", () => {
|
|
187
192
|
const code = `<WuButton>{a ? "A" : b ? "B" : "C"}</WuButton>`;
|
|
188
193
|
const result = transform(code);
|
|
189
194
|
expect(result).toContain(`<WuTranslate __i18nKey="A"></WuTranslate>`);
|
|
190
|
-
|
|
191
|
-
expect(result).toContain(
|
|
195
|
+
expect(result).toContain(`<WuTranslate __i18nKey="B"></WuTranslate>`);
|
|
196
|
+
expect(result).toContain(`<WuTranslate __i18nKey="C"></WuTranslate>`);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("12g2. Deeply nested ternary: all levels translated", () => {
|
|
200
|
+
const code = `<WuButton>{a ? "A" : b ? "B" : c ? "C" : "D"}</WuButton>`;
|
|
201
|
+
const result = transform(code);
|
|
202
|
+
expect(result).toContain(`<WuTranslate __i18nKey="A"></WuTranslate>`);
|
|
203
|
+
expect(result).toContain(`<WuTranslate __i18nKey="B"></WuTranslate>`);
|
|
204
|
+
expect(result).toContain(`<WuTranslate __i18nKey="C"></WuTranslate>`);
|
|
205
|
+
expect(result).toContain(`<WuTranslate __i18nKey="D"></WuTranslate>`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("12g3. Nested ternary with dynamic branch: dynamic skipped, statics translated", () => {
|
|
209
|
+
const code = `<WuButton>{a ? "A" : b ? variable : "C"}</WuButton>`;
|
|
210
|
+
const result = transform(code);
|
|
211
|
+
expect(result).toContain(`<WuTranslate __i18nKey="A"></WuTranslate>`);
|
|
212
|
+
expect(result).toContain(`variable`);
|
|
213
|
+
expect(result).not.toContain(`__i18nKey="variable"`);
|
|
214
|
+
expect(result).toContain(`<WuTranslate __i18nKey="C"></WuTranslate>`);
|
|
192
215
|
});
|
|
193
216
|
|
|
194
217
|
it("12h. Ternary outside any Wu/wrapper component is not translated", () => {
|
|
@@ -593,3 +616,258 @@ describe("wt() call expressions", () => {
|
|
|
593
616
|
expect(Object.keys(dict)).toHaveLength(0);
|
|
594
617
|
});
|
|
595
618
|
});
|
|
619
|
+
|
|
620
|
+
// ─── translatableProps ────────────────────────────────────────────────────────
|
|
621
|
+
//
|
|
622
|
+
// The `Label` prop on Wu* components is rewritten to
|
|
623
|
+
// `{<WuTranslate __i18nKey="..." />}` and recorded in the dictionary.
|
|
624
|
+
// Custom props can be passed via `translatableProps` option to override the
|
|
625
|
+
// default. Dynamic props, ignored components, and data-skip are untouched.
|
|
626
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
describe("translatableProps", () => {
|
|
630
|
+
it("P1. prop on Wu* component rewritten to wt() and import injected", () => {
|
|
631
|
+
const code = `<WuField Label="First name" />`;
|
|
632
|
+
const result = transform(code);
|
|
633
|
+
expect(result).toContain(`import { wt }`);
|
|
634
|
+
expect(result).toContain(`Label={wt("First name")}`);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("P2. placeholder and title translated by default", () => {
|
|
638
|
+
const r1 = transform(`<WuInput placeholder="Enter name" />`);
|
|
639
|
+
expect(r1).toContain(`placeholder={wt("Enter name")}`);
|
|
640
|
+
const r2 = transform(`<WuDialog title="Confirm?" />`);
|
|
641
|
+
expect(r2).toContain(`title={wt("Confirm?")}`);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("P3. prop not in translatableProps is left untouched", () => {
|
|
645
|
+
const code = `<WuInput variant="secondary" Label="First name" />`;
|
|
646
|
+
const result = transform(code);
|
|
647
|
+
expect(result).toContain(`variant="secondary"`);
|
|
648
|
+
expect(result).not.toContain(`wt("secondary")`);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("P4. prop on non-Wu component is not transformed", () => {
|
|
652
|
+
const code = `<input placeholder="Enter name" />`;
|
|
653
|
+
const result = transform(code);
|
|
654
|
+
expect(result).toBe(code);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("P5. dynamic (expression) prop value is not transformed", () => {
|
|
658
|
+
const code = `<WuField Label={someProp} />`;
|
|
659
|
+
const result = transform(code);
|
|
660
|
+
expect(result).toBe(code);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("P6. empty string prop is skipped", () => {
|
|
664
|
+
const code = `<WuField Label="" />`;
|
|
665
|
+
const result = transform(code);
|
|
666
|
+
expect(result).toBe(code);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("P7. whitespace-only prop is skipped", () => {
|
|
670
|
+
const code = `<WuField Label=" " />`;
|
|
671
|
+
const result = transform(code);
|
|
672
|
+
expect(result).toBe(code);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("P8. prop on ignored component (WuIcon) is not transformed", () => {
|
|
676
|
+
const code = `<WuIcon title="star icon" />`;
|
|
677
|
+
const result = transform(code);
|
|
678
|
+
expect(result).toBe(code);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("P9. data-skip on element suppresses prop translation", () => {
|
|
682
|
+
const code = `<WuField data-skip Label="First name" />`;
|
|
683
|
+
const result = transform(code);
|
|
684
|
+
expect(result).not.toContain('wt(');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("P10. data-i18n-skip on element suppresses prop translation", () => {
|
|
688
|
+
const code = `<WuField data-i18n-skip Label="First name" />`;
|
|
689
|
+
const result = transform(code);
|
|
690
|
+
expect(result).not.toContain('wt(');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("P11. prop wt() and JSX text WuTranslate coexist — both imports injected", () => {
|
|
694
|
+
const code = `<WuField Label="First name">Submit</WuField>`;
|
|
695
|
+
const result = transform(code);
|
|
696
|
+
expect(result).toContain(`import { wt }`);
|
|
697
|
+
expect(result).toContain(`import { WuTranslate }`);
|
|
698
|
+
expect(result).toContain(`Label={wt("First name")}`);
|
|
699
|
+
expect(result).toContain(`<WuTranslate __i18nKey="Submit">`);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("P12. wt already imported — no duplicate import injected", () => {
|
|
703
|
+
const code = [
|
|
704
|
+
`import { wt } from '@npm-questionpro/wick-ui-lib';`,
|
|
705
|
+
`<WuField Label="First name" />`,
|
|
706
|
+
].join('\n');
|
|
707
|
+
const result = transform(code);
|
|
708
|
+
const matches = (result.match(/import \{ wt \}/g) || []).length;
|
|
709
|
+
expect(matches).toBe(1);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("P13. multiple translatable props on same element are all transformed", () => {
|
|
713
|
+
const code = `<WuInput placeholder="Enter name" title="Name field" />`;
|
|
714
|
+
const result = transform(code);
|
|
715
|
+
expect(result).toContain(`placeholder={wt("Enter name")}`);
|
|
716
|
+
expect(result).toContain(`title={wt("Name field")}`);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("P14. passing translatableProps overrides defaults entirely", () => {
|
|
720
|
+
const code = `<WuField Label="First name" title="Name" />`;
|
|
721
|
+
const result = transform(code, {translatableProps: ['title']});
|
|
722
|
+
expect(result).toContain(`title={wt("Name")}`);
|
|
723
|
+
expect(result).toContain(`Label="First name"`);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("P15. HTML entity in prop value is skipped", () => {
|
|
727
|
+
const code = `<WuField Label="Hello & World" />`;
|
|
728
|
+
const result = transform(code);
|
|
729
|
+
expect(result).not.toContain('wt(');
|
|
730
|
+
expect(result).toContain('&');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("P16. multi-word prop value preserved as-is in the key", () => {
|
|
734
|
+
const code = `<WuField Label="Enter your full name" />`;
|
|
735
|
+
const result = transform(code);
|
|
736
|
+
expect(result).toContain(`wt("Enter your full name")`);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("P17. newlines and extra spaces in prop value are normalised", () => {
|
|
740
|
+
const code = `<WuField Label="Enter\nyour name" />`;
|
|
741
|
+
const result = transform(code);
|
|
742
|
+
expect(result).toContain(`wt("Enter your name")`);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("P18. prop on user-configured custom component is transformed", () => {
|
|
746
|
+
const code = `<MyWidget Label="Enter name" />`;
|
|
747
|
+
const result = transform(code, {components: ['MyWidget']});
|
|
748
|
+
expect(result).toContain(`Label={wt("Enter name")}`);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("P19. prop values are recorded in the translation dictionary", () => {
|
|
752
|
+
const code = `<WuField Label="First name" />`;
|
|
753
|
+
const dict = getDictionary(code);
|
|
754
|
+
expect(dict).toHaveProperty('First name', 'First name');
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// ─── .ts data-file key extraction ───────────────────────────────────────────────────
|
|
759
|
+
//
|
|
760
|
+
// Standalone wt() in .ts data files (e.g. nav config arrays) lets the plugin
|
|
761
|
+
// extract string literals into wick-ui-i18n.json without any JSX transform.
|
|
762
|
+
// The code is returned unchanged (null); wt() handles the lookup at render time.
|
|
763
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
764
|
+
|
|
765
|
+
// ─── extractFromKeys ────────────────────────────────────────────────────────
|
|
766
|
+
//
|
|
767
|
+
// Object property names configured via `extractFromKeys` have their string
|
|
768
|
+
// values recorded automatically — no wt() wrapping required in data files.
|
|
769
|
+
// Code is never rewritten; wt() handles the lookup at render time.
|
|
770
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
771
|
+
|
|
772
|
+
describe('extractFromKeys', () => {
|
|
773
|
+
it('EK1. string values of a configured key are recorded without code rewrite', () => {
|
|
774
|
+
const code = [
|
|
775
|
+
`export const ITEMS = [`,
|
|
776
|
+
` { label: 'Analytics', icon: 'wm-analytics' },`,
|
|
777
|
+
` { label: 'Engagement', icon: 'wm-trending-up' },`,
|
|
778
|
+
`]`,
|
|
779
|
+
].join('\n');
|
|
780
|
+
const dict = getDictionary(code, {extractFromKeys: ['label']});
|
|
781
|
+
expect(dict).toHaveProperty('Analytics', 'Analytics');
|
|
782
|
+
expect(dict).toHaveProperty('Engagement', 'Engagement');
|
|
783
|
+
// non-listed keys are ignored
|
|
784
|
+
expect(dict).not.toHaveProperty('wm-analytics');
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('EK2. transform returns null — data file code is never rewritten', () => {
|
|
788
|
+
const code = `export const ITEMS = [{ label: 'Analytics' }]`;
|
|
789
|
+
const plugin = wickuiI18nPlugin({extractFromKeys: ['label']});
|
|
790
|
+
const result = plugin.transform(code, 'navItems.ts');
|
|
791
|
+
expect(result).toBeNull();
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('EK3. works on .ts, .tsx, and .jsx files', () => {
|
|
795
|
+
const code = `export const x = { label: 'Hello' }`;
|
|
796
|
+
const opts = {extractFromKeys: ['label']};
|
|
797
|
+
for (const ext of ['ts', 'tsx', 'jsx']) {
|
|
798
|
+
const plugin = wickuiI18nPlugin(opts);
|
|
799
|
+
plugin.buildStart();
|
|
800
|
+
plugin.transform(code, `file.${ext}`);
|
|
801
|
+
let dict = {};
|
|
802
|
+
plugin.generateBundle.call({emitFile: ({source}) => { dict = JSON.parse(source); }});
|
|
803
|
+
expect(dict).toHaveProperty('Hello', 'Hello');
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it('EK4. multiple configured keys all extracted', () => {
|
|
808
|
+
const code = `const x = { label: 'Save', title: 'Confirm', key: 'btn-save' }`;
|
|
809
|
+
const dict = getDictionary(code, {extractFromKeys: ['label', 'title']});
|
|
810
|
+
expect(dict).toHaveProperty('Save', 'Save');
|
|
811
|
+
expect(dict).toHaveProperty('Confirm', 'Confirm');
|
|
812
|
+
expect(dict).not.toHaveProperty('btn-save');
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('EK5. dynamic (non-literal) values are ignored', () => {
|
|
816
|
+
const code = `const x = { label: someVar }`;
|
|
817
|
+
const dict = getDictionary(code, {extractFromKeys: ['label']});
|
|
818
|
+
expect(Object.keys(dict)).toHaveLength(0);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('EK6. empty string values are ignored', () => {
|
|
822
|
+
const code = `const x = { label: '' }`;
|
|
823
|
+
const dict = getDictionary(code, {extractFromKeys: ['label']});
|
|
824
|
+
expect(Object.keys(dict)).toHaveLength(0);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('EK7. HTML entity in value is ignored', () => {
|
|
828
|
+
const code = `const x = { label: 'Hello & World' }`;
|
|
829
|
+
const dict = getDictionary(code, {extractFromKeys: ['label']});
|
|
830
|
+
expect(Object.keys(dict)).toHaveLength(0);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('EK8. no extractFromKeys configured — object properties are not touched', () => {
|
|
834
|
+
const code = `const x = { label: 'Analytics' }`;
|
|
835
|
+
const dict = getDictionary(code, {});
|
|
836
|
+
expect(Object.keys(dict)).toHaveLength(0);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('EK9. coexists with JSX text-child translation in the same build', () => {
|
|
840
|
+
const plugin = wickuiI18nPlugin({extractFromKeys: ['label']});
|
|
841
|
+
plugin.buildStart();
|
|
842
|
+
plugin.transform(`export const ITEMS = [{ label: 'Analytics' }]`, 'navItems.ts');
|
|
843
|
+
plugin.transform(`<WuButton>Submit</WuButton>`, 'Form.tsx');
|
|
844
|
+
let dict = {};
|
|
845
|
+
plugin.generateBundle.call({emitFile: ({source}) => { dict = JSON.parse(source); }});
|
|
846
|
+
expect(dict).toHaveProperty('Analytics', 'Analytics');
|
|
847
|
+
expect(dict).toHaveProperty('Submit', 'Submit');
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('EK10. full SECONDARY_NAVBAR_ITEMS shape — all 8 labels extracted', () => {
|
|
851
|
+
const code = `
|
|
852
|
+
export const SECONDARY_NAVBAR_ITEMS = [
|
|
853
|
+
{ key: 'account-operations', label: 'Analytics', icon: 'wm-analytics' },
|
|
854
|
+
{ key: 'engagement', label: 'Engagement', icon: 'wm-trending-up' },
|
|
855
|
+
{ key: 'system-notifications', label: 'System Notifications', icon: 'wm-notifications' },
|
|
856
|
+
{ key: 'search', label: 'Search', icon: 'wm-search' },
|
|
857
|
+
{ key: 'templates', label: 'Templates', icon: 'wc-static-content' },
|
|
858
|
+
{ key: 'translations', label: 'Translations', icon: 'wm-translate' },
|
|
859
|
+
{ key: 'ai', label: 'AI', icon: 'wc-ai' },
|
|
860
|
+
{ key: 'cpq-pricing', label: 'CPQ Pricing', icon: 'wc-pricing-analysis' },
|
|
861
|
+
]`;
|
|
862
|
+
const dict = getDictionary(code, {extractFromKeys: ['label']});
|
|
863
|
+
expect(Object.keys(dict)).toHaveLength(8);
|
|
864
|
+
expect(dict).toHaveProperty('Analytics');
|
|
865
|
+
expect(dict).toHaveProperty('System Notifications');
|
|
866
|
+
expect(dict).toHaveProperty('CPQ Pricing');
|
|
867
|
+
// non-label keys not extracted
|
|
868
|
+
expect(dict).not.toHaveProperty('account-operations');
|
|
869
|
+
expect(dict).not.toHaveProperty('wm-analytics');
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
|