@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 CHANGED
@@ -1,78 +1,104 @@
1
- # Wick UI Auto I18n
1
+ # wick-ui-i18n
2
2
 
3
- **Build-time extractor that wraps static strings in `<WuTranslate>` and generates a JSON dictionary.**
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
- ### Quick Start
8
-
9
- ```javascript
10
- import wickuiI18nPlugin from "@npm-questionpro/wick-ui-i18n";
11
- export default {
12
- plugins: [wickuiI18nPlugin({ debug: true })],
13
- };
14
- ```
15
-
16
- ---
17
-
18
- ### 🛠 Attributes
19
-
20
- - **`data-i18n-wrapper`**: Forces extraction on non-Wu components (e.g., `<div data-i18n-wrapper>Text</div>`).
21
- - **`data-i18n-key="custom.key"`**: Overrides the default "text-as-key" behavior.
22
- - **`data-i18n-skip`**: Prevents the plugin from touching the element or its children.
23
-
24
- ---
25
-
26
- ### 📖 Rules & Logic
27
-
28
- - **Auto-Detection**: Targets all `Wu*` prefixed components by default.
29
- - **Supported**: Static `JSXText`, `{"Strings"}`, and non-dynamic `{`Template Literals`}`.
30
- - **Ignored**: Any expression containing variables (e.g., `{`Hello ${name}`}`).
31
- - **Output**: Generates `dist/wick_extracted_strings.json` during bundle phase.
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
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
- ### 🔄 Logic Map (Pseudo-code)
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
- RETURN Modified Code + Source Maps
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
- export interface AutoTranslateOptions {
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
- * List of components to extract text from.
4
- * If empty, matches all Wu* components automatically.
11
+ * JSX prop names rewritten to `{wt("...")}` on Wu* components.
12
+ * @default ['Label', 'placeholder', 'title', 'aria-label', 'aria-placeholder']
5
13
  */
6
- components?: string[];
14
+ translatableProps?: string[]
7
15
 
8
16
  /**
9
- * List of components to ignore text from.
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
- ignoreComponents?: string[];
21
+ extractFromKeys?: string[]
12
22
 
13
- // /**
14
- // * Files to include in the transformation.
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
- * Enable debug logging for extraction process.
21
- */
22
- debug?: boolean;
26
+ /** Log every transform to the console. */
27
+ debug?: boolean
23
28
  }
24
29
 
25
30
  /**
26
- * Vite plugin that extracts text from Wick UI components and wraps them in WuTranslate.
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 autoTranslate(options?: AutoTranslateOptions): any;
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
- if (!filter(id) || !code.includes("Wu")) return null;
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("/wick-ui-i18n.json", (_req, res) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npm-questionpro/wick-ui-i18n",
3
- "version": "0.9.0",
3
+ "version": "0.14.0",
4
4
  "license": "ISC",
5
5
  "description": "Auto-translation AST wrapper for Wick UI",
6
6
  "type": "module",
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} options
25
- * @param {string[]} options.components - Component names that trigger translation.
26
- * @param {string[]} [options.ignoreComponents] - Extra components to exclude.
27
- * @param {boolean} [options.debug] - Enable verbose logging.
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, rewrites translatable
3
- * text nodes to `<WuTranslate>`, and prepends the import if needed.
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 (&amp;), decimal (&#169;), or hex (&#x00A9;).
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 &amp; → "&", &nbsp; → "\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(path, text, start, end, ms, processor, id, skipExplicitKey = false) {
38
- const cleanText = text.trim().replace(/\n/g, " ").replace(/\s{2,}/g, " ");
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 "&amp;" 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
- * Track whether `WuTranslate` is already imported from wick-ui-lib so we
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
- hasImport = path.node.specifiers.some(
80
- (s) => s.imported?.name === "WuTranslate",
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 (&amp; → "&", &#169; → "©").
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
- const consText = getStaticString(expr.consequent);
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