@npm-questionpro/wick-ui-i18n 2.0.0-next.9 → 2.0.1

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 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
- translatable props to `{wt("...")}`, and emits `wick-ui-i18n.json`.
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>&amp;</WuButton>` | ❌ |
19
- | `<WuButton>Hello &amp; World</WuButton>` | ✅ `<WuTranslate __i18nKey="Hello" /> &amp; <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
- | 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
8
+ ---
56
9
 
57
- Defaults: `Label`, `placeholder`, `title`, `aria-label`, `aria-placeholder`.
10
+ ## Quick start
58
11
 
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" />` | ❌ |
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
- ## `wt()` calls
18
+ ---
71
19
 
72
- Plugin records static args into `wick-ui-i18n.json`. No code rewrite unless
73
- template literal with expressions.
20
+ ## What it transforms
74
21
 
75
- | Input | Dictionary | Code output |
76
- | ----------------------------- | --------------------- | --------------------------------------------- |
77
- | `wt("Hello")` | `"Hello"` | `wt("Hello")` |
78
- | ``wt(`Hello`)`` | `"Hello"` | ``wt(`Hello`)`` |
79
- | ``wt(`Hello ${name}`)`` | ✅ `"Hello"` | `` `${wt("Hello")} ${name}` `` |
80
- | ``wt(`Hello ${a} and ${b}`)`` | ✅ `"Hello"`, `"and"` | `` `${wt("Hello")} ${a} ${wt("and")} ${b}` `` |
81
- | `wt(variable)` | ❌ | — |
82
- | ``wt(`${a}${b}`)`` | ❌ | |
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
- ## Data files (`extractFromKeys` option)
32
+ ---
85
33
 
86
- No code rewrite — keys only recorded in `wick-ui-i18n.json`.
34
+ ## Options
87
35
 
88
- | Input | Dictionary |
89
- | ------------------------------------------------------- | ---------------- |
90
- | `{ label: 'Analytics' }` + `extractFromKeys: ['label']` | `"Analytics"` |
91
- | `{ label: variable }` | |
92
- | `{ label: '' }` | |
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
- ## Options
48
+ ## Breaking changes
97
49
 
98
- | Option | Default | Description |
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 `{wt("...")}` on Wu* components.
13
- * @default ['Label', 'placeholder', 'title', 'aria-label', 'aria-placeholder']
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. Use for data files
20
- * (nav items, option lists, etc.) paired with `wt(item.label)` at render time.
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
- extractFromKeys?: string[]
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
- /** Log every transform to the console. */
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, title, Label, …) → `{wt("...")}`
36
- * - Emits `wick-ui-i18n.json` with all extracted keys
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 `{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']).
32
- * @property {string|string[]|RegExp} [excludeFiles] - Files to skip (passed as `exclude` to Vite's createFilter).
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
- extractFromKeys: options.extractFromKeys,
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
- [...processor.components].some(c => code.includes(c))) ||
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
- return transformFile(code, id, processor)
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
- JSON.stringify(Object.fromEntries(processor.dictionary), null, 2),
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.0.0-next.9",
3
+ "version": "2.0.1",
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
- 'WuAPpHeadeMenu',
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 wt(). Overrides defaults.
37
- * @param {string[]} [options.extractFromKeys] - Object property names whose string values are recorded.
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.extractFromKeys || [])
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-skip` / `data-i18n-skip` attr → ignored
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 `{wt("...")}`. *
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-skip / data-i18n-skip on the same element
137
+ // Respect data-i18n-skip on the same element
152
138
  const attrs = openingEl.attributes || []
153
- if (attrs.some(a => ['data-skip', 'data-i18n-skip'].includes(a.name?.name)))
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.`,