bctranslate 1.0.0 → 1.0.2
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/bin/bctranslate.js +132 -132
- package/package.json +1 -1
- package/python/translator.py +7 -0
- package/src/bridges/python.js +61 -55
- package/src/index.js +158 -97
- package/src/parsers/html.js +3 -3
- package/src/parsers/js.js +4 -4
- package/src/parsers/json.js +1 -1
- package/src/parsers/react.js +55 -60
- package/src/parsers/vue.js +99 -105
- package/src/utils.js +159 -23
package/src/parsers/react.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import * as babelParser from '@babel/parser';
|
|
2
2
|
import _traverse from '@babel/traverse';
|
|
3
|
-
import _generate from '@babel/generator';
|
|
4
|
-
import * as t from '@babel/types';
|
|
5
3
|
import MagicString from 'magic-string';
|
|
6
|
-
import {
|
|
4
|
+
import { contextKey, isTranslatable } from '../utils.js';
|
|
7
5
|
|
|
8
6
|
// Handle ESM default export quirks
|
|
9
7
|
const traverse = _traverse.default || _traverse;
|
|
10
|
-
const generate = _generate.default || _generate;
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* Non-translatable JSX attribute names.
|
|
@@ -25,8 +22,31 @@ const ATTR_WHITELIST = new Set([
|
|
|
25
22
|
'aria-placeholder', 'aria-description',
|
|
26
23
|
]);
|
|
27
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Walk up the Babel path tree to find the nearest enclosing function
|
|
27
|
+
* with a BlockStatement body — that is the React component function.
|
|
28
|
+
* Returns the character offset right after the opening `{`.
|
|
29
|
+
*/
|
|
30
|
+
function findComponentBodyStart(jsxPath) {
|
|
31
|
+
let p = jsxPath.parentPath;
|
|
32
|
+
while (p) {
|
|
33
|
+
const { node } = p;
|
|
34
|
+
if (
|
|
35
|
+
(node.type === 'FunctionDeclaration' ||
|
|
36
|
+
node.type === 'FunctionExpression' ||
|
|
37
|
+
node.type === 'ArrowFunctionExpression') &&
|
|
38
|
+
node.body?.type === 'BlockStatement'
|
|
39
|
+
) {
|
|
40
|
+
return node.body.start + 1; // right after opening {
|
|
41
|
+
}
|
|
42
|
+
p = p.parentPath;
|
|
43
|
+
}
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
/**
|
|
29
48
|
* Parse a JSX/TSX file and extract translatable strings.
|
|
49
|
+
* Hook injection uses AST-derived character positions — no regex.
|
|
30
50
|
*/
|
|
31
51
|
export function parseReact(source, filePath) {
|
|
32
52
|
const extracted = [];
|
|
@@ -45,13 +65,12 @@ export function parseReact(source, filePath) {
|
|
|
45
65
|
'decorators-legacy',
|
|
46
66
|
].filter(Boolean),
|
|
47
67
|
});
|
|
48
|
-
} catch
|
|
49
|
-
// If Babel fails, return unmodified
|
|
68
|
+
} catch {
|
|
50
69
|
return { source, extracted: [], modified: false };
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
const s = new MagicString(source);
|
|
54
|
-
let
|
|
73
|
+
let hookInsertPos = -1;
|
|
55
74
|
|
|
56
75
|
traverse(ast, {
|
|
57
76
|
// JSX text children: <div>Hello World</div>
|
|
@@ -59,28 +78,23 @@ export function parseReact(source, filePath) {
|
|
|
59
78
|
const text = path.node.value.trim();
|
|
60
79
|
if (!isTranslatable(text)) return;
|
|
61
80
|
|
|
62
|
-
const key =
|
|
63
|
-
const start = path.node
|
|
64
|
-
const end = path.node.end;
|
|
65
|
-
|
|
66
|
-
// Preserve whitespace
|
|
81
|
+
const key = contextKey(text, filePath);
|
|
82
|
+
const { start, end } = path.node;
|
|
67
83
|
const original = path.node.value;
|
|
68
84
|
const leadingWs = original.match(/^(\s*)/)[1];
|
|
69
85
|
const trailingWs = original.match(/(\s*)$/)[1];
|
|
70
86
|
|
|
71
87
|
s.overwrite(start, end, `${leadingWs}{t('${key}')}${trailingWs}`);
|
|
72
88
|
extracted.push({ key, text, context: 'jsx-text' });
|
|
73
|
-
|
|
89
|
+
|
|
90
|
+
if (hookInsertPos === -1) hookInsertPos = findComponentBodyStart(path);
|
|
74
91
|
},
|
|
75
92
|
|
|
76
93
|
// JSX string attributes: <input placeholder="Enter name" />
|
|
77
94
|
JSXAttribute(path) {
|
|
78
95
|
const name = path.node.name?.name;
|
|
79
|
-
if (!name) return;
|
|
80
|
-
|
|
81
|
-
if (ATTR_BLACKLIST.has(name)) return;
|
|
96
|
+
if (!name || ATTR_BLACKLIST.has(name)) return;
|
|
82
97
|
|
|
83
|
-
// Only translate whitelisted attrs, or unknown attrs if they have translatable values
|
|
84
98
|
const value = path.node.value;
|
|
85
99
|
if (!value || value.type !== 'StringLiteral') return;
|
|
86
100
|
|
|
@@ -88,60 +102,41 @@ export function parseReact(source, filePath) {
|
|
|
88
102
|
if (!isTranslatable(text)) return;
|
|
89
103
|
if (!ATTR_WHITELIST.has(name) && text.length < 3) return;
|
|
90
104
|
|
|
91
|
-
const key =
|
|
92
|
-
|
|
93
|
-
const attrEnd = path.node.end;
|
|
94
|
-
|
|
95
|
-
s.overwrite(attrStart, attrEnd, `${name}={t('${key}')}`);
|
|
105
|
+
const key = contextKey(text, filePath);
|
|
106
|
+
s.overwrite(path.node.start, path.node.end, `${name}={t('${key}')}`);
|
|
96
107
|
extracted.push({ key, text, context: `jsx-attr-${name}` });
|
|
97
|
-
|
|
108
|
+
|
|
109
|
+
if (hookInsertPos === -1) hookInsertPos = findComponentBodyStart(path);
|
|
98
110
|
},
|
|
99
111
|
|
|
100
|
-
//
|
|
112
|
+
// alert('...'), confirm('...')
|
|
101
113
|
CallExpression(path) {
|
|
102
114
|
const callee = path.node.callee;
|
|
103
|
-
const calleeName = callee.name ||
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
extracted.push({ key, text: arg.value, context: 'call-arg' });
|
|
112
|
-
needsImport = true;
|
|
113
|
-
}
|
|
115
|
+
const calleeName = callee.name || callee.property?.name;
|
|
116
|
+
if (!['alert', 'confirm'].includes(calleeName)) return;
|
|
117
|
+
|
|
118
|
+
const arg = path.node.arguments[0];
|
|
119
|
+
if (arg?.type === 'StringLiteral' && isTranslatable(arg.value)) {
|
|
120
|
+
const key = contextKey(arg.value, filePath);
|
|
121
|
+
s.overwrite(arg.start, arg.end, `t('${key}')`);
|
|
122
|
+
extracted.push({ key, text: arg.value, context: 'call-arg' });
|
|
114
123
|
}
|
|
115
124
|
},
|
|
116
125
|
});
|
|
117
126
|
|
|
118
|
-
//
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (importMatch) {
|
|
126
|
-
// Insert after all imports
|
|
127
|
-
const lastImportMatch = [...source.matchAll(/^import\s.+$/gm)];
|
|
128
|
-
if (lastImportMatch.length > 0) {
|
|
129
|
-
const last = lastImportMatch[lastImportMatch.length - 1];
|
|
130
|
-
insertPos = last.index + last[0].length;
|
|
131
|
-
}
|
|
127
|
+
// ── Inject import and hook using AST-derived positions ───────────────────────
|
|
128
|
+
if (extracted.length > 0) {
|
|
129
|
+
if (!source.includes('useTranslation')) {
|
|
130
|
+
// Find the last ImportDeclaration node — safe even with 'use client' directives
|
|
131
|
+
let lastImportEnd = 0;
|
|
132
|
+
for (const node of ast.program.body) {
|
|
133
|
+
if (node.type === 'ImportDeclaration') lastImportEnd = node.end;
|
|
132
134
|
}
|
|
133
|
-
|
|
134
|
-
s.appendRight(insertPos, `\nimport { useTranslation } from 'react-i18next';\n`);
|
|
135
|
+
s.appendRight(lastImportEnd, `\nimport { useTranslation } from 'react-i18next';`);
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Find the function body
|
|
140
|
-
const funcMatch = source.match(/(?:function\s+\w+|const\s+\w+\s*=\s*(?:\([^)]*\)\s*=>|\w+\s*=>))\s*\{/);
|
|
141
|
-
if (funcMatch) {
|
|
142
|
-
const insertAt = funcMatch.index + funcMatch[0].length;
|
|
143
|
-
s.appendRight(insertAt, `\n const { t } = useTranslation();\n`);
|
|
144
|
-
}
|
|
138
|
+
if (!source.includes('useTranslation()') && hookInsertPos > 0) {
|
|
139
|
+
s.appendRight(hookInsertPos, `\n const { t } = useTranslation();\n`);
|
|
145
140
|
}
|
|
146
141
|
}
|
|
147
142
|
|
|
@@ -150,4 +145,4 @@ export function parseReact(source, filePath) {
|
|
|
150
145
|
extracted,
|
|
151
146
|
modified: extracted.length > 0,
|
|
152
147
|
};
|
|
153
|
-
}
|
|
148
|
+
}
|
package/src/parsers/vue.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import * as compiler from '@vue/compiler-dom';
|
|
2
2
|
import MagicString from 'magic-string';
|
|
3
|
-
import {
|
|
3
|
+
import { contextKey, isTranslatable } from '../utils.js';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Non-translatable attribute names.
|
|
7
|
-
*/
|
|
8
5
|
const ATTR_BLACKLIST = new Set([
|
|
9
6
|
'id', 'class', 'style', 'src', 'href', 'ref', 'key', 'is',
|
|
10
7
|
'v-model', 'v-bind', 'v-on', 'v-if', 'v-else', 'v-else-if',
|
|
@@ -17,149 +14,149 @@ const ATTR_BLACKLIST = new Set([
|
|
|
17
14
|
'xmlns:xlink', 'xlink:href', 'data-testid', 'data-cy',
|
|
18
15
|
]);
|
|
19
16
|
|
|
20
|
-
/**
|
|
21
|
-
* Translatable attribute names.
|
|
22
|
-
*/
|
|
23
17
|
const ATTR_WHITELIST = new Set([
|
|
24
18
|
'title', 'placeholder', 'label', 'alt', 'aria-label',
|
|
25
19
|
'aria-placeholder', 'aria-description', 'aria-roledescription',
|
|
26
20
|
]);
|
|
27
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Detect which t-function call to emit in templates.
|
|
24
|
+
*
|
|
25
|
+
* Rules:
|
|
26
|
+
* - If <script setup> and file already destructures `t` from a composable
|
|
27
|
+
* → use t('key') (matches user's existing pattern)
|
|
28
|
+
* - If <script setup> but no `t` yet
|
|
29
|
+
* → use t('key') AND inject `const { t } = useI18n()` into script
|
|
30
|
+
* - Options API (no setup)
|
|
31
|
+
* → use $t('key') (global plugin property)
|
|
32
|
+
*/
|
|
33
|
+
function detectTStyle(source) {
|
|
34
|
+
const isSetup = /<script\b[^>]*\bsetup\b/i.test(source);
|
|
35
|
+
|
|
36
|
+
// Already has: const { t } = ... or const { t, ... } = ...
|
|
37
|
+
const hasT = /const\s*\{[^}]*\bt\b[^}]*\}\s*=/.test(source);
|
|
38
|
+
|
|
39
|
+
return { isSetup, hasT };
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
/**
|
|
29
43
|
* Parse a .vue file and extract translatable strings.
|
|
30
|
-
* Returns the modified source and the list of extracted strings.
|
|
31
44
|
*/
|
|
32
45
|
export function parseVue(source, filePath) {
|
|
33
|
-
const extracted = [];
|
|
46
|
+
const extracted = [];
|
|
34
47
|
const s = new MagicString(source);
|
|
35
|
-
let modified = false;
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
const { isSetup, hasT } = detectTStyle(source);
|
|
50
|
+
|
|
51
|
+
// In templates: use t() for Composition API, $t() for Options API
|
|
52
|
+
const tpl = (key) => (isSetup || hasT) ? `t('${key}')` : `$t('${key}')`;
|
|
53
|
+
// In scripts: use t() for setup, this.$t() for options
|
|
54
|
+
const scr = (key) => isSetup ? `t('${key}')` : `this.$t('${key}')`;
|
|
55
|
+
|
|
56
|
+
// ── Template ────────────────────────────────────────────────────────────────
|
|
38
57
|
const templateMatch = source.match(/<template\b[^>]*>([\s\S]*?)<\/template>/);
|
|
39
58
|
if (templateMatch) {
|
|
40
|
-
const templateStart = source.indexOf(templateMatch[0]);
|
|
41
59
|
const templateContent = templateMatch[1];
|
|
42
|
-
const templateOffset =
|
|
60
|
+
const templateOffset =
|
|
61
|
+
source.indexOf(templateMatch[0]) + templateMatch[0].indexOf(templateContent);
|
|
43
62
|
|
|
44
63
|
try {
|
|
45
|
-
const ast = compiler.parse(templateContent, {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
walkTemplate(ast.children, s, templateOffset, extracted);
|
|
51
|
-
modified = extracted.length > 0;
|
|
52
|
-
} catch (err) {
|
|
53
|
-
// If Vue parser fails, fall back to regex-based extraction for template
|
|
54
|
-
extractTemplateRegex(source, s, extracted);
|
|
55
|
-
modified = extracted.length > 0;
|
|
64
|
+
const ast = compiler.parse(templateContent, { comments: true, getTextMode: () => 0 });
|
|
65
|
+
walkTemplate(ast.children, s, templateOffset, extracted, filePath, tpl);
|
|
66
|
+
} catch {
|
|
67
|
+
extractTemplateRegex(source, s, extracted, filePath, tpl);
|
|
56
68
|
}
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
//
|
|
71
|
+
// ── Script ──────────────────────────────────────────────────────────────────
|
|
60
72
|
const scriptMatch = source.match(/<script\b[^>]*>([\s\S]*?)<\/script>/);
|
|
61
73
|
if (scriptMatch) {
|
|
62
|
-
const scriptStart = source.indexOf(scriptMatch[0]);
|
|
63
74
|
const scriptContent = scriptMatch[1];
|
|
64
|
-
const scriptOffset =
|
|
65
|
-
|
|
75
|
+
const scriptOffset =
|
|
76
|
+
source.indexOf(scriptMatch[0]) + scriptMatch[0].indexOf(scriptContent);
|
|
77
|
+
extractScriptStrings(scriptContent, s, scriptOffset, extracted, filePath, scr);
|
|
78
|
+
}
|
|
66
79
|
|
|
67
|
-
|
|
80
|
+
// ── Inject `const { t } = useI18n()` if setup but t not yet declared ────────
|
|
81
|
+
if (extracted.length > 0 && isSetup && !hasT) {
|
|
82
|
+
const scriptSetupMatch = source.match(/(<script\b[^>]*\bsetup\b[^>]*>)([\s\S]*?)<\/script>/i);
|
|
83
|
+
if (scriptSetupMatch) {
|
|
84
|
+
const insertAt =
|
|
85
|
+
source.indexOf(scriptSetupMatch[0]) + scriptSetupMatch[1].length;
|
|
86
|
+
const needsImport = !source.includes('useI18n');
|
|
87
|
+
const importLine = needsImport ? `import { useI18n } from 'vue-i18n';\n` : '';
|
|
88
|
+
s.appendRight(insertAt, `\n${importLine}const { t } = useI18n();\n`);
|
|
89
|
+
}
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
return {
|
|
71
|
-
source:
|
|
93
|
+
source: extracted.length > 0 ? s.toString() : source,
|
|
72
94
|
extracted,
|
|
73
|
-
modified:
|
|
95
|
+
modified: extracted.length > 0,
|
|
74
96
|
};
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
|
|
99
|
+
// ── Template walker ───────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
function walkTemplate(nodes, s, baseOffset, extracted, filePath, tpl) {
|
|
78
102
|
for (const node of nodes) {
|
|
79
|
-
//
|
|
103
|
+
// Text node (type 2)
|
|
80
104
|
if (node.type === 2) {
|
|
81
105
|
const text = node.content.trim();
|
|
82
106
|
if (isTranslatable(text)) {
|
|
83
|
-
const key = hashKey(text);
|
|
84
107
|
const start = baseOffset + node.loc.start.offset;
|
|
85
|
-
const end
|
|
108
|
+
const end = baseOffset + node.loc.end.offset;
|
|
86
109
|
|
|
87
|
-
// Check if already wrapped in $t()
|
|
88
110
|
if (!isAlreadyWrapped(s.original, start, end)) {
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
s.overwrite(start, end, `${leadingWs}{{ $t('${key}') }}${trailingWs}`);
|
|
111
|
+
const key = contextKey(text, filePath);
|
|
112
|
+
const orig = node.content;
|
|
113
|
+
const lws = orig.match(/^(\s*)/)[1];
|
|
114
|
+
const tws = orig.match(/(\s*)$/)[1];
|
|
115
|
+
s.overwrite(start, end, `${lws}{{ ${tpl(key)} }}${tws}`);
|
|
95
116
|
extracted.push({ key, text, context: 'template-text' });
|
|
96
117
|
}
|
|
97
118
|
}
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
//
|
|
121
|
+
// Element (type 1) — check translatable attributes, then recurse
|
|
101
122
|
if (node.type === 1) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (ATTR_WHITELIST.has(attrName) && isTranslatable(prop.value.content)) {
|
|
110
|
-
const text = prop.value.content;
|
|
111
|
-
const key = hashKey(text);
|
|
123
|
+
for (const prop of node.props ?? []) {
|
|
124
|
+
if (prop.type === 6 && prop.value) {
|
|
125
|
+
const attrName = prop.name.toLowerCase();
|
|
126
|
+
if (ATTR_WHITELIST.has(attrName) && isTranslatable(prop.value.content)) {
|
|
127
|
+
const text = prop.value.content;
|
|
128
|
+
if (!ATTR_BLACKLIST.has(attrName)) {
|
|
129
|
+
const key = contextKey(text, filePath);
|
|
112
130
|
const attrStart = baseOffset + prop.loc.start.offset;
|
|
113
|
-
const attrEnd
|
|
114
|
-
|
|
115
|
-
// Convert static attribute to v-bind with $t()
|
|
116
|
-
s.overwrite(attrStart, attrEnd, `:${attrName}="$t('${key}')"`);
|
|
131
|
+
const attrEnd = baseOffset + prop.loc.end.offset;
|
|
132
|
+
s.overwrite(attrStart, attrEnd, `:${attrName}="${tpl(key)}"`);
|
|
117
133
|
extracted.push({ key, text, context: `template-attr-${attrName}` });
|
|
118
134
|
}
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
}
|
|
122
|
-
|
|
123
|
-
// Recurse into children
|
|
124
|
-
if (node.children) {
|
|
125
|
-
walkTemplate(node.children, s, baseOffset, extracted);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Type 5 = Interpolation ({{ expr }})
|
|
130
|
-
if (node.type === 5 && node.content) {
|
|
131
|
-
// Check for compound expressions that contain string literals
|
|
132
|
-
// e.g., {{ isError ? 'Failed' : 'Success' }}
|
|
133
|
-
// We only extract simple string content, not expressions
|
|
138
|
+
if (node.children) walkTemplate(node.children, s, baseOffset, extracted, filePath, tpl);
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
//
|
|
137
|
-
// Type 11 = ForNode — recurse into children
|
|
141
|
+
// ForNode (type 11)
|
|
138
142
|
if (node.type === 11 && node.children) {
|
|
139
|
-
walkTemplate(node.children, s, baseOffset, extracted);
|
|
143
|
+
walkTemplate(node.children, s, baseOffset, extracted, filePath, tpl);
|
|
140
144
|
}
|
|
141
|
-
|
|
145
|
+
|
|
146
|
+
// IfNode (type 9) — walk branches
|
|
142
147
|
if (node.type === 9 && node.branches) {
|
|
143
148
|
for (const branch of node.branches) {
|
|
144
|
-
if (branch.children)
|
|
145
|
-
walkTemplate(branch.children, s, baseOffset, extracted);
|
|
146
|
-
}
|
|
149
|
+
if (branch.children) walkTemplate(branch.children, s, baseOffset, extracted, filePath, tpl);
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
// Match string literals that look like translatable text in common patterns:
|
|
154
|
-
// - alert('text'), confirm('text')
|
|
155
|
-
// - title: 'text', label: 'text', placeholder: 'text', message: 'text'
|
|
156
|
-
// - toast('text'), notify('text')
|
|
157
|
-
// - error/success/warning message strings
|
|
155
|
+
// ── Script string extractor ───────────────────────────────────────────────────
|
|
158
156
|
|
|
157
|
+
function extractScriptStrings(scriptContent, s, baseOffset, extracted, filePath, scr) {
|
|
159
158
|
const patterns = [
|
|
160
|
-
// Function calls: alert('...'), confirm('...'), toast('...'), notify('...')
|
|
161
159
|
/\b(alert|confirm|toast|notify|message\.(?:success|error|warning|info))\s*\(\s*(['"`])((?:(?!\2).)+)\2\s*\)/g,
|
|
162
|
-
// Object properties: title: '...', label: '...', message: '...'
|
|
163
160
|
/\b(title|label|placeholder|message|text|description|tooltip|hint|caption|header|subtitle|errorMessage|successMessage)\s*:\s*(['"`])((?:(?!\2).)+)\2/g,
|
|
164
161
|
];
|
|
165
162
|
|
|
@@ -168,44 +165,41 @@ function extractScriptStrings(scriptContent, s, baseOffset, extracted, isSetup)
|
|
|
168
165
|
while ((match = pattern.exec(scriptContent)) !== null) {
|
|
169
166
|
const text = match[3];
|
|
170
167
|
if (isTranslatable(text) && text.length > 1) {
|
|
171
|
-
const key
|
|
172
|
-
const fullMatchStart = baseOffset + match.index;
|
|
168
|
+
const key = contextKey(text, filePath);
|
|
173
169
|
const quoteChar = match[2];
|
|
174
170
|
const textStart = baseOffset + match.index + match[0].indexOf(quoteChar + text);
|
|
175
|
-
const textEnd
|
|
176
|
-
|
|
177
|
-
const tCall = isSetup ? `t('${key}')` : `this.$t('${key}')`;
|
|
178
|
-
|
|
179
|
-
s.overwrite(textStart, textEnd, tCall);
|
|
171
|
+
const textEnd = textStart + text.length + 2;
|
|
172
|
+
s.overwrite(textStart, textEnd, scr(key));
|
|
180
173
|
extracted.push({ key, text, context: 'script' });
|
|
181
174
|
}
|
|
182
175
|
}
|
|
183
176
|
}
|
|
184
177
|
}
|
|
185
178
|
|
|
179
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
186
181
|
function isAlreadyWrapped(source, start, end) {
|
|
187
|
-
//
|
|
188
|
-
const before = source.slice(Math.max(0, start -
|
|
189
|
-
return
|
|
182
|
+
// Look back 25 chars for an open t( or $t( call — node is inside an interpolation
|
|
183
|
+
const before = source.slice(Math.max(0, start - 25), start);
|
|
184
|
+
return /\$?t\s*\(\s*['"]/.test(before);
|
|
190
185
|
}
|
|
191
186
|
|
|
192
|
-
function extractTemplateRegex(source, s, extracted) {
|
|
193
|
-
|
|
194
|
-
const textPattern = />([^<]+)</g;
|
|
187
|
+
function extractTemplateRegex(source, s, extracted, filePath, tpl) {
|
|
188
|
+
const pattern = />([^<]+)</g;
|
|
195
189
|
let match;
|
|
196
|
-
while ((match =
|
|
190
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
197
191
|
const text = match[1].trim();
|
|
198
192
|
if (isTranslatable(text)) {
|
|
199
|
-
const key = hashKey(text);
|
|
200
193
|
const textStart = match.index + 1;
|
|
201
|
-
const textEnd
|
|
194
|
+
const textEnd = textStart + match[1].length;
|
|
202
195
|
if (!isAlreadyWrapped(source, textStart, textEnd)) {
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
196
|
+
const key = contextKey(text, filePath);
|
|
197
|
+
const orig = match[1];
|
|
198
|
+
const lws = orig.match(/^(\s*)/)[1];
|
|
199
|
+
const tws = orig.match(/(\s*)$/)[1];
|
|
200
|
+
s.overwrite(textStart, textEnd, `${lws}{{ ${tpl(key)} }}${tws}`);
|
|
207
201
|
extracted.push({ key, text, context: 'template-text' });
|
|
208
202
|
}
|
|
209
203
|
}
|
|
210
204
|
}
|
|
211
|
-
}
|
|
205
|
+
}
|