@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 +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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 (&), decimal (©), or hex (©).
|
|
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 & → "&", → "\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
|
+
}
|
package/src/transformWtCalls.js
DELETED
|
@@ -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
|
-
}
|