@npm-questionpro/wick-ui-i18n 2.0.0-next.9 → 2.0.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.
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import {getComponentTree} from './debug.js'
21
+ import {splitQuasi} from './utils.js'
21
22
 
22
23
  /**
23
24
  * Splits the raw source around HTML entities (capturing group keeps entities
@@ -26,33 +27,12 @@ import {getComponentTree} from './debug.js'
26
27
  * "Hello & World" → ["Hello ", "&", " World"]
27
28
  * "<Tag>" → ["", "<", "Tag", ">", ""]
28
29
  */
29
- const HTML_ENTITY_SPLIT_RE =
30
- /(&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);)/g
30
+ const HTML_ENTITY_SPLIT_RE = /(&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);)/g
31
31
 
32
32
  /**
33
33
  * Tests whether a single segment (from the split above) is itself an entity.
34
34
  */
35
- const HTML_ENTITY_SEGMENT_RE =
36
- /^&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);$/
37
-
38
- /**
39
- * Given a text segment, extract leading whitespace, the translatable core,
40
- * and trailing whitespace so spacing around entities is preserved.
41
- *
42
- * @param {string} raw
43
- * @returns {{ leading: string, text: string, trailing: string }}
44
- */
45
- function splitSegment(raw) {
46
- const leading = raw.match(/^\s*/)[0]
47
- const trailing = raw.match(/\s*$/)[0]
48
- // Normalise internal whitespace to match handleCapture behaviour:
49
- // newlines → single space, consecutive spaces → single space.
50
- const text = raw
51
- .slice(leading.length, raw.length - trailing.length)
52
- .replace(/\n/g, ' ')
53
- .replace(/\s{2,}/g, ' ')
54
- return {leading, text, trailing}
55
- }
35
+ const HTML_ENTITY_SEGMENT_RE = /^&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);$/
56
36
 
57
37
  /**
58
38
  * Transform a JSXText node whose raw source contains at least one HTML entity.
@@ -73,13 +53,7 @@ function splitSegment(raw) {
73
53
  * @param {string} id - Source file path (for collision warnings).
74
54
  * @returns {boolean} `true` when a replacement was written.
75
55
  */
76
- export function transformJSXTextWithEntities(
77
- path,
78
- rawSource,
79
- ms,
80
- processor,
81
- id,
82
- ) {
56
+ export function transformJsxTextWithEntities(path, rawSource, ms, processor, id) {
83
57
  if (!processor.shouldTranslate(path)) return false
84
58
 
85
59
  // data-i18n-key cannot span multiple split segments — warn and ignore it.
@@ -105,13 +79,12 @@ export function transformJSXTextWithEntities(
105
79
  // Raw entity — emit as-is, no translation key
106
80
  parts.push(seg)
107
81
  } else {
108
- const {leading, text, trailing} = splitSegment(seg)
82
+ // splitQuasi normalises internal whitespace and separates leading/trailing padding
83
+ const {leading, text, trailing} = splitQuasi(seg)
109
84
 
110
85
  if (text) {
111
86
  processor.record(text, text, id, componentTree)
112
- parts.push(
113
- `${leading}<WuTranslate __i18nKey=${JSON.stringify(text)}></WuTranslate>${trailing}`,
114
- )
87
+ parts.push(`${leading}<WuTranslate __i18nKey=${JSON.stringify(text)}></WuTranslate>${trailing}`)
115
88
  hasTranslatable = true
116
89
  } else {
117
90
  // Pure whitespace between / around entities — preserve as JSX text
@@ -11,21 +11,7 @@
11
11
  */
12
12
 
13
13
  import {getComponentTree} from './debug.js'
14
-
15
- /**
16
- * Given a quasi's cooked string, extract leading whitespace, trimmed text,
17
- * and trailing whitespace as separate pieces so spacing is preserved in the
18
- * reconstructed JSX fragment.
19
- *
20
- * @param {string} raw - The cooked value of a TemplateElement.
21
- * @returns {{ leading: string, text: string, trailing: string }}
22
- */
23
- function splitQuasi(raw) {
24
- const leading = raw.match(/^\s*/)[0]
25
- const trailing = raw.match(/\s*$/)[0]
26
- const text = raw.slice(leading.length, raw.length - trailing.length)
27
- return {leading, text, trailing}
28
- }
14
+ import {splitQuasi, HTML_ENTITY_RE} from './utils.js'
29
15
 
30
16
  /**
31
17
  * Transform a JSXExpressionContainer whose expression is a TemplateLiteral
@@ -35,6 +21,7 @@ function splitQuasi(raw) {
35
21
  * → <><WuTranslate __i18nKey="hello" /> {name} <WuTranslate __i18nKey="how are you" /></>
36
22
  *
37
23
  * Quasis that are empty or whitespace-only are skipped (no key emitted).
24
+ * Quasis that contain HTML entities are emitted as plain text (not wrapped).
38
25
  * Leading/trailing whitespace within a quasi is preserved as JSX text around
39
26
  * the WuTranslate element so words don't run together after translation.
40
27
  *
@@ -45,13 +32,7 @@ function splitQuasi(raw) {
45
32
  * @param {string} id - Source file path (for collision warnings).
46
33
  * @returns {boolean} `true` when a replacement was written.
47
34
  */
48
- export function transformTemplateLiteralExpression(
49
- path,
50
- code,
51
- ms,
52
- processor,
53
- id,
54
- ) {
35
+ export function transformTemplateLiteralExpression(path, code, ms, processor, id) {
55
36
  if (!processor.shouldTranslate(path)) return false
56
37
 
57
38
  const container = path.node
@@ -67,11 +48,14 @@ export function transformTemplateLiteralExpression(
67
48
  const {leading, text, trailing} = splitQuasi(cooked)
68
49
 
69
50
  if (text) {
70
- processor.record(text, text, id, componentTree)
71
- parts.push(
72
- `${leading}<WuTranslate __i18nKey=${JSON.stringify(text)}></WuTranslate>${trailing}`,
73
- )
74
- hasTranslatable = true
51
+ if (HTML_ENTITY_RE.test(text)) {
52
+ // Entity-like string in this quasi — preserve as literal JSX text, no key
53
+ parts.push(cooked)
54
+ } else {
55
+ processor.record(text, text, id, componentTree)
56
+ parts.push(`${leading}<WuTranslate __i18nKey=${JSON.stringify(text)}></WuTranslate>${trailing}`)
57
+ hasTranslatable = true
58
+ }
75
59
  }
76
60
  // whitespace-only or empty quasi — emit nothing (no key, no node)
77
61
 
package/src/utils.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @fileoverview Shared utilities used across all transform modules:
3
+ * Babel traversal setup, constants, and text-normalisation helpers.
4
+ *
5
+ * Having a single source of truth here ensures every module applies
6
+ * identical normalisation and uses the same regex/plugin list.
7
+ */
8
+
9
+ import _traverse from '@babel/traverse'
10
+
11
+ /** @babel/traverse ESM/CJS interop — handles both `import traverse` forms. */
12
+ export const traverse = _traverse.default || _traverse
13
+
14
+ /** Babel parser plugins applied to every file. */
15
+ export const BABEL_PLUGINS = ['jsx', 'typescript']
16
+
17
+ /**
18
+ * Matches any HTML entity: named (&amp;), decimal (&#169;), or hex (&#x00A9;).
19
+ * Used to skip text segments that contain entities — they must be left as-is.
20
+ * Note: for JSXText this must be tested against the RAW source, not the Babel
21
+ * decoded .value (Babel turns &amp; → "&", &nbsp; → "\u00a0", etc.).
22
+ */
23
+ export const HTML_ENTITY_RE = /&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);/
24
+
25
+ /**
26
+ * Normalise a raw string: trim edges, then collapse newlines and whitespace
27
+ * runs to a single space. Applied to every string before it is used as a
28
+ * translation key so keys are stable regardless of source formatting.
29
+ *
30
+ * @param {string} raw
31
+ * @returns {string}
32
+ */
33
+ export function normalise(raw) {
34
+ return raw
35
+ .trim()
36
+ .replace(/\n/g, ' ')
37
+ .replace(/\s{2,}/g, ' ')
38
+ }
39
+
40
+ /**
41
+ * Split a template quasi (or any padded string segment) into leading
42
+ * whitespace, normalised translatable text, and trailing whitespace.
43
+ *
44
+ * Leading/trailing whitespace is preserved separately so spacing around
45
+ * the replacement node is maintained in the reconstructed JSX output.
46
+ * The middle part is passed through `normalise` so keys are consistent.
47
+ *
48
+ * Used by both the JSX template-literal transformer and the wt() template
49
+ * transformer — a single shared implementation ensures the two paths
50
+ * produce identical keys for the same source text.
51
+ *
52
+ * @param {string} raw - Cooked value of a TemplateElement or a split text segment.
53
+ * @returns {{ leading: string, text: string, trailing: string }}
54
+ */
55
+ export function splitQuasi(raw) {
56
+ const leading = raw.match(/^\s*/)[0]
57
+ const trailing = raw.match(/\s*$/)[0]
58
+ const text = normalise(raw.slice(leading.length, raw.length - trailing.length))
59
+ return {leading, text, trailing}
60
+ }
@@ -1,136 +0,0 @@
1
- /**
2
- * @fileoverview transformWtCalls — detects wt("static string") call expressions
3
- * and records the argument as a translation key.
4
- *
5
- * No code transformation is performed. wt() handles runtime lookup via the
6
- * dictionary loaded by WuTranslateProvider. The plugin's only job here is to
7
- * ensure static arguments appear in wick-ui-i18n.json at build time so they
8
- * reach the translation API.
9
- *
10
- * Supported argument forms:
11
- * wt("hello") — StringLiteral
12
- * wt(`hello`) — TemplateLiteral with zero expressions
13
- *
14
- * Ignored:
15
- * wt(variable) — dynamic, cannot be statically analysed
16
- * wt(`hello ${name}`) — has expressions, not fully static
17
- * wt() / wt(a, b) — wrong arity
18
- */
19
-
20
- /**
21
- * Extract leading whitespace, normalised text, and trailing whitespace from a
22
- * template quasi string. Mirrors the normalisation applied everywhere else in
23
- * the plugin (newlines → space, consecutive spaces → one space).
24
- *
25
- * @param {string} raw
26
- * @returns {{ leading: string, text: string, trailing: string }}
27
- */
28
- function splitQuasi(raw) {
29
- const leading = raw.match(/^\s*/)[0]
30
- const trailing = raw.match(/\s*$/)[0]
31
- const text = raw
32
- .slice(leading.length, raw.length - trailing.length)
33
- .replace(/\n/g, ' ')
34
- .replace(/\s{2,}/g, ' ')
35
- return {leading, text, trailing}
36
- }
37
-
38
- /**
39
- * If `path` is a `wt(staticString)` call expression, record the key in the
40
- * processor dictionary. Returns `true` when a key was recorded.
41
- *
42
- * @param {import('@babel/traverse').NodePath} path - CallExpression path.
43
- * @param {import('./processor.js').TranslationProcessor} processor
44
- * @param {string} id - Source file path (for collision warnings).
45
- * @returns {boolean}
46
- */
47
- export function recordWtCall(path, processor, id) {
48
- const {callee, arguments: args} = path.node
49
-
50
- // Only handle bare `wt(...)` identifiers — not obj.wt(...) etc.
51
- if (callee.type !== 'Identifier' || callee.name !== 'wt') return false
52
- if (args.length !== 1) return false
53
-
54
- const arg = args[0]
55
- let text = null
56
-
57
- if (arg.type === 'StringLiteral') {
58
- text = arg.value
59
- } else if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) {
60
- text = arg.quasis[0].value.cooked ?? arg.quasis[0].value.raw
61
- }
62
-
63
- if (text === null) return false
64
-
65
- const cleanText = text
66
- .trim()
67
- .replace(/\n/g, ' ')
68
- .replace(/\s{2,}/g, ' ')
69
- if (!cleanText) return false
70
-
71
- processor.record(cleanText, cleanText, id, '(wt)')
72
- return true
73
- }
74
-
75
- /**
76
- * Transform a `wt(\`template ${expr} literal\`)` call whose template has one
77
- * or more dynamic expressions.
78
- *
79
- * Each static quasi is independently extracted and replaced with a nested
80
- * `wt("text")` call; dynamic expressions are preserved in place. The whole
81
- * `wt(\`...\`)` call is replaced with a plain template literal:
82
- *
83
- * wt(`Hello ${name}`) → `${wt("Hello")} ${name}`
84
- * wt(`${a} and ${b}`) → `${a} ${wt("and")} ${b}`
85
- * wt(`Hello ${a} and ${b}`) → `${wt("Hello")} ${a} ${wt("and")} ${b}`
86
- *
87
- * If no quasi contains translatable text, the call is left untouched and the
88
- * function returns `false`.
89
- *
90
- * @param {import('@babel/traverse').NodePath} path - CallExpression path.
91
- * @param {string} code - Original source (for slicing expression text).
92
- * @param {import('magic-string').default} ms
93
- * @param {import('./processor.js').TranslationProcessor} processor
94
- * @param {string} id - Source file path.
95
- * @returns {boolean} `true` when the call was rewritten.
96
- */
97
- export function transformWtTemplateLiteral(path, code, ms, processor, id) {
98
- const {callee, arguments: args} = path.node
99
-
100
- if (callee.type !== 'Identifier' || callee.name !== 'wt') return false
101
- if (args.length !== 1) return false
102
-
103
- const arg = args[0]
104
- if (arg.type !== 'TemplateLiteral' || arg.expressions.length === 0)
105
- return false
106
-
107
- const {quasis, expressions} = arg
108
- const parts = ['`']
109
- let hasTranslatable = false
110
-
111
- for (let i = 0; i < quasis.length; i++) {
112
- const cooked = quasis[i].value.cooked ?? quasis[i].value.raw
113
- const {leading, text, trailing} = splitQuasi(cooked)
114
-
115
- if (text) {
116
- processor.record(text, text, id, '(wt)')
117
- parts.push(`${leading}\${wt(${JSON.stringify(text)})}${trailing}`)
118
- hasTranslatable = true
119
- } else {
120
- // empty or whitespace-only quasi — preserve as literal template text
121
- parts.push(cooked)
122
- }
123
-
124
- if (i < expressions.length) {
125
- const exprSrc = code.slice(expressions[i].start, expressions[i].end)
126
- parts.push(`\${${exprSrc}}`)
127
- }
128
- }
129
-
130
- parts.push('`')
131
-
132
- if (!hasTranslatable) return false
133
-
134
- ms.overwrite(path.node.start, path.node.end, parts.join(''))
135
- return true
136
- }