@npm-questionpro/wick-ui-i18n 2.0.0-next.9 → 2.1.0-rc.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 +34 -89
- package/index.d.ts +46 -9
- package/index.js +58 -18
- package/package.json +1 -4
- package/src/debug.js +4 -10
- package/src/extractStrings.js +58 -0
- package/src/processor.js +18 -38
- package/src/recordWtCalls.js +166 -0
- package/src/telemetry.js +106 -0
- package/src/transform.js +132 -110
- package/src/{transformJSXTextWithEntities.js → transformReactTextWithEntities.js} +7 -34
- package/src/transformTemplateLiteral.js +11 -27
- package/src/utils.js +60 -0
- package/src/transformWtCalls.js +0 -136
package/README.md
CHANGED
|
@@ -1,105 +1,50 @@
|
|
|
1
1
|
# wick-ui-i18n
|
|
2
2
|
|
|
3
|
-
Vite plugin — wraps JSX text in Wu components with `<WuTranslate>`, rewrites
|
|
4
|
-
|
|
3
|
+
Vite plugin — wraps JSX text in Wu\* components with `<WuTranslate>`, rewrites translatable props to `{useWt()("...")}`,
|
|
4
|
+
records `wt()` / `useWt()()` calls, and emits `wick-ui-i18n.json`.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
6
|
+
> Full docs and examples: Storybook → **i18n/Overview**
|
|
39
7
|
|
|
40
|
-
|
|
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
|
|
8
|
+
---
|
|
56
9
|
|
|
57
|
-
|
|
10
|
+
## Quick start
|
|
58
11
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
| `<WuField Label={variable} />` | ❌ |
|
|
65
|
-
| `<WuField Label="" />` | ❌ |
|
|
66
|
-
| `<WuIcon Label="x" />` | ❌ |
|
|
67
|
-
| `<input placeholder="x" />` | ❌ |
|
|
68
|
-
| `<WuField data-skip Label="x" />` | ❌ |
|
|
12
|
+
```ts
|
|
13
|
+
// vite.config.ts
|
|
14
|
+
import wickI18n from '@npm-questionpro/wick-ui-i18n'
|
|
15
|
+
export default defineConfig({plugins: [react(), wickI18n()]})
|
|
16
|
+
```
|
|
69
17
|
|
|
70
|
-
|
|
18
|
+
---
|
|
71
19
|
|
|
72
|
-
|
|
73
|
-
template literal with expressions.
|
|
20
|
+
## What it transforms
|
|
74
21
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| `wt(
|
|
82
|
-
|
|
|
22
|
+
| Source | Output |
|
|
23
|
+
| ----------------------------------------- | --------------------------------------------- |
|
|
24
|
+
| `<WuButton>Save</WuButton>` | `<WuTranslate __i18nKey="Save" />` |
|
|
25
|
+
| `<WuInput Label="First name" />` | `Label={useWt()("First name")}` |
|
|
26
|
+
| ``<WuButton>{`${n} results`}</WuButton>`` | `<><WuTranslate __i18nKey="results" />{n}</>` |
|
|
27
|
+
| `wt("Hello")` | recorded in dictionary |
|
|
28
|
+
| ``wt(`Hello ${name}`)`` | `` `${wt("Hello")} ${name}` `` |
|
|
29
|
+
| `useWt()("Hello")` | recorded in dictionary |
|
|
30
|
+
| ``useWt()(`Hello ${name}`)`` | `` `${useWt()("Hello")} ${name}` `` |
|
|
83
31
|
|
|
84
|
-
|
|
32
|
+
---
|
|
85
33
|
|
|
86
|
-
|
|
34
|
+
## Options
|
|
87
35
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
36
|
+
| Option | Default | Description |
|
|
37
|
+
| ------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
38
|
+
| `components` | `[]` | Extra components treated like Wu\* |
|
|
39
|
+
| `ignoreComponents` | `[]` | Extra components never translated |
|
|
40
|
+
| `translatableProps` | `['Label','placeholder','title','aria-label','aria-placeholder']` | Props rewritten to `useWt()()` |
|
|
41
|
+
| `dataLabelKeys` | `[]` | Property names whose values are labels in mixed data files |
|
|
42
|
+
| `dictionaryFiles` | — | Globs (project files via Vite) or exact paths (anywhere, incl. `node_modules` via `fs`) |
|
|
43
|
+
| `excludeFiles` | — | Files skipped entirely |
|
|
44
|
+
| `debug` | `false` | Print extraction table at build |
|
|
93
45
|
|
|
94
46
|
---
|
|
95
47
|
|
|
96
|
-
##
|
|
48
|
+
## Breaking changes
|
|
97
49
|
|
|
98
|
-
|
|
99
|
-
| ------------------- | ----------------------------------------------------------------- | ------------------------------------- |
|
|
100
|
-
| `components` | `[]` | Extra components treated like Wu\* |
|
|
101
|
-
| `ignoreComponents` | `[]` | Extra components never translated |
|
|
102
|
-
| `translatableProps` | `['Label','placeholder','title','aria-label','aria-placeholder']` | Props rewritten to `wt()` |
|
|
103
|
-
| `extractFromKeys` | `[]` | Object keys extracted into dictionary |
|
|
104
|
-
| `excludeFiles` | — | Files skipped entirely |
|
|
105
|
-
| `debug` | `false` | Log transforms to console |
|
|
50
|
+
See [`BREAKING_CHANGES.md`](./BREAKING_CHANGES.md).
|
package/index.d.ts
CHANGED
|
@@ -5,26 +5,61 @@ export interface WickI18nOptions {
|
|
|
5
5
|
/** Extra component names to translate (in addition to Wu* components). */
|
|
6
6
|
components?: string[]
|
|
7
7
|
|
|
8
|
-
/** Component names to exclude from translation. */
|
|
8
|
+
/** Component names to exclude from translation (extends the built-in ignore list). */
|
|
9
9
|
ignoreComponents?: string[]
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* JSX prop names rewritten to `{
|
|
13
|
-
*
|
|
12
|
+
* Extra JSX prop names rewritten to `{useWt()("...")}` on Wu* components.
|
|
13
|
+
* Appended to the defaults — defaults are always kept:
|
|
14
|
+
* `['Label', 'placeholder', 'title', 'aria-label', 'aria-placeholder']`.
|
|
14
15
|
*/
|
|
15
16
|
translatableProps?: string[]
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Object property names whose string values are extracted into
|
|
19
|
-
* `wick-ui-i18n.json` without rewriting code.
|
|
20
|
-
*
|
|
20
|
+
* `wick-ui-i18n.json` without rewriting code.
|
|
21
|
+
*
|
|
22
|
+
* Use for mixed data files (nav items, option lists, table configs) where
|
|
23
|
+
* only specific property names hold display labels — icon names, routes, and
|
|
24
|
+
* IDs with other key names are left alone.
|
|
25
|
+
*
|
|
26
|
+
* At render time, look up values with `wt(item.label)`.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* dataLabelKeys: ['label', 'title']
|
|
30
|
+
* // { key: 'analytics', label: 'Analytics', icon: 'wm-analytics' }
|
|
31
|
+
* // ^^^^^^^^^^^ extracted, icon name ignored
|
|
21
32
|
*/
|
|
22
|
-
|
|
33
|
+
dataLabelKeys?: string[]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Files where **every** string value is a display label — object properties
|
|
37
|
+
* and TypeScript enum members are all extracted into `wick-ui-i18n.json`.
|
|
38
|
+
*
|
|
39
|
+
* Use **only** for dedicated enum/constant files. Do NOT point at mixed data
|
|
40
|
+
* files — use `dataLabelKeys` for those.
|
|
41
|
+
*
|
|
42
|
+
* Two modes depending on the value:
|
|
43
|
+
* - **Glob patterns** (`'src/enums/*.ts'`) — matched via Vite's transform pipeline.
|
|
44
|
+
* Works for files in your project; does **not** reach `node_modules`.
|
|
45
|
+
* - **Exact paths** (`'node_modules/@company/shared/enums.ts'`) — read directly
|
|
46
|
+
* with `fs` at build time, bypassing Vite's transform pipeline. Use this for
|
|
47
|
+
* `node_modules` files. Missing files produce a `console.warn`.
|
|
48
|
+
*
|
|
49
|
+
* Both modes can be mixed in the same array.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* dictionaryFiles: [
|
|
53
|
+
* 'src/enums/*.ts', // glob — project files
|
|
54
|
+
* 'node_modules/@company/shared/enums.ts', // exact — node_modules
|
|
55
|
+
* ]
|
|
56
|
+
*/
|
|
57
|
+
dictionaryFiles?: string | RegExp | Array<string | RegExp>
|
|
23
58
|
|
|
24
59
|
/** Files to skip entirely. Passed to Vite's `createFilter` as `exclude`. */
|
|
25
60
|
excludeFiles?: string | RegExp | Array<string | RegExp>
|
|
26
61
|
|
|
27
|
-
/**
|
|
62
|
+
/** Print a build-time extraction report table to stdout. */
|
|
28
63
|
debug?: boolean
|
|
29
64
|
}
|
|
30
65
|
|
|
@@ -32,7 +67,9 @@ export interface WickI18nOptions {
|
|
|
32
67
|
* Vite plugin that automatically translates Wick UI components.
|
|
33
68
|
*
|
|
34
69
|
* - JSX text content inside Wu* → `<WuTranslate __i18nKey="..." />`
|
|
35
|
-
* - Translatable props (placeholder
|
|
36
|
-
* -
|
|
70
|
+
* - Translatable props (`Label`, `placeholder`, `title`, …) → `{useWt()("...")}`
|
|
71
|
+
* - `wt("literal")` and `useWt()("literal")` calls → keys recorded in dictionary
|
|
72
|
+
* - `wt(\`Hello ${name}\`)` and `useWt()(\`Hello ${name}\`)` → each static quasi extracted and rewritten
|
|
73
|
+
* - Emits `wick-ui-i18n.json` at build time / serves it at `GET /wick-ui-i18n.json` in dev
|
|
37
74
|
*/
|
|
38
75
|
export default function wickuiI18nPlugin(options?: WickI18nOptions): Plugin
|
package/index.js
CHANGED
|
@@ -18,18 +18,27 @@
|
|
|
18
18
|
* };
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
import path from 'node:path'
|
|
21
23
|
import {createFilter} from 'vite'
|
|
22
24
|
import {TranslationProcessor} from './src/processor.js'
|
|
23
25
|
import {transformFile} from './src/transform.js'
|
|
24
26
|
import {printReport} from './src/debug.js'
|
|
27
|
+
import {TelemetryCollector} from './src/telemetry.js'
|
|
28
|
+
import {extractStringsFromFile} from './src/extractStrings.js'
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* @typedef {object} WickI18nOptions
|
|
28
32
|
* @property {string[]} [components] - Extra component names that trigger translation.
|
|
29
33
|
* @property {string[]} [ignoreComponents] - Component names to exclude from translation.
|
|
30
|
-
* @property {string[]} [translatableProps] - JSX prop names rewritten to `{
|
|
31
|
-
* @property {string[]} [
|
|
32
|
-
*
|
|
34
|
+
* @property {string[]} [translatableProps] - JSX prop names rewritten to `{useWt()("...")}`. Defaults to `['Label','placeholder','title','aria-label','aria-placeholder']`.
|
|
35
|
+
* @property {string[]} [dataLabelKeys] - Object property names whose string values are labels in mixed data
|
|
36
|
+
* files (e.g. `['label', 'title']` in nav config arrays). No code rewrite;
|
|
37
|
+
* keys are recorded into `wick-ui-i18n.json` for the translation API.
|
|
38
|
+
* @property {string|string[]|RegExp} [dictionaryFiles] - Files where every string is a display label (enum files,
|
|
39
|
+
* message constants). Pattern: `'**\/enums\/*.ts'`.
|
|
40
|
+
* Do NOT point at mixed data files — use `dataLabelKeys` for those.
|
|
41
|
+
* @property {string|string[]|RegExp} [excludeFiles] - Files to skip (passed as `exclude` to Vite's createFilter).
|
|
33
42
|
* @property {boolean} [debug] - Log transform activity to the console.
|
|
34
43
|
*/
|
|
35
44
|
|
|
@@ -40,17 +49,30 @@ import {printReport} from './src/debug.js'
|
|
|
40
49
|
* @returns {import('vite').Plugin}
|
|
41
50
|
*/
|
|
42
51
|
export default function wickuiI18nPlugin(options = {}) {
|
|
52
|
+
const telemetry = new TelemetryCollector()
|
|
53
|
+
|
|
43
54
|
const processor = new TranslationProcessor({
|
|
44
55
|
components: options.components || [],
|
|
45
56
|
ignoreComponents: options.ignoreComponents,
|
|
46
57
|
translatableProps: options.translatableProps,
|
|
47
|
-
|
|
58
|
+
dataLabelKeys: options.dataLabelKeys,
|
|
48
59
|
debug: options.debug,
|
|
49
60
|
})
|
|
50
61
|
|
|
51
62
|
const filter = createFilter([/\.(jsx|tsx|ts)$/], options.excludeFiles)
|
|
63
|
+
const enumFilter = options.dictionaryFiles ? createFilter(options.dictionaryFiles) : null
|
|
64
|
+
|
|
65
|
+
// Exact-path entries from dictionaryFiles (no glob chars) are read via fs in buildStart
|
|
66
|
+
// so they work for node_modules files that Vite never processes through transforms.
|
|
67
|
+
const dictionaryPatterns = (() => {
|
|
68
|
+
const raw = options.dictionaryFiles
|
|
69
|
+
if (!raw) return []
|
|
70
|
+
const arr = Array.isArray(raw) ? raw : [raw]
|
|
71
|
+
return arr.filter(p => typeof p === 'string' && !/[*?{}[\]]/.test(p))
|
|
72
|
+
})()
|
|
52
73
|
|
|
53
74
|
let base = '/'
|
|
75
|
+
let root = '/'
|
|
54
76
|
|
|
55
77
|
return {
|
|
56
78
|
name: 'wick-ui-i18n',
|
|
@@ -64,6 +86,8 @@ export default function wickuiI18nPlugin(options = {}) {
|
|
|
64
86
|
*/
|
|
65
87
|
configResolved(resolvedConfig) {
|
|
66
88
|
base = resolvedConfig.base
|
|
89
|
+
root = resolvedConfig.root
|
|
90
|
+
telemetry.scanVersions(resolvedConfig.root)
|
|
67
91
|
},
|
|
68
92
|
|
|
69
93
|
/**
|
|
@@ -73,6 +97,17 @@ export default function wickuiI18nPlugin(options = {}) {
|
|
|
73
97
|
buildStart() {
|
|
74
98
|
processor.dictionary.clear()
|
|
75
99
|
processor.entries = []
|
|
100
|
+
telemetry.reset()
|
|
101
|
+
|
|
102
|
+
for (const rel of dictionaryPatterns) {
|
|
103
|
+
const abs = path.isAbsolute(rel) ? rel : path.resolve(root, rel)
|
|
104
|
+
try {
|
|
105
|
+
const code = fs.readFileSync(abs, 'utf8')
|
|
106
|
+
extractStringsFromFile(code, abs, processor)
|
|
107
|
+
} catch {
|
|
108
|
+
console.warn(`[wick-i18n] dictionaryFiles: could not read "${rel}" — file not found or unreadable`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
76
111
|
},
|
|
77
112
|
|
|
78
113
|
/**
|
|
@@ -83,15 +118,16 @@ export default function wickuiI18nPlugin(options = {}) {
|
|
|
83
118
|
* @param {string} id
|
|
84
119
|
*/
|
|
85
120
|
transform(code, id) {
|
|
121
|
+
if (enumFilter?.(id)) extractStringsFromFile(code, id, processor)
|
|
122
|
+
|
|
86
123
|
const hasAnyTarget =
|
|
87
124
|
code.includes('Wu') ||
|
|
88
|
-
/\bwt\(/.test(code) ||
|
|
89
|
-
(processor.components.size > 0 &&
|
|
90
|
-
|
|
91
|
-
(processor.extractFromKeys.size > 0 &&
|
|
92
|
-
[...processor.extractFromKeys].some(k => code.includes(k)))
|
|
125
|
+
/\bwt\(|useWt\(/.test(code) ||
|
|
126
|
+
(processor.components.size > 0 && [...processor.components].some(c => code.includes(c))) ||
|
|
127
|
+
(processor.extractFromKeys.size > 0 && [...processor.extractFromKeys].some(k => code.includes(k)))
|
|
93
128
|
if (!filter(id) || !hasAnyTarget) return null
|
|
94
|
-
|
|
129
|
+
// Pass telemetry into the same traversal — avoids a second parse of the file.
|
|
130
|
+
return transformFile(code, id, processor, {telemetry: code.includes('Wu') ? telemetry : null})
|
|
95
131
|
},
|
|
96
132
|
|
|
97
133
|
/**
|
|
@@ -102,22 +138,26 @@ export default function wickuiI18nPlugin(options = {}) {
|
|
|
102
138
|
configureServer(server) {
|
|
103
139
|
server.middlewares.use(`${base}wick-ui-i18n.json`, (_req, res) => {
|
|
104
140
|
res.setHeader('Content-Type', 'application/json')
|
|
105
|
-
res.end(
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
res.end(JSON.stringify(Object.fromEntries(processor.dictionary), null, 2))
|
|
142
|
+
})
|
|
143
|
+
server.middlewares.use(`${base}telemetry.json`, (_req, res) => {
|
|
144
|
+
res.setHeader('Content-Type', 'application/json')
|
|
145
|
+
res.end(JSON.stringify(telemetry.toJSON()))
|
|
108
146
|
})
|
|
109
147
|
},
|
|
110
148
|
|
|
111
149
|
/** Emit the translation dictionary as a build asset and print debug table. */
|
|
112
150
|
generateBundle() {
|
|
151
|
+
this.emitFile({
|
|
152
|
+
type: 'asset',
|
|
153
|
+
fileName: 'telemetry.json',
|
|
154
|
+
source: JSON.stringify(telemetry.toJSON()),
|
|
155
|
+
})
|
|
156
|
+
|
|
113
157
|
this.emitFile({
|
|
114
158
|
type: 'asset',
|
|
115
159
|
fileName: 'wick-ui-i18n.json',
|
|
116
|
-
source: JSON.stringify(
|
|
117
|
-
Object.fromEntries(processor.dictionary),
|
|
118
|
-
null,
|
|
119
|
-
2,
|
|
120
|
-
),
|
|
160
|
+
source: JSON.stringify(Object.fromEntries(processor.dictionary), null, 2),
|
|
121
161
|
})
|
|
122
162
|
|
|
123
163
|
printReport(processor.entries)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npm-questionpro/wick-ui-i18n",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0-rc.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"description": "Auto-translation AST wrapper for Wick UI",
|
|
@@ -34,13 +34,10 @@
|
|
|
34
34
|
"index.d.ts",
|
|
35
35
|
"src"
|
|
36
36
|
],
|
|
37
|
-
"prettier": "@npm-questionpro/wick-ui-prettier-config",
|
|
38
37
|
"scripts": {
|
|
39
38
|
"test": "vitest run",
|
|
40
39
|
"test:watch": "vitest",
|
|
41
40
|
"test:ci": "vitest run --coverage",
|
|
42
|
-
"format": "prettier --write .",
|
|
43
|
-
"format:ci": "prettier --check .",
|
|
44
41
|
"lint": "eslint . --fix",
|
|
45
42
|
"lint:ci": "eslint --max-warnings 0 ."
|
|
46
43
|
}
|
package/src/debug.js
CHANGED
|
@@ -16,9 +16,7 @@ export function getComponentTree(path) {
|
|
|
16
16
|
const parts = []
|
|
17
17
|
path.findParent(p => {
|
|
18
18
|
if (p.isJSXElement()) {
|
|
19
|
-
const name =
|
|
20
|
-
p.node.openingElement.name.name ||
|
|
21
|
-
p.node.openingElement.name.property?.name
|
|
19
|
+
const name = p.node.openingElement.name.name || p.node.openingElement.name.property?.name
|
|
22
20
|
if (name) parts.unshift(name)
|
|
23
21
|
}
|
|
24
22
|
return false
|
|
@@ -50,19 +48,15 @@ export function printReport(entries) {
|
|
|
50
48
|
|
|
51
49
|
const rows = entries.map(e => [e.text, basename(e.file), e.componentTree])
|
|
52
50
|
|
|
53
|
-
const widths = headers.map((h, i) =>
|
|
54
|
-
Math.max(h.length, ...rows.map(r => r[i].length)),
|
|
55
|
-
)
|
|
51
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => r[i].length)))
|
|
56
52
|
|
|
57
53
|
const pad = (str, w) => str.padEnd(w)
|
|
58
|
-
const sep = (l, m, r, fill) =>
|
|
59
|
-
l + widths.map(w => fill.repeat(w + 2)).join(m) + r
|
|
54
|
+
const sep = (l, m, r, fill) => l + widths.map(w => fill.repeat(w + 2)).join(m) + r
|
|
60
55
|
|
|
61
56
|
const top = sep('┌', '┬', '┐', '─')
|
|
62
57
|
const mid = sep('├', '┼', '┤', '─')
|
|
63
58
|
const bottom = sep('└', '┴', '┘', '─')
|
|
64
|
-
const row = cells =>
|
|
65
|
-
'│' + cells.map((c, i) => ` ${pad(c, widths[i])} `).join('│') + '│'
|
|
59
|
+
const row = cells => '│' + cells.map((c, i) => ` ${pad(c, widths[i])} `).join('│') + '│'
|
|
66
60
|
|
|
67
61
|
const lines = [
|
|
68
62
|
'',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview extractStrings — collects every non-empty StringLiteral value
|
|
3
|
+
* from object properties and TypeScript enum members in a source file.
|
|
4
|
+
*
|
|
5
|
+
* Used for constant/enum files declared via the `dictionaryFiles` plugin option.
|
|
6
|
+
* No code is rewritten; keys are simply added to the translation dictionary.
|
|
7
|
+
*
|
|
8
|
+
* Handles:
|
|
9
|
+
* export const LABEL = { BRAND_NAME: 'QuestionPro', ... }
|
|
10
|
+
* enum Status { Active = 'Active', Draft = 'Draft' }
|
|
11
|
+
* const enum Direction { Up = 'up', Down = 'down' }
|
|
12
|
+
*
|
|
13
|
+
* Skips:
|
|
14
|
+
* empty strings, numeric enum members, computed keys, HTML entities
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {parse} from '@babel/parser'
|
|
18
|
+
import {traverse, BABEL_PLUGINS, HTML_ENTITY_RE, normalise} from './utils.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse `code` and record every non-empty string literal value into the
|
|
22
|
+
* processor dictionary. Works for plain objects and TS enum declarations.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} code
|
|
25
|
+
* @param {string} id - File path (for collision warnings).
|
|
26
|
+
* @param {import('./processor.js').TranslationProcessor} processor
|
|
27
|
+
*/
|
|
28
|
+
export function extractStringsFromFile(code, id, processor) {
|
|
29
|
+
let ast
|
|
30
|
+
try {
|
|
31
|
+
ast = parse(code, {sourceType: 'module', plugins: BABEL_PLUGINS})
|
|
32
|
+
} catch {
|
|
33
|
+
processor.log(`[dictionaryFiles] failed to parse ${id} — skipping`)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
traverse(ast, {
|
|
38
|
+
/** Object property with a string value: { BRAND_NAME: 'QuestionPro' } */
|
|
39
|
+
ObjectProperty(path) {
|
|
40
|
+
const value = path.node.value
|
|
41
|
+
if (value.type !== 'StringLiteral') return
|
|
42
|
+
const text = normalise(value.value)
|
|
43
|
+
if (!text) return
|
|
44
|
+
if (HTML_ENTITY_RE.test(text)) return
|
|
45
|
+
processor.record(text, text, id, '(dictionaryFiles:object)')
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/** TypeScript enum member with a string initialiser: Active = 'Active' */
|
|
49
|
+
TSEnumMember(path) {
|
|
50
|
+
const init = path.node.initializer
|
|
51
|
+
if (!init || init.type !== 'StringLiteral') return
|
|
52
|
+
const text = normalise(init.value)
|
|
53
|
+
if (!text) return
|
|
54
|
+
if (HTML_ENTITY_RE.test(text)) return
|
|
55
|
+
processor.record(text, text, id, '(dictionaryFiles:enum)')
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
}
|
package/src/processor.js
CHANGED
|
@@ -4,13 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/** Prop names translated by default on Wu* components. Pass `translatableProps` to override. */
|
|
7
|
-
const DEFAULT_TRANSLATABLE_PROPS = [
|
|
8
|
-
'Label',
|
|
9
|
-
'placeholder',
|
|
10
|
-
'title',
|
|
11
|
-
'aria-label',
|
|
12
|
-
'aria-placeholder',
|
|
13
|
-
]
|
|
7
|
+
const DEFAULT_TRANSLATABLE_PROPS = ['Label', 'placeholder', 'title', 'aria-label', 'aria-placeholder']
|
|
14
8
|
|
|
15
9
|
/** Components always excluded from translation regardless of user config. */
|
|
16
10
|
const DEFAULT_IGNORE = [
|
|
@@ -19,7 +13,7 @@ const DEFAULT_IGNORE = [
|
|
|
19
13
|
'WuHelpButton',
|
|
20
14
|
'WuActivityLog',
|
|
21
15
|
'WuAppHeader',
|
|
22
|
-
'
|
|
16
|
+
'WuAppHeaderMenu',
|
|
23
17
|
'WuCopyToClipboard',
|
|
24
18
|
'WuMenuIcon',
|
|
25
19
|
'WuScrollArea',
|
|
@@ -33,21 +27,18 @@ export class TranslationProcessor {
|
|
|
33
27
|
* @param {object} options
|
|
34
28
|
* @param {string[]} options.components - Component names that trigger translation.
|
|
35
29
|
* @param {string[]} [options.ignoreComponents] - Extra components to exclude.
|
|
36
|
-
* @param {string[]} [options.translatableProps] - JSX prop names to rewrite to
|
|
37
|
-
* @param {string[]} [options.
|
|
30
|
+
* @param {string[]} [options.translatableProps] - Extra JSX prop names to rewrite to useWt()(). Appended to defaults.
|
|
31
|
+
* @param {string[]} [options.dataLabelKeys] - Object property names whose string values are labels in mixed
|
|
32
|
+
* data files (e.g. ['label', 'title'] in nav config arrays).
|
|
38
33
|
* @param {boolean} [options.debug] - Enable verbose logging.
|
|
39
34
|
*/
|
|
40
35
|
constructor(options) {
|
|
41
36
|
this.components = new Set(options.components)
|
|
42
|
-
this.ignoreComponents = new Set(
|
|
43
|
-
DEFAULT_IGNORE.concat(options.ignoreComponents || []),
|
|
44
|
-
)
|
|
37
|
+
this.ignoreComponents = new Set(DEFAULT_IGNORE.concat(options.ignoreComponents || []))
|
|
45
38
|
/** @type {Set<string>} JSX prop names that should be translated. */
|
|
46
|
-
this.translatableProps = new Set(
|
|
47
|
-
options.translatableProps ?? DEFAULT_TRANSLATABLE_PROPS,
|
|
48
|
-
)
|
|
39
|
+
this.translatableProps = new Set([...DEFAULT_TRANSLATABLE_PROPS, ...(options.translatableProps || [])])
|
|
49
40
|
/** @type {Set<string>} Object property key names whose string values are extracted (e.g. 'label'). */
|
|
50
|
-
this.extractFromKeys = new Set(options.
|
|
41
|
+
this.extractFromKeys = new Set(options.dataLabelKeys || [])
|
|
51
42
|
/** @type {Map<string, string>} key → original text */
|
|
52
43
|
this.dictionary = new Map()
|
|
53
44
|
/** @type {import('./debug.js').DebugEntry[]} */
|
|
@@ -74,9 +65,7 @@ export class TranslationProcessor {
|
|
|
74
65
|
*/
|
|
75
66
|
record(key, text, file, componentTree) {
|
|
76
67
|
if (this.dictionary.has(key) && this.dictionary.get(key) !== text) {
|
|
77
|
-
console.warn(
|
|
78
|
-
`[wick-i18n] Collision in ${file}\nKey: "${key}"\nNew: "${text}"`,
|
|
79
|
-
)
|
|
68
|
+
console.warn(`[wick-i18n] Collision in ${file}\nKey: "${key}"\nNew: "${text}"`)
|
|
80
69
|
return
|
|
81
70
|
}
|
|
82
71
|
this.dictionary.set(key, text)
|
|
@@ -88,7 +77,7 @@ export class TranslationProcessor {
|
|
|
88
77
|
* component that should be translated.
|
|
89
78
|
*
|
|
90
79
|
* Rules (first match wins, walking outward):
|
|
91
|
-
* - `data-
|
|
80
|
+
* - `data-i18n-skip` attr → ignored
|
|
92
81
|
* - component in `ignoreComponents` → ignored
|
|
93
82
|
* - `data-i18n-wrapper` attr OR component starts with "Wu" / is in `components` → translate
|
|
94
83
|
*
|
|
@@ -102,14 +91,10 @@ export class TranslationProcessor {
|
|
|
102
91
|
path.findParent(p => {
|
|
103
92
|
if (!p.isJSXElement()) return false
|
|
104
93
|
|
|
105
|
-
const name =
|
|
106
|
-
p.node.openingElement.name.name ||
|
|
107
|
-
p.node.openingElement.name.property?.name
|
|
94
|
+
const name = p.node.openingElement.name.name || p.node.openingElement.name.property?.name
|
|
108
95
|
const attrs = p.node.openingElement.attributes || []
|
|
109
96
|
|
|
110
|
-
if (
|
|
111
|
-
attrs.some(a => ['data-skip', 'data-i18n-skip'].includes(a.name?.name))
|
|
112
|
-
) {
|
|
97
|
+
if (attrs.some(a => a.name?.name === 'data-i18n-skip')) {
|
|
113
98
|
isIgnored = true
|
|
114
99
|
return true
|
|
115
100
|
}
|
|
@@ -136,7 +121,8 @@ export class TranslationProcessor {
|
|
|
136
121
|
/**
|
|
137
122
|
* Return `true` when `propName` is in the translatable-props set and the
|
|
138
123
|
* immediate parent JSX element is a Wu* component or in `components`, and
|
|
139
|
-
* is not in `ignoreComponents`. Matched props are rewritten to `{
|
|
124
|
+
* is not in `ignoreComponents`. Matched props are rewritten to `{useWt()("...")}` by the transform.
|
|
125
|
+
*
|
|
140
126
|
* @param {string} propName
|
|
141
127
|
* @param {import('@babel/traverse').NodePath} path - JSXAttribute path.
|
|
142
128
|
* @returns {boolean}
|
|
@@ -148,10 +134,9 @@ export class TranslationProcessor {
|
|
|
148
134
|
const name = openingEl.name?.name || openingEl.name?.property?.name
|
|
149
135
|
if (!name) return false
|
|
150
136
|
if (this.ignoreComponents.has(name)) return false
|
|
151
|
-
// Respect data-
|
|
137
|
+
// Respect data-i18n-skip on the same element
|
|
152
138
|
const attrs = openingEl.attributes || []
|
|
153
|
-
if (attrs.some(a =>
|
|
154
|
-
return false
|
|
139
|
+
if (attrs.some(a => a.name?.name === 'data-i18n-skip')) return false
|
|
155
140
|
return name.startsWith('Wu') || this.components.has(name)
|
|
156
141
|
}
|
|
157
142
|
|
|
@@ -166,14 +151,9 @@ export class TranslationProcessor {
|
|
|
166
151
|
let result = null
|
|
167
152
|
path.findParent(p => {
|
|
168
153
|
if (!p.isJSXElement()) return false
|
|
169
|
-
const attr = p.node.openingElement.attributes.find(
|
|
170
|
-
a => a.name?.name === 'data-i18n-key',
|
|
171
|
-
)
|
|
154
|
+
const attr = p.node.openingElement.attributes.find(a => a.name?.name === 'data-i18n-key')
|
|
172
155
|
if (!attr) return false
|
|
173
|
-
const val =
|
|
174
|
-
attr.value.type === 'StringLiteral'
|
|
175
|
-
? attr.value.value
|
|
176
|
-
: attr.value.expression?.value
|
|
156
|
+
const val = attr.value.type === 'StringLiteral' ? attr.value.value : attr.value.expression?.value
|
|
177
157
|
if (!val) {
|
|
178
158
|
console.warn(
|
|
179
159
|
`[wick-i18n] data-i18n-key on <${p.node.openingElement.name.name}> is dynamic or empty — falling back to text content.`,
|