bctranslate 1.0.3 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bctranslate",
3
- "version": "1.0.3",
3
+ "version": "1.0.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": {
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
@@ -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
- 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}')`;
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 setup but t not yet declared ────────
81
- if (extracted.length > 0 && isSetup && !hasT) {
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
- if (scriptSetupMatch) {
84
- const insertAt =
85
- source.indexOf(scriptSetupMatch[0]) + scriptSetupMatch[1].length;
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
- // Pattern A: alert/confirm/toast/notify calls → match[2]=quote, match[3]=text
159
- // Pattern B: UI object property string values → match[2]=quote, match[3]=text
160
- const patternsAB = [
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 patternsAB) {
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
- const quoteChar = match[2];
171
- const relPos = match.index + match[0].indexOf(quoteChar + text);
172
- if (isAlreadyWrappedScript(scriptContent, relPos)) continue;
173
- const key = contextKey(text, filePath);
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
- // Pattern D: computed(() => 'string') → match[1]=quote, match[2]=text
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 = textStart + innerStr.length;
228
+ const textEnd = textStart + innerStr.length;
229
+
214
230
  s.overwrite(textStart, textEnd, scr(key));
215
- extracted.push({ key, text, context: 'script-computed' });
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( or $t( call — node is inside an interpolation
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
- return /\$?t\s*\(\s*['"]/.test(before);
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()