bctranslate 1.0.3 → 1.0.5
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/package.json +1 -1
- package/src/generators/setup.js +87 -24
- package/src/index.js +1 -1
- package/src/parsers/html.js +23 -1
- package/src/parsers/js.js +112 -57
- package/src/parsers/vue.js +85 -67
- package/python/t2.py +0 -76
package/package.json
CHANGED
package/src/generators/setup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { getLocaleDir } from './locales.js';
|
|
@@ -150,39 +150,102 @@ async function ensureVanillaI18n(cwd, from, to, localeDir) {
|
|
|
150
150
|
locales[lang] = await resp.json();
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function t(key, params) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
function t(key, params) {
|
|
154
|
+
// Support both flat keys ("home.submit") and nested objects ({ home: { submit: ... } })
|
|
155
|
+
const dict = locales[currentLocale] || {};
|
|
156
|
+
const direct = Object.prototype.hasOwnProperty.call(dict, key) ? dict[key] : null;
|
|
157
|
+
const msg =
|
|
158
|
+
direct !== null && direct !== undefined
|
|
159
|
+
? direct
|
|
160
|
+
: key.split('.').reduce((obj, i) => (obj ? obj[i] : null), dict) || key;
|
|
161
|
+
|
|
162
|
+
if (!params) return msg;
|
|
163
|
+
return String(msg).replace(/\\{(\\w+)\\}/g, (match, k) =>
|
|
164
|
+
params[k] !== undefined ? params[k] : match
|
|
165
|
+
);
|
|
166
|
+
}
|
|
160
167
|
|
|
161
168
|
async function setLocale(lang) {
|
|
162
169
|
await loadLocale(lang);
|
|
163
170
|
currentLocale = lang;
|
|
164
171
|
// Re-translate all elements with data-i18n attribute
|
|
165
|
-
document.querySelectorAll('[data-i18n]').forEach(function (el) {
|
|
166
|
-
const key = el.getAttribute('data-i18n');
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
document.querySelectorAll('[data-i18n]').forEach(function (el) {
|
|
173
|
+
const key = el.getAttribute('data-i18n');
|
|
174
|
+
const translated = t(key);
|
|
175
|
+
// Preserve markup translations (e.g. "Hello <strong>world</strong>")
|
|
176
|
+
if (el.children.length > 0 || /<[^>]+>/.test(String(translated))) {
|
|
177
|
+
el.innerHTML = translated;
|
|
178
|
+
} else {
|
|
179
|
+
el.textContent = translated;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
169
182
|
document.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
|
|
170
183
|
el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
|
|
171
184
|
});
|
|
172
|
-
document.querySelectorAll('[data-i18n-title]').forEach(function (el) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
document.querySelectorAll('[data-i18n-title]').forEach(function (el) {
|
|
186
|
+
const translated = t(el.getAttribute('data-i18n-title'));
|
|
187
|
+
if (el.tagName === 'TITLE') {
|
|
188
|
+
document.title = translated;
|
|
189
|
+
} else {
|
|
190
|
+
el.title = translated;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Auto-init
|
|
196
|
+
loadLocale('${from}');
|
|
197
|
+
setLocale('${to}');
|
|
180
198
|
|
|
181
199
|
window.i18n = { t: t, setLocale: setLocale, loadLocale: loadLocale };
|
|
182
200
|
})();
|
|
183
201
|
`;
|
|
184
202
|
|
|
185
|
-
writeFileSync(i18nFile, content, 'utf-8');
|
|
186
|
-
console.log(chalk.green(` ✓ Created ${i18nFile}`));
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
writeFileSync(i18nFile, content, 'utf-8');
|
|
204
|
+
console.log(chalk.green(` ✓ Created ${i18nFile}`));
|
|
205
|
+
|
|
206
|
+
const injected = injectVanillaI18nEntrypoint(cwd);
|
|
207
|
+
if (!injected) {
|
|
208
|
+
console.log(chalk.yellow(` ⚠ Add <script src="i18n.js"></script> to your HTML`));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function injectVanillaI18nEntrypoint(cwd) {
|
|
213
|
+
// Prefer HTML entrypoint if present
|
|
214
|
+
const htmlPath = join(cwd, 'index.html');
|
|
215
|
+
if (existsSync(htmlPath)) {
|
|
216
|
+
const html = readFileSync(htmlPath, 'utf-8');
|
|
217
|
+
if (!/\bi18n\.js\b/.test(html)) {
|
|
218
|
+
const scriptTag = ` <script src="./i18n.js"></script>\n`;
|
|
219
|
+
let updated = html;
|
|
220
|
+
|
|
221
|
+
const firstScript = updated.match(/<script\b/i);
|
|
222
|
+
if (firstScript) {
|
|
223
|
+
updated = updated.replace(firstScript[0], scriptTag + firstScript[0]);
|
|
224
|
+
} else if (updated.includes('</body>')) {
|
|
225
|
+
updated = updated.replace('</body>', scriptTag + '</body>');
|
|
226
|
+
} else if (updated.includes('</head>')) {
|
|
227
|
+
updated = updated.replace('</head>', scriptTag + '</head>');
|
|
228
|
+
} else {
|
|
229
|
+
updated += '\n' + scriptTag;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
writeFileSync(htmlPath, updated, 'utf-8');
|
|
233
|
+
console.log(chalk.green(` ✓ Updated ${htmlPath} (added i18n.js script)`));
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Fallback: ESM entrypoint
|
|
239
|
+
const jsPath = join(cwd, 'index.js');
|
|
240
|
+
if (existsSync(jsPath)) {
|
|
241
|
+
const js = readFileSync(jsPath, 'utf-8');
|
|
242
|
+
const alreadyImports = js.includes("./i18n.js") || js.includes("'./i18n.js'") || js.includes("\"./i18n.js\"");
|
|
243
|
+
if (!alreadyImports && /\b(import|export)\b/.test(js)) {
|
|
244
|
+
writeFileSync(jsPath, `import './i18n.js';\n` + js, 'utf-8');
|
|
245
|
+
console.log(chalk.green(` ✓ Updated ${jsPath} (imported i18n.js)`));
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return false;
|
|
251
|
+
}
|
package/src/index.js
CHANGED
|
@@ -128,7 +128,7 @@ export async function writeFileResult(parseResult, translations, opts) {
|
|
|
128
128
|
export async function translateAllFiles(files, opts) {
|
|
129
129
|
const { from, to, cwd, project, verbose, localesDir } = opts;
|
|
130
130
|
|
|
131
|
-
const resolvedLocaleDir = localesDir || getLocaleDir(cwd, project);
|
|
131
|
+
const resolvedLocaleDir = opts.outdir ? join(opts.outdir, 'locales') : (localesDir || getLocaleDir(cwd, project));
|
|
132
132
|
const existingTarget = loadLocale(resolvedLocaleDir, to);
|
|
133
133
|
|
|
134
134
|
// Phase 1 — parse
|
package/src/parsers/html.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import MagicString from 'magic-string';
|
|
2
2
|
import { parse, NodeTypes } from '@vue/compiler-dom';
|
|
3
|
+
import { parseJs } from './js.js';
|
|
3
4
|
import { contextKey, isTranslatable } from '../utils.js';
|
|
4
5
|
|
|
5
6
|
const ATTR_WHITELIST = new Set([
|
|
@@ -30,7 +31,7 @@ const CONTENT_TAGS = new Set([
|
|
|
30
31
|
'td',
|
|
31
32
|
]);
|
|
32
33
|
|
|
33
|
-
const SKIP_TAGS = new Set(['
|
|
34
|
+
const SKIP_TAGS = new Set(['style', 'noscript']);
|
|
34
35
|
|
|
35
36
|
function stripTags(html) {
|
|
36
37
|
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
@@ -71,6 +72,27 @@ export function parseHtml(source, filePath, project) {
|
|
|
71
72
|
|
|
72
73
|
const openTagText = source.slice(node.loc.start.offset, openTagEnd);
|
|
73
74
|
|
|
75
|
+
// Special case: inline <script> blocks inside HTML
|
|
76
|
+
if (tag === 'script' && !node.isSelfClosing) {
|
|
77
|
+
const hasSrc = (node.props || []).some(
|
|
78
|
+
(p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'src'
|
|
79
|
+
);
|
|
80
|
+
if (hasSrc) return;
|
|
81
|
+
|
|
82
|
+
const closeRel = node.loc.source.lastIndexOf('</');
|
|
83
|
+
if (closeRel > -1) {
|
|
84
|
+
const closeTagStart = node.loc.start.offset + closeRel;
|
|
85
|
+
const scriptJs = source.slice(openTagEnd + 1, closeTagStart);
|
|
86
|
+
const jsResult = parseJs(scriptJs, filePath, project);
|
|
87
|
+
if (jsResult.modified) {
|
|
88
|
+
s.overwrite(openTagEnd + 1, closeTagStart, jsResult.source);
|
|
89
|
+
extracted.push(...jsResult.extracted);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
// 1) Extract translatable attributes
|
|
75
97
|
for (const prop of node.props || []) {
|
|
76
98
|
if (prop.type !== NodeTypes.ATTRIBUTE) continue;
|
package/src/parsers/js.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as babelParser from '@babel/parser';
|
|
2
|
-
import _traverse from '@babel/traverse';
|
|
3
|
-
import * as t from '@babel/types';
|
|
4
|
-
import MagicString from 'magic-string';
|
|
5
|
-
import { contextKey, isTranslatable } from '../utils.js';
|
|
1
|
+
import * as babelParser from '@babel/parser';
|
|
2
|
+
import _traverse from '@babel/traverse';
|
|
3
|
+
import * as t from '@babel/types';
|
|
4
|
+
import MagicString from 'magic-string';
|
|
5
|
+
import { contextKey, isTranslatable } from '../utils.js';
|
|
6
6
|
|
|
7
7
|
const traverse = _traverse.default || _traverse;
|
|
8
8
|
|
|
@@ -10,14 +10,37 @@ const traverse = _traverse.default || _traverse;
|
|
|
10
10
|
* Parse a .js/.ts file and extract translatable strings.
|
|
11
11
|
* These are strings in common UI patterns: alert(), confirm(), DOM manipulation, etc.
|
|
12
12
|
*/
|
|
13
|
-
export function parseJs(source, filePath, project) {
|
|
14
|
-
const extracted = [];
|
|
15
|
-
const isTS = filePath.endsWith('.ts');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
export function parseJs(source, filePath, project) {
|
|
14
|
+
const extracted = [];
|
|
15
|
+
const isTS = filePath.endsWith('.ts');
|
|
16
|
+
|
|
17
|
+
const tFuncFor = (key) => {
|
|
18
|
+
if (project.type === 'vanilla') return `i18n.t('${key}')`;
|
|
19
|
+
if (project.type === 'vue') return `this.$t('${key}')`;
|
|
20
|
+
return `t('${key}')`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const translatableVarNames = new Set([
|
|
24
|
+
'title', 'pageTitle', 'heading', 'subheading', 'header', 'subtitle',
|
|
25
|
+
'label', 'placeholder', 'message', 'text', 'description', 'tooltip', 'hint', 'caption',
|
|
26
|
+
'error', 'errorMessage', 'success', 'successMessage',
|
|
27
|
+
'buttonText', 'linkText',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const translatableAttrNames = new Set([
|
|
31
|
+
'title',
|
|
32
|
+
'placeholder',
|
|
33
|
+
'label',
|
|
34
|
+
'alt',
|
|
35
|
+
'aria-label',
|
|
36
|
+
'aria-placeholder',
|
|
37
|
+
'aria-description',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
let ast;
|
|
41
|
+
try {
|
|
42
|
+
ast = babelParser.parse(source, {
|
|
43
|
+
sourceType: 'module',
|
|
21
44
|
plugins: [
|
|
22
45
|
isTS ? 'typescript' : null,
|
|
23
46
|
'classProperties',
|
|
@@ -33,12 +56,32 @@ export function parseJs(source, filePath, project) {
|
|
|
33
56
|
|
|
34
57
|
const s = new MagicString(source);
|
|
35
58
|
|
|
36
|
-
// Track which string literals to translate
|
|
37
|
-
traverse(ast, {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
// Track which string literals to translate
|
|
60
|
+
traverse(ast, {
|
|
61
|
+
VariableDeclarator(path) {
|
|
62
|
+
const { id, init } = path.node;
|
|
63
|
+
if (!init) return;
|
|
64
|
+
if (id.type !== 'Identifier') return;
|
|
65
|
+
if (!translatableVarNames.has(id.name)) return;
|
|
66
|
+
|
|
67
|
+
let value = null;
|
|
68
|
+
if (init.type === 'StringLiteral') value = init.value;
|
|
69
|
+
else if (init.type === 'TemplateLiteral' && init.expressions.length === 0) {
|
|
70
|
+
value = init.quasis[0]?.value?.cooked ?? '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof value !== 'string' || !isTranslatable(value)) return;
|
|
74
|
+
|
|
75
|
+
const key = contextKey(value, filePath);
|
|
76
|
+
const tFunc = tFuncFor(key);
|
|
77
|
+
s.overwrite(init.start, init.end, tFunc);
|
|
78
|
+
extracted.push({ key, text: value, context: `js-var-${id.name}` });
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Object property values with translatable keys
|
|
82
|
+
ObjectProperty(path) {
|
|
83
|
+
const keyNode = path.node.key;
|
|
84
|
+
const valueNode = path.node.value;
|
|
42
85
|
|
|
43
86
|
if (valueNode.type !== 'StringLiteral') return;
|
|
44
87
|
if (!isTranslatable(valueNode.value)) return;
|
|
@@ -53,19 +96,19 @@ export function parseJs(source, filePath, project) {
|
|
|
53
96
|
'name', 'displayName',
|
|
54
97
|
]);
|
|
55
98
|
|
|
56
|
-
if (!translatableKeys.has(keyName)) return;
|
|
99
|
+
if (!translatableKeys.has(keyName)) return;
|
|
100
|
+
|
|
101
|
+
const key = contextKey(valueNode.value, filePath);
|
|
102
|
+
const tFunc = tFuncFor(key);
|
|
103
|
+
|
|
104
|
+
s.overwrite(valueNode.start, valueNode.end, tFunc);
|
|
105
|
+
extracted.push({ key, text: valueNode.value, context: `js-prop-${keyName}` });
|
|
106
|
+
},
|
|
57
107
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
extracted.push({ key, text: valueNode.value, context: `js-prop-${keyName}` });
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
// Function calls: alert('text'), console messages excluded
|
|
66
|
-
CallExpression(path) {
|
|
67
|
-
const callee = path.node.callee;
|
|
68
|
-
let calleeName = '';
|
|
108
|
+
// Function calls: alert('text'), console messages excluded
|
|
109
|
+
CallExpression(path) {
|
|
110
|
+
const callee = path.node.callee;
|
|
111
|
+
let calleeName = '';
|
|
69
112
|
|
|
70
113
|
if (callee.type === 'Identifier') {
|
|
71
114
|
calleeName = callee.name;
|
|
@@ -76,19 +119,35 @@ export function parseJs(source, filePath, project) {
|
|
|
76
119
|
// Skip console.*, require(), import()
|
|
77
120
|
if (calleeName.startsWith('console.') || calleeName === 'require') return;
|
|
78
121
|
|
|
79
|
-
// Translate alert/confirm/prompt first arg
|
|
80
|
-
if (['alert', 'confirm', 'prompt'].includes(calleeName)) {
|
|
81
|
-
const arg = path.node.arguments[0];
|
|
82
|
-
if (arg && arg.type === 'StringLiteral' && isTranslatable(arg.value)) {
|
|
83
|
-
const key = contextKey(arg.value, filePath);
|
|
84
|
-
const tFunc =
|
|
85
|
-
s.overwrite(arg.start, arg.end, tFunc);
|
|
86
|
-
extracted.push({ key, text: arg.value, context: 'js-call' });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// .
|
|
91
|
-
|
|
122
|
+
// Translate alert/confirm/prompt first arg
|
|
123
|
+
if (['alert', 'confirm', 'prompt'].includes(calleeName)) {
|
|
124
|
+
const arg = path.node.arguments[0];
|
|
125
|
+
if (arg && arg.type === 'StringLiteral' && isTranslatable(arg.value)) {
|
|
126
|
+
const key = contextKey(arg.value, filePath);
|
|
127
|
+
const tFunc = tFuncFor(key);
|
|
128
|
+
s.overwrite(arg.start, arg.end, tFunc);
|
|
129
|
+
extracted.push({ key, text: arg.value, context: 'js-call' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// element.setAttribute('title', '...')
|
|
134
|
+
if (callee.type === 'MemberExpression' && callee.property?.name === 'setAttribute') {
|
|
135
|
+
const [nameArg, valueArg] = path.node.arguments;
|
|
136
|
+
if (
|
|
137
|
+
nameArg?.type === 'StringLiteral' &&
|
|
138
|
+
valueArg?.type === 'StringLiteral' &&
|
|
139
|
+
translatableAttrNames.has(nameArg.value) &&
|
|
140
|
+
isTranslatable(valueArg.value)
|
|
141
|
+
) {
|
|
142
|
+
const key = contextKey(valueArg.value, filePath);
|
|
143
|
+
const tFunc = tFuncFor(key);
|
|
144
|
+
s.overwrite(valueArg.start, valueArg.end, tFunc);
|
|
145
|
+
extracted.push({ key, text: valueArg.value, context: `js-attr-${nameArg.value}` });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// .textContent = 'text', .innerText = 'text', .title = 'text'
|
|
150
|
+
},
|
|
92
151
|
|
|
93
152
|
// Assignment: element.textContent = 'text'
|
|
94
153
|
AssignmentExpression(path) {
|
|
@@ -105,18 +164,14 @@ export function parseJs(source, filePath, project) {
|
|
|
105
164
|
'alt', 'innerHTML',
|
|
106
165
|
]);
|
|
107
166
|
|
|
108
|
-
if (domProps.has(propName)) {
|
|
109
|
-
const key = contextKey(right.value, filePath);
|
|
110
|
-
const tFunc =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
s.overwrite(right.start, right.end, tFunc);
|
|
117
|
-
extracted.push({ key, text: right.value, context: `js-dom-${propName}` });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
167
|
+
if (domProps.has(propName)) {
|
|
168
|
+
const key = contextKey(right.value, filePath);
|
|
169
|
+
const tFunc = tFuncFor(key);
|
|
170
|
+
|
|
171
|
+
s.overwrite(right.start, right.end, tFunc);
|
|
172
|
+
extracted.push({ key, text: right.value, context: `js-dom-${propName}` });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
120
175
|
},
|
|
121
176
|
});
|
|
122
177
|
|
|
@@ -125,4 +180,4 @@ export function parseJs(source, filePath, project) {
|
|
|
125
180
|
extracted,
|
|
126
181
|
modified: extracted.length > 0,
|
|
127
182
|
};
|
|
128
|
-
}
|
|
183
|
+
}
|
package/src/parsers/vue.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as compiler from '@vue/compiler-dom';
|
|
2
|
+
import { parse as babelParse } from '@babel/parser';
|
|
3
|
+
import _traverse from '@babel/traverse';
|
|
4
|
+
const traverse = _traverse.default;
|
|
2
5
|
import MagicString from 'magic-string';
|
|
3
6
|
import { contextKey, isTranslatable } from '../utils.js';
|
|
4
7
|
|
|
@@ -30,14 +33,6 @@ const ATTR_WHITELIST = new Set([
|
|
|
30
33
|
* - Options API (no setup)
|
|
31
34
|
* → use $t('key') (global plugin property)
|
|
32
35
|
*/
|
|
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
36
|
|
|
42
37
|
/**
|
|
43
38
|
* Parse a .vue file and extract translatable strings.
|
|
@@ -46,12 +41,9 @@ export function parseVue(source, filePath) {
|
|
|
46
41
|
const extracted = [];
|
|
47
42
|
const s = new MagicString(source);
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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}')`;
|
|
44
|
+
// Always use t() for consistency, as promised in the README.
|
|
45
|
+
const tpl = (key) => `t('${key}')`;
|
|
46
|
+
const scr = (key) => `t('${key}')`;
|
|
55
47
|
|
|
56
48
|
// ── Template ────────────────────────────────────────────────────────────────
|
|
57
49
|
const templateMatch = source.match(/<template\b[^>]*>([\s\S]*?)<\/template>/);
|
|
@@ -77,12 +69,13 @@ export function parseVue(source, filePath) {
|
|
|
77
69
|
extractScriptStrings(scriptContent, s, scriptOffset, extracted, filePath, scr);
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
// ── Inject `const { t } = useI18n()` if
|
|
81
|
-
if (extracted.length > 0
|
|
72
|
+
// ── Inject `const { t } = useI18n()` if not yet declared ────────
|
|
73
|
+
if (extracted.length > 0) {
|
|
82
74
|
const scriptSetupMatch = source.match(/(<script\b[^>]*\bsetup\b[^>]*>)([\s\S]*?)<\/script>/i);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
const hasT = /const\s*\{[^}]*\bt\b[^}]*\}\s*=/.test(source);
|
|
76
|
+
|
|
77
|
+
if (scriptSetupMatch && !hasT) {
|
|
78
|
+
const insertAt = source.indexOf(scriptSetupMatch[0]) + scriptSetupMatch[1].length;
|
|
86
79
|
const needsImport = !source.includes('useI18n');
|
|
87
80
|
const importLine = needsImport ? `import { useI18n } from 'vue-i18n';\n` : '';
|
|
88
81
|
s.appendRight(insertAt, `\n${importLine}const { t } = useI18n();\n`);
|
|
@@ -155,64 +148,87 @@ function walkTemplate(nodes, s, baseOffset, extracted, filePath, tpl) {
|
|
|
155
148
|
// ── Script string extractor ───────────────────────────────────────────────────
|
|
156
149
|
|
|
157
150
|
function extractScriptStrings(scriptContent, s, baseOffset, extracted, filePath, scr) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
try {
|
|
152
|
+
const ast = babelParse(scriptContent, {
|
|
153
|
+
sourceType: 'module',
|
|
154
|
+
plugins: ['typescript', 'jsx'], // Enable TS and JSX support
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const replacements = [];
|
|
158
|
+
|
|
159
|
+
traverse(ast, {
|
|
160
|
+
enter(path) {
|
|
161
|
+
let text;
|
|
162
|
+
|
|
163
|
+
if (path.isStringLiteral()) {
|
|
164
|
+
text = path.node.value;
|
|
165
|
+
} else if (path.isTemplateLiteral()) {
|
|
166
|
+
if (path.node.expressions.length > 0 || path.node.quasis.length !== 1) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
text = path.node.quasis[0].value.cooked;
|
|
170
|
+
} else {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!isTranslatable(text)) return;
|
|
175
|
+
|
|
176
|
+
// --- Parent checks to avoid replacing the wrong strings ---
|
|
177
|
+
if (path.parent.type === 'CallExpression' && path.parent.callee.name === 't') return;
|
|
178
|
+
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(path.parent.type)) return;
|
|
179
|
+
if (path.parent.type === 'ObjectProperty' && path.parent.key === path.node) return;
|
|
180
|
+
if (path.parent.type === 'Property' && ['name'].includes(path.parent.key.name)) return;
|
|
181
|
+
|
|
182
|
+
// --- Passed all checks, schedule replacement ---
|
|
183
|
+
const key = contextKey(text, filePath);
|
|
184
|
+
const start = baseOffset + path.node.start;
|
|
185
|
+
const end = baseOffset + path.node.end;
|
|
186
|
+
replacements.push({ start, end, key });
|
|
187
|
+
extracted.push({ key, text, context: 'script' });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Apply replacements in reverse order to avoid offset issues
|
|
192
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
193
|
+
const { start, end, key } = replacements[i];
|
|
194
|
+
s.overwrite(start, end, scr(key));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error('Babel parsing failed:', e);
|
|
199
|
+
// If babel parsing fails, fallback to regex.
|
|
200
|
+
extractScriptStringsRegex(scriptContent, s, baseOffset, extracted, filePath, scr);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
function extractScriptStringsRegex(scriptContent, s, baseOffset, extracted, filePath, scr) {
|
|
206
|
+
// This is the old regex-based implementation, kept as a fallback.
|
|
207
|
+
const patterns = [
|
|
161
208
|
/\b(alert|confirm|toast|notify|message\.(?:success|error|warning|info))\s*\(\s*(['"`])((?:(?!\2).)+)\2\s*\)/g,
|
|
162
209
|
/\b(title|label|placeholder|message|text|description|tooltip|hint|caption|header|subtitle|errorMessage|successMessage|emptyText|noData|loadingText|buttonText|confirmText|cancelText|successText|failText|warningText|helperText|hintText)\s*:\s*(['"`])((?:(?!\2).)+)\2/g,
|
|
210
|
+
/\bref\s*\(\s*(['"`])((?:(?!\1)[^\\]|\\.)+)\1\s*\)/g,
|
|
211
|
+
/\bcomputed\s*\(\s*\(\s*\)\s*=>\s*(['"`])((?:(?!\1)[^\\]|\\.)+)\1\s*\)/g,
|
|
163
212
|
];
|
|
164
213
|
|
|
165
|
-
for (const pattern of
|
|
214
|
+
for (const pattern of patterns) {
|
|
166
215
|
let match;
|
|
167
216
|
while ((match = pattern.exec(scriptContent)) !== null) {
|
|
168
|
-
const text = match[3];
|
|
217
|
+
const text = match[3] || match[2];
|
|
169
218
|
if (!isTranslatable(text) || text.length <= 1) continue;
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const textStart = baseOffset + relPos;
|
|
175
|
-
const textEnd = textStart + text.length + 2;
|
|
176
|
-
s.overwrite(textStart, textEnd, scr(key));
|
|
177
|
-
extracted.push({ key, text, context: 'script' });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
219
|
+
|
|
220
|
+
const quoteChar = match[2] || match[1];
|
|
221
|
+
const innerStr = quoteChar + text + quoteChar;
|
|
222
|
+
const relPos = match.index + match[0].lastIndexOf(innerStr);
|
|
180
223
|
|
|
181
|
-
// Pattern C: ref('string') / ref("string") → match[1]=quote, match[2]=text
|
|
182
|
-
const refPattern = /\bref\s*\(\s*(['"`])((?:(?!\1)[^\\]|\\.)+)\1\s*\)/g;
|
|
183
|
-
{
|
|
184
|
-
let match;
|
|
185
|
-
while ((match = refPattern.exec(scriptContent)) !== null) {
|
|
186
|
-
const text = match[2];
|
|
187
|
-
if (!isTranslatable(text) || text.length <= 1) continue;
|
|
188
|
-
const quoteChar = match[1];
|
|
189
|
-
const innerStr = quoteChar + text + quoteChar;
|
|
190
|
-
const relPos = match.index + match[0].indexOf(innerStr);
|
|
191
224
|
if (isAlreadyWrappedScript(scriptContent, relPos)) continue;
|
|
192
|
-
const key = contextKey(text, filePath);
|
|
193
|
-
const textStart = baseOffset + relPos;
|
|
194
|
-
const textEnd = textStart + innerStr.length;
|
|
195
|
-
s.overwrite(textStart, textEnd, scr(key));
|
|
196
|
-
extracted.push({ key, text, context: 'script-ref' });
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
225
|
|
|
200
|
-
|
|
201
|
-
const computedPattern = /\bcomputed\s*\(\s*\(\s*\)\s*=>\s*(['"`])((?:(?!\1)[^\\]|\\.)+)\1\s*\)/g;
|
|
202
|
-
{
|
|
203
|
-
let match;
|
|
204
|
-
while ((match = computedPattern.exec(scriptContent)) !== null) {
|
|
205
|
-
const text = match[2];
|
|
206
|
-
if (!isTranslatable(text) || text.length <= 1) continue;
|
|
207
|
-
const quoteChar = match[1];
|
|
208
|
-
const innerStr = quoteChar + text + quoteChar;
|
|
209
|
-
const relPos = match.index + match[0].indexOf(innerStr);
|
|
210
|
-
if (isAlreadyWrappedScript(scriptContent, relPos)) continue;
|
|
211
|
-
const key = contextKey(text, filePath);
|
|
226
|
+
const key = contextKey(text, filePath);
|
|
212
227
|
const textStart = baseOffset + relPos;
|
|
213
|
-
const textEnd
|
|
228
|
+
const textEnd = textStart + innerStr.length;
|
|
229
|
+
|
|
214
230
|
s.overwrite(textStart, textEnd, scr(key));
|
|
215
|
-
extracted.push({ key, text, context: 'script-
|
|
231
|
+
extracted.push({ key, text, context: 'script-regex' });
|
|
216
232
|
}
|
|
217
233
|
}
|
|
218
234
|
}
|
|
@@ -222,12 +238,14 @@ function isAlreadyWrappedScript(scriptContent, pos) {
|
|
|
222
238
|
return /\$?t\s*\(\s*$/.test(before);
|
|
223
239
|
}
|
|
224
240
|
|
|
241
|
+
|
|
225
242
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
226
243
|
|
|
227
244
|
function isAlreadyWrapped(source, start, end) {
|
|
228
|
-
// Look back 25 chars for an open t(
|
|
245
|
+
// Look back 25 chars for an open t( call — node is inside an interpolation
|
|
229
246
|
const before = source.slice(Math.max(0, start - 25), start);
|
|
230
|
-
|
|
247
|
+
// Note: We only check for `t` now, not `$t`.
|
|
248
|
+
return /t\s*\(\s*['"]/.test(before);
|
|
231
249
|
}
|
|
232
250
|
|
|
233
251
|
function extractTemplateRegex(source, s, extracted, filePath, tpl) {
|
package/python/t2.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
# Suppress TensorFlow logs
|
|
6
|
-
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
import argostranslate.package
|
|
10
|
-
import argostranslate.translate
|
|
11
|
-
except ImportError:
|
|
12
|
-
print(json.dumps({"error": "argostranslate not found. Please run: pip install argostranslate"}), file=sys.stderr)
|
|
13
|
-
sys.exit(1)
|
|
14
|
-
|
|
15
|
-
def main():
|
|
16
|
-
if len(sys.argv) != 3:
|
|
17
|
-
print(json.dumps({"error": "Usage: python translator.py <from_code> <to_code>"}), file=sys.stderr)
|
|
18
|
-
sys.exit(1)
|
|
19
|
-
|
|
20
|
-
from_code = sys.argv[1]
|
|
21
|
-
to_code = sys.argv[2]
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
input_data = sys.stdin.read()
|
|
25
|
-
batch = json.loads(input_data)
|
|
26
|
-
except json.JSONDecodeError:
|
|
27
|
-
print(json.dumps({"error": "Invalid JSON input"}), file=sys.stderr)
|
|
28
|
-
sys.exit(1)
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
# 1. Find the installed languages
|
|
32
|
-
installed_languages = argostranslate.translate.get_installed_languages()
|
|
33
|
-
from_lang = next((lang for lang in installed_languages if lang.code == from_code), None)
|
|
34
|
-
to_lang = next((lang for lang in installed_languages if lang.code == to_code), None)
|
|
35
|
-
|
|
36
|
-
if not from_lang or not to_lang:
|
|
37
|
-
# This should ideally be handled by the check in the Node.js bridge,
|
|
38
|
-
# but as a fallback, we report it here too.
|
|
39
|
-
available_codes = [l.code for l in installed_languages]
|
|
40
|
-
print(json.dumps({
|
|
41
|
-
"error": f"Language pair not installed: {from_code}->{to_code}. Installed: {available_codes}"
|
|
42
|
-
}), file=sys.stderr)
|
|
43
|
-
sys.exit(1)
|
|
44
|
-
|
|
45
|
-
# 2. Get the translation object
|
|
46
|
-
translation = from_lang.get_translation(to_lang)
|
|
47
|
-
if not translation:
|
|
48
|
-
# This may happen if the translation direction is not supported (e.g., en->en)
|
|
49
|
-
if from_code == to_code:
|
|
50
|
-
# If source and target are the same, just return the original text
|
|
51
|
-
print(json.dumps(batch))
|
|
52
|
-
sys.exit(0)
|
|
53
|
-
else:
|
|
54
|
-
print(json.dumps({"error": f"Translation from {from_code} to {to_code} is not supported by the installed model."}), file=sys.stderr)
|
|
55
|
-
sys.exit(1)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# 3. Translate texts
|
|
59
|
-
translated_batch = []
|
|
60
|
-
for item in batch:
|
|
61
|
-
original_text = item.get('text', '')
|
|
62
|
-
translated_text = translation.translate(original_text)
|
|
63
|
-
translated_batch.append({
|
|
64
|
-
"key": item.get('key'),
|
|
65
|
-
"text": translated_text
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
# 4. Output the result as a JSON array
|
|
69
|
-
print(json.dumps(translated_batch, ensure_ascii=False))
|
|
70
|
-
|
|
71
|
-
except Exception as e:
|
|
72
|
-
print(json.dumps({"error": f"An unexpected error occurred: {str(e)}"}), file=sys.stderr)
|
|
73
|
-
sys.exit(1)
|
|
74
|
-
|
|
75
|
-
if __name__ == "__main__":
|
|
76
|
-
main()
|