@npm-questionpro/wick-ui-i18n 0.9.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 +26 -3
- package/package.json +1 -1
- package/src/processor.js +36 -4
- package/src/transform.js +153 -45
- package/src/transformJSXTextWithEntities.js +127 -0
- package/src/transformTemplateLiteral.js +90 -0
- package/src/transformWtCalls.js +136 -0
- package/wickuii18n.test.js +699 -13
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,15 +43,29 @@ 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);
|
|
52
|
+
|
|
53
|
+
let base = "/";
|
|
48
54
|
|
|
49
55
|
return {
|
|
50
56
|
name: "wick-ui-i18n",
|
|
51
57
|
enforce: "pre",
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Capture the resolved base so the dev-server middleware path matches
|
|
61
|
+
* what `import.meta.env.BASE_URL` resolves to in the consumer app.
|
|
62
|
+
*
|
|
63
|
+
* @param {import('vite').ResolvedConfig} resolvedConfig
|
|
64
|
+
*/
|
|
65
|
+
configResolved(resolvedConfig) {
|
|
66
|
+
base = resolvedConfig.base;
|
|
67
|
+
},
|
|
68
|
+
|
|
53
69
|
/**
|
|
54
70
|
* Clear state on every build so stale keys don't accumulate across
|
|
55
71
|
* watch-mode rebuilds.
|
|
@@ -67,7 +83,14 @@ export default function wickuiI18nPlugin(options = {}) {
|
|
|
67
83
|
* @param {string} id
|
|
68
84
|
*/
|
|
69
85
|
transform(code, id) {
|
|
70
|
-
|
|
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;
|
|
71
94
|
return transformFile(code, id, processor);
|
|
72
95
|
},
|
|
73
96
|
|
|
@@ -77,7 +100,7 @@ export default function wickuiI18nPlugin(options = {}) {
|
|
|
77
100
|
* @param {import('vite').ViteDevServer} server
|
|
78
101
|
*/
|
|
79
102
|
configureServer(server) {
|
|
80
|
-
server.middlewares.use(
|
|
103
|
+
server.middlewares.use(`${base}wick-ui-i18n.json`, (_req, res) => {
|
|
81
104
|
res.setHeader("Content-Type", "application/json");
|
|
82
105
|
res.end(
|
|
83
106
|
JSON.stringify(Object.fromEntries(processor.dictionary), null, 2),
|
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,18 +1,34 @@
|
|
|
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";
|
|
7
9
|
import { parse } from "@babel/parser";
|
|
8
10
|
import _traverse from "@babel/traverse";
|
|
9
11
|
import { getComponentTree } from "./debug.js";
|
|
12
|
+
import { transformTemplateLiteralExpression } from "./transformTemplateLiteral.js";
|
|
13
|
+
import { transformJSXTextWithEntities } from "./transformJSXTextWithEntities.js";
|
|
14
|
+
import {
|
|
15
|
+
recordWtCall,
|
|
16
|
+
transformWtTemplateLiteral,
|
|
17
|
+
} from "./transformWtCalls.js";
|
|
10
18
|
|
|
11
19
|
const traverse = _traverse.default || _traverse;
|
|
12
20
|
|
|
13
21
|
/** Babel parser plugins applied to every file. */
|
|
14
22
|
const BABEL_PLUGINS = ["jsx", "typescript"];
|
|
15
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Matches any HTML entity: named (&), decimal (©), or hex (©).
|
|
26
|
+
* Used to skip text segments that contain entities — they must be left as-is.
|
|
27
|
+
* Note: for JSXText this must be tested against the RAW source, not the Babel
|
|
28
|
+
* decoded .value (Babel turns & → "&", → "\u00a0", etc.).
|
|
29
|
+
*/
|
|
30
|
+
const HTML_ENTITY_RE = /&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);/;
|
|
31
|
+
|
|
16
32
|
/** @param {import('@babel/types').Node} node @returns {string|null} */
|
|
17
33
|
function getStaticString(node) {
|
|
18
34
|
if (node.type === "StringLiteral") return node.value;
|
|
@@ -21,6 +37,34 @@ function getStaticString(node) {
|
|
|
21
37
|
return null;
|
|
22
38
|
}
|
|
23
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
|
+
|
|
24
68
|
/**
|
|
25
69
|
* Replace a translatable text node with a `<WuTranslate>` element and record
|
|
26
70
|
* the key in the processor's dictionary.
|
|
@@ -34,18 +78,29 @@ function getStaticString(node) {
|
|
|
34
78
|
* @param {string} id - File path (for collision warnings).
|
|
35
79
|
* @returns {boolean} `true` when replacement was made.
|
|
36
80
|
*/
|
|
37
|
-
function handleCapture(
|
|
38
|
-
|
|
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, " ");
|
|
39
95
|
if (!cleanText || !processor.shouldTranslate(path)) return false;
|
|
96
|
+
// For StringLiteral / TemplateLiteral quasis / ternary branches: entities are
|
|
97
|
+
// not decoded by the JS parser, so cleanText still contains "&" etc.
|
|
98
|
+
if (HTML_ENTITY_RE.test(cleanText)) return false;
|
|
40
99
|
|
|
41
100
|
const key = (!skipExplicitKey && processor.getExplicitKey(path)) || cleanText;
|
|
42
101
|
processor.record(key, cleanText, id, getComponentTree(path));
|
|
43
102
|
|
|
44
|
-
ms.overwrite(
|
|
45
|
-
start,
|
|
46
|
-
end,
|
|
47
|
-
`<WuTranslate __i18nKey=${JSON.stringify(key)}></WuTranslate>`,
|
|
48
|
-
);
|
|
103
|
+
ms.overwrite(start, end, `<WuTranslate __i18nKey=${JSON.stringify(key)}></WuTranslate>`);
|
|
49
104
|
return true;
|
|
50
105
|
}
|
|
51
106
|
|
|
@@ -68,24 +123,92 @@ export function transformFile(code, id, processor) {
|
|
|
68
123
|
const ms = new MagicString(code);
|
|
69
124
|
let needsImport = false;
|
|
70
125
|
let hasImport = false;
|
|
126
|
+
let hasWtTransform = false;
|
|
127
|
+
let needsWtImport = false;
|
|
128
|
+
let hasWtImport = false;
|
|
71
129
|
|
|
72
130
|
traverse(ast, {
|
|
73
|
-
/**
|
|
74
|
-
*
|
|
75
|
-
* don't duplicate the import statement.
|
|
131
|
+
/** wt("static string") call — record the key in the dictionary.
|
|
132
|
+
* No code transformation; wt() handles runtime lookup.
|
|
76
133
|
*/
|
|
134
|
+
CallExpression(path) {
|
|
135
|
+
if (recordWtCall(path, processor, id)) return;
|
|
136
|
+
if (transformWtTemplateLiteral(path, code, ms, processor, id)) {
|
|
137
|
+
hasWtTransform = true;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/** Track whether WuTranslate / wt are already imported so we don't duplicate them. */
|
|
77
142
|
ImportDeclaration(path) {
|
|
78
143
|
if (path.node.source.value.includes("wick-ui-lib")) {
|
|
79
|
-
|
|
80
|
-
(s
|
|
81
|
-
|
|
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
|
+
}
|
|
82
148
|
}
|
|
83
149
|
},
|
|
84
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
|
+
|
|
85
198
|
/** Plain JSX text: `<Foo>Hello world</Foo>` */
|
|
86
199
|
JSXText(path) {
|
|
87
200
|
const text = path.node.value;
|
|
88
201
|
const trimmed = text.trim();
|
|
202
|
+
// Babel decodes entities in JSXText.value (& → "&", © → "©").
|
|
203
|
+
// Check the raw source slice — if entities found, split around them so
|
|
204
|
+
// translatable text segments are still wrapped while entities stay put.
|
|
205
|
+
const rawSource = code.slice(path.node.start, path.node.end);
|
|
206
|
+
if (HTML_ENTITY_RE.test(rawSource)) {
|
|
207
|
+
if (transformJSXTextWithEntities(path, rawSource, ms, processor, id)) {
|
|
208
|
+
needsImport = true;
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
89
212
|
const start = path.node.start + text.indexOf(trimmed);
|
|
90
213
|
if (
|
|
91
214
|
handleCapture(
|
|
@@ -117,37 +240,18 @@ export function transformFile(code, id, processor) {
|
|
|
117
240
|
|
|
118
241
|
if (expr.type === "StringLiteral") {
|
|
119
242
|
text = expr.value;
|
|
243
|
+
} else if (
|
|
244
|
+
expr.type === "TemplateLiteral" &&
|
|
245
|
+
expr.expressions.length > 0
|
|
246
|
+
) {
|
|
247
|
+
if (transformTemplateLiteralExpression(path, code, ms, processor, id)) {
|
|
248
|
+
needsImport = true;
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
120
251
|
} else if (expr.type === "TemplateLiteral" && !expr.expressions.length) {
|
|
121
252
|
text = expr.quasis[0].value.cooked;
|
|
122
253
|
} else if (expr.type === "ConditionalExpression") {
|
|
123
|
-
|
|
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;
|
|
254
|
+
if (handleConditional(expr, path, ms, processor, id)) needsImport = true;
|
|
151
255
|
return;
|
|
152
256
|
}
|
|
153
257
|
|
|
@@ -168,9 +272,13 @@ export function transformFile(code, id, processor) {
|
|
|
168
272
|
},
|
|
169
273
|
});
|
|
170
274
|
|
|
171
|
-
if (!needsImport) 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
|
+
}
|
|
172
280
|
|
|
173
|
-
if (!hasImport) {
|
|
281
|
+
if (needsImport && !hasImport) {
|
|
174
282
|
ms.prepend(`import { WuTranslate } from '@npm-questionpro/wick-ui-lib';\n`);
|
|
175
283
|
}
|
|
176
284
|
|