bctranslate 1.0.0-beta.2 → 1.0.0-beta.4
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 +1 -1
- package/package.json +1 -1
- package/src/generators/setup.js +1 -0
- package/src/parsers/js.js +33 -0
- package/src/parsers/vue.js +146 -18
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# bctranslate ⚡
|
|
2
2
|
|
|
3
|
-
# Notice: This app is in Beta mode, work in progress;
|
|
3
|
+
# Notice: This app is in Beta mode, work in progress; the tool works but necessitates user intervention to fix some bugs and alter directories and imports, contribution is welcome at: [BCtranslate github repo](https://github.com/mrkinix/bctranslate)
|
|
4
4
|
|
|
5
5
|
`bctranslate` is a command-line tool to automatically transform source code into an i18n-ready format. It extracts hardcoded strings from your files, replaces them with calls to a translation function, and generates locale files with translations powered by [Argos Translate](https://www.argosopentech.com/).
|
|
6
6
|
|
package/package.json
CHANGED
package/src/generators/setup.js
CHANGED
package/src/parsers/js.js
CHANGED
|
@@ -20,6 +20,10 @@ export function parseJs(source, filePath, project) {
|
|
|
20
20
|
return `t('${key}')`;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const arrayKeys = new Set([
|
|
24
|
+
'options', 'items', 'labels', 'actions', 'tabs', 'menu', 'choices', 'buttons', 'breadcrumbs',
|
|
25
|
+
]);
|
|
26
|
+
|
|
23
27
|
const translatableAttrNames = new Set([
|
|
24
28
|
'title',
|
|
25
29
|
'placeholder',
|
|
@@ -77,6 +81,35 @@ export function parseJs(source, filePath, project) {
|
|
|
77
81
|
s.overwrite(valueNode.start, valueNode.end, tFunc);
|
|
78
82
|
extracted.push({ key, text: valueNode.value, context: `js-prop-${keyName}` });
|
|
79
83
|
},
|
|
84
|
+
|
|
85
|
+
ArrayExpression(path) {
|
|
86
|
+
const parent = path.parent;
|
|
87
|
+
if (!parent) return;
|
|
88
|
+
|
|
89
|
+
// Translate arrays of strings only when clearly UI-ish, e.g. { options: ['A','B'] }
|
|
90
|
+
if (parent.type === 'ObjectProperty' && parent.value === path.node) {
|
|
91
|
+
const keyNode = parent.key;
|
|
92
|
+
const keyName = keyNode.name || keyNode.value || '';
|
|
93
|
+
if (!arrayKeys.has(keyName) && !translatableKeys.has(keyName)) return;
|
|
94
|
+
|
|
95
|
+
for (const el of path.node.elements) {
|
|
96
|
+
if (!el) continue;
|
|
97
|
+
if (el.type === 'StringLiteral' && isTranslatable(el.value)) {
|
|
98
|
+
const key = contextKey(el.value, filePath);
|
|
99
|
+
const tFunc = tFuncFor(key);
|
|
100
|
+
s.overwrite(el.start, el.end, tFunc);
|
|
101
|
+
extracted.push({ key, text: el.value, context: `js-array-${keyName}` });
|
|
102
|
+
} else if (el.type === 'TemplateLiteral' && el.expressions.length === 0) {
|
|
103
|
+
const value = el.quasis[0]?.value?.cooked ?? '';
|
|
104
|
+
if (!isTranslatable(value)) continue;
|
|
105
|
+
const key = contextKey(value, filePath);
|
|
106
|
+
const tFunc = tFuncFor(key);
|
|
107
|
+
s.overwrite(el.start, el.end, tFunc);
|
|
108
|
+
extracted.push({ key, text: value, context: `js-array-${keyName}` });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
80
113
|
|
|
81
114
|
// Function calls: alert('text'), console messages excluded
|
|
82
115
|
CallExpression(path) {
|
package/src/parsers/vue.js
CHANGED
|
@@ -50,7 +50,12 @@ export function parseVue(source, filePath) {
|
|
|
50
50
|
const templatePrefersDollarT = /\$t\s*\(/.test(templateBlock);
|
|
51
51
|
const templatePrefersT = !templatePrefersDollarT && /\bt\s*\(/.test(templateBlock);
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// Safer default: in <script setup>, `t()` can always be injected; `$t` depends on globalInjection/legacy.
|
|
54
|
+
const tpl = (key) => {
|
|
55
|
+
if (templatePrefersDollarT) return `$t('${key}')`;
|
|
56
|
+
if (templatePrefersT) return `t('${key}')`;
|
|
57
|
+
return hasScriptSetup ? `t('${key}')` : `$t('${key}')`;
|
|
58
|
+
};
|
|
54
59
|
const scr = (key) => (hasScriptSetup ? `t('${key}')` : `this.$t('${key}')`);
|
|
55
60
|
|
|
56
61
|
// ── Template ────────────────────────────────────────────────────────────────
|
|
@@ -161,18 +166,142 @@ function walkTemplate(nodes, s, baseOffset, extracted, filePath, tpl) {
|
|
|
161
166
|
|
|
162
167
|
// ── Script string extractor ───────────────────────────────────────────────────
|
|
163
168
|
|
|
164
|
-
function extractScriptStrings(scriptContent, s, baseOffset, extracted, filePath, scr) {
|
|
165
|
-
try {
|
|
169
|
+
function extractScriptStrings(scriptContent, s, baseOffset, extracted, filePath, scr) {
|
|
170
|
+
try {
|
|
166
171
|
const ast = babelParse(scriptContent, {
|
|
167
172
|
sourceType: 'module',
|
|
168
173
|
plugins: ['typescript', 'jsx'], // Enable TS and JSX support
|
|
169
174
|
});
|
|
170
175
|
|
|
171
|
-
const replacements = [];
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const replacements = [];
|
|
177
|
+
|
|
178
|
+
const translatableKeys = new Set([
|
|
179
|
+
'title', 'label', 'placeholder', 'message', 'text',
|
|
180
|
+
'description', 'tooltip', 'hint', 'caption', 'header',
|
|
181
|
+
'subtitle', 'errorMessage', 'successMessage', 'content',
|
|
182
|
+
'heading', 'subheading', 'buttonText', 'linkText',
|
|
183
|
+
'emptyText', 'noData', 'loadingText', 'confirmText',
|
|
184
|
+
'cancelText', 'successText', 'failText', 'warningText',
|
|
185
|
+
'helperText', 'hintText',
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
const arrayKeys = new Set([
|
|
189
|
+
'options', 'items', 'labels', 'actions', 'tabs', 'menu', 'choices', 'buttons', 'breadcrumbs',
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const skipCallees = new Set([
|
|
193
|
+
'defineEmits',
|
|
194
|
+
'defineProps',
|
|
195
|
+
'defineExpose',
|
|
196
|
+
'withDefaults',
|
|
197
|
+
'emit',
|
|
198
|
+
'$emit',
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
const skipMemberCallees = new Set([
|
|
202
|
+
'localStorage.getItem',
|
|
203
|
+
'localStorage.setItem',
|
|
204
|
+
'localStorage.removeItem',
|
|
205
|
+
'sessionStorage.getItem',
|
|
206
|
+
'sessionStorage.setItem',
|
|
207
|
+
'sessionStorage.removeItem',
|
|
208
|
+
'document.querySelector',
|
|
209
|
+
'document.querySelectorAll',
|
|
210
|
+
'document.getElementById',
|
|
211
|
+
'document.getElementsByClassName',
|
|
212
|
+
'document.getElementsByTagName',
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
const allowCallees = new Set(['alert', 'confirm', 'prompt', 'toast', 'notify']);
|
|
216
|
+
|
|
217
|
+
const getKeyName = (keyNode) => {
|
|
218
|
+
if (!keyNode) return '';
|
|
219
|
+
if (keyNode.type === 'Identifier') return keyNode.name;
|
|
220
|
+
if (keyNode.type === 'StringLiteral') return keyNode.value;
|
|
221
|
+
return '';
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const calleeName = (callee) => {
|
|
225
|
+
if (!callee) return '';
|
|
226
|
+
if (callee.type === 'Identifier') return callee.name;
|
|
227
|
+
if (callee.type === 'MemberExpression' && callee.property) {
|
|
228
|
+
const obj = callee.object?.name || '';
|
|
229
|
+
const prop = callee.property?.name || callee.property?.value || '';
|
|
230
|
+
return obj && prop ? `${obj}.${prop}` : prop;
|
|
231
|
+
}
|
|
232
|
+
return '';
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const isInSkippedCall = (p) => {
|
|
236
|
+
const call = p.findParent((pp) => pp.isCallExpression());
|
|
237
|
+
if (!call) return false;
|
|
238
|
+
const name = calleeName(call.node.callee);
|
|
239
|
+
return skipCallees.has(name) || skipMemberCallees.has(name);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const isTSOnly = (p) =>
|
|
243
|
+
!!p.findParent((pp) =>
|
|
244
|
+
pp.isTSTypeAnnotation?.() ||
|
|
245
|
+
pp.isTSLiteralType?.() ||
|
|
246
|
+
pp.isTSUnionType?.() ||
|
|
247
|
+
pp.isTSInterfaceDeclaration?.() ||
|
|
248
|
+
pp.isTSTypeAliasDeclaration?.()
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const shouldTranslate = (p, text) => {
|
|
252
|
+
if (!isTranslatable(text)) return false;
|
|
253
|
+
if (isTSOnly(p)) return false;
|
|
254
|
+
|
|
255
|
+
// Never touch selectors / computed property names: obj['key']
|
|
256
|
+
if (p.parent?.type === 'MemberExpression' && p.parent.computed && p.parent.property === p.node) return false;
|
|
257
|
+
|
|
258
|
+
// Skip event/prop definitions and other internal identifiers
|
|
259
|
+
if (isInSkippedCall(p)) return false;
|
|
260
|
+
|
|
261
|
+
// Skip already wrapped
|
|
262
|
+
if (p.parent?.type === 'CallExpression' && (calleeName(p.parent.callee) === 't' || calleeName(p.parent.callee) === '$t')) return false;
|
|
263
|
+
|
|
264
|
+
// UI-ish object properties
|
|
265
|
+
if (p.parent?.type === 'ObjectProperty' && p.parent.value === p.node) {
|
|
266
|
+
const key = getKeyName(p.parent.key);
|
|
267
|
+
if (translatableKeys.has(key)) return true;
|
|
268
|
+
|
|
269
|
+
// Arrays of visible labels/options
|
|
270
|
+
if (
|
|
271
|
+
(p.parent.value?.type === 'ArrayExpression' && (arrayKeys.has(key) || translatableKeys.has(key)))
|
|
272
|
+
) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Arrays of strings (translate only when clearly UI lists)
|
|
280
|
+
if (p.parent?.type === 'ArrayExpression') {
|
|
281
|
+
const gp = p.parentPath?.parent;
|
|
282
|
+
if (gp?.type === 'ObjectProperty' && gp.value === p.parent) {
|
|
283
|
+
const key = getKeyName(gp.key);
|
|
284
|
+
if (arrayKeys.has(key) || translatableKeys.has(key)) return true;
|
|
285
|
+
}
|
|
286
|
+
if (gp?.type === 'VariableDeclarator' && gp.id?.type === 'Identifier') {
|
|
287
|
+
const name = gp.id.name;
|
|
288
|
+
if (arrayKeys.has(name) || name.endsWith('Options') || name.endsWith('Labels')) return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Allow common UI calls (alert/toast/etc)
|
|
294
|
+
if (p.parent?.type === 'CallExpression') {
|
|
295
|
+
const name = calleeName(p.parent.callee);
|
|
296
|
+
if (allowCallees.has(name)) return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return false;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
traverse(ast, {
|
|
303
|
+
enter(path) {
|
|
304
|
+
let text;
|
|
176
305
|
|
|
177
306
|
if (path.isStringLiteral()) {
|
|
178
307
|
text = path.node.value;
|
|
@@ -185,16 +314,15 @@ function extractScriptStrings(scriptContent, s, baseOffset, extracted, filePath,
|
|
|
185
314
|
return;
|
|
186
315
|
}
|
|
187
316
|
|
|
188
|
-
if (!
|
|
189
|
-
|
|
190
|
-
// --- Parent checks to avoid replacing the wrong strings ---
|
|
191
|
-
if (
|
|
192
|
-
if (
|
|
193
|
-
if (path.parent.type === '
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const key = contextKey(text, filePath);
|
|
317
|
+
if (!shouldTranslate(path, text)) return;
|
|
318
|
+
|
|
319
|
+
// --- Parent checks to avoid replacing the wrong strings ---
|
|
320
|
+
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(path.parent.type)) return;
|
|
321
|
+
if (path.parent.type === 'ObjectProperty' && path.parent.key === path.node) return;
|
|
322
|
+
if (path.parent.type === 'Property' && ['name'].includes(path.parent.key.name)) return;
|
|
323
|
+
|
|
324
|
+
// --- Passed all checks, schedule replacement ---
|
|
325
|
+
const key = contextKey(text, filePath);
|
|
198
326
|
const start = baseOffset + path.node.start;
|
|
199
327
|
const end = baseOffset + path.node.end;
|
|
200
328
|
replacements.push({ start, end, key });
|