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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # bctranslate ⚡
2
2
 
3
- # Notice: This app is in Beta mode, work in progress; currently works for html files, contribution is welcome at: [BCtranslate github repo](https://github.com/mrkinix/bctranslate)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bctranslate",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
4
4
  "description": "CLI to transform source files into i18n-ready code with automatic translation via argostranslate",
5
5
  "type": "module",
6
6
  "bin": {
@@ -78,6 +78,7 @@ function nestMessages(input) {
78
78
 
79
79
  const i18n = createI18n({
80
80
  legacy: false,
81
+ globalInjection: true,
81
82
  locale: '${from}',
82
83
  fallbackLocale: '${from}',
83
84
  messages: {
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) {
@@ -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
- const tpl = (key) => (templatePrefersT ? `t('${key}')` : `$t('${key}')`);
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
- traverse(ast, {
174
- enter(path) {
175
- let text;
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 (!isTranslatable(text)) return;
189
-
190
- // --- Parent checks to avoid replacing the wrong strings ---
191
- if (path.parent.type === 'CallExpression' && path.parent.callee.name === 't') return;
192
- if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(path.parent.type)) return;
193
- if (path.parent.type === 'ObjectProperty' && path.parent.key === path.node) return;
194
- if (path.parent.type === 'Property' && ['name'].includes(path.parent.key.name)) return;
195
-
196
- // --- Passed all checks, schedule replacement ---
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 });