bctranslate 1.0.0

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 ADDED
@@ -0,0 +1,88 @@
1
+ # bctranslate ⚡
2
+
3
+ `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/).
4
+
5
+ It's designed to be a quick and easy way to "bake in" internationalization into a project with minimal refactoring.
6
+
7
+ ## Features
8
+
9
+ - **Automatic String Extraction:** Finds and extracts hardcoded strings from various file types.
10
+ - **Code Transformation:** Replaces extracted strings with i18n function calls (e.g., `t('key')`).
11
+ - **Machine Translation:** Uses Python and Argos Translate to provide instant translations for extracted strings.
12
+ - **Project-Aware:** Automatically detects project types (Vue, React, Vanilla JS/HTML) to apply the correct parsing and transformation rules.
13
+ - **Framework Support:**
14
+ - Vue (`.vue`, `.js`, `.ts`)
15
+ - React (`.jsx`, `.tsx`, `.js`, `.ts`)
16
+ - Vanilla JS and HTML
17
+ - JSON (`.json`)
18
+ - **Dry Run Mode:** Preview all changes without modifying any files.
19
+
20
+ ## Prerequisites
21
+
22
+ 1. **Node.js:** Requires Node.js version 18.0.0 or higher.
23
+ 2. **Python:** Requires Python 3.8 or higher.
24
+ 3. **Argos Translate:** The `argostranslate` Python package must be installed.
25
+ ```sh
26
+ pip install argostranslate
27
+ ```
28
+
29
+ ## Installation
30
+
31
+ You can install `bctranslate` globally via npm:
32
+
33
+ ```sh
34
+ npm install -g .
35
+ ```
36
+
37
+ This will make the `bctranslate` command available in your terminal.
38
+
39
+ ## Usage
40
+
41
+ The most common use case is to run `bctranslate` in the root of your project to automatically detect and process all relevant files.
42
+
43
+ ```sh
44
+ bctranslate
45
+ ```
46
+
47
+ ### Command-Line Options
48
+
49
+ ```
50
+ bctranslate [file] [from] [options]
51
+ ```
52
+
53
+ **Arguments:**
54
+
55
+ - `[file]`: (Optional) A specific file or glob pattern to process. If omitted, the tool auto-detects files based on the project type.
56
+ - `[from]`: (Optional) The source language code for translation (default: `en`).
57
+
58
+ **Options:**
59
+
60
+ | Option | Description | Default |
61
+ | ---------------------- | ---------------------------------------------------------------------------- | ---------- |
62
+ | `-t, --to <lang>` | Target language code for translation. | `fr` |
63
+ | `-d, --dry-run` | Preview changes without writing to files. | `false` |
64
+ | `-o, --outdir <dir>` | Output directory for new locale files (default: in-place). | |
65
+ | `--no-setup` | Skip the generation of i18n setup files (e.g., `i18n.js`). | |
66
+ | `--json-mode <mode>` | For `.json` files, translate `values` or the `full` file structure. | `values` |
67
+ | `-v, --verbose` | Enable verbose logging for debugging. | `false` |
68
+ | `-h, --help` | Display help for the command. | |
69
+
70
+ ### Examples
71
+
72
+ **Auto-translate a whole project from English to French:**
73
+
74
+ ```sh
75
+ bctranslate --to fr
76
+ ```
77
+
78
+ **Translate a single HTML file from English to Spanish:**
79
+
80
+ ```sh
81
+ bctranslate "src/index.html" en -t es
82
+ ```
83
+
84
+ **Preview changes for all Vue files without modifying them:**
85
+
86
+ ```sh
87
+ bctranslate "src/**/*.vue" --dry-run
88
+ ```
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { resolve, basename, dirname } from 'path';
6
+ import { existsSync, statSync } from 'fs';
7
+ import { glob } from 'glob';
8
+ import { translateFile } from '../src/index.js';
9
+ import { detectProject } from '../src/detect.js';
10
+ import { ensureI18nSetup } from '../src/generators/setup.js';
11
+ import { checkPythonBridge } from '../src/bridges/python.js';
12
+ import { loadConfig, saveConfig } from '../src/config.js';
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('bctranslate')
18
+ .description('Transform source files into i18n-ready code with automatic translation')
19
+ .version('1.0.0');
20
+
21
+ // ── Common languages for init prompts ────────────────────────────────────────
22
+ const COMMON_LANGUAGES = [
23
+ { name: 'French (fr)', value: 'fr' },
24
+ { name: 'Spanish (es)', value: 'es' },
25
+ { name: 'German (de)', value: 'de' },
26
+ { name: 'Italian (it)', value: 'it' },
27
+ { name: 'Portuguese (pt)', value: 'pt' },
28
+ { name: 'Dutch (nl)', value: 'nl' },
29
+ { name: 'Russian (ru)', value: 'ru' },
30
+ { name: 'Chinese (zh)', value: 'zh' },
31
+ { name: 'Japanese (ja)', value: 'ja' },
32
+ { name: 'Korean (ko)', value: 'ko' },
33
+ { name: 'Arabic (ar)', value: 'ar' },
34
+ { name: 'Turkish (tr)', value: 'tr' },
35
+ { name: 'Polish (pl)', value: 'pl' },
36
+ { name: 'Swedish (sv)', value: 'sv' },
37
+ { name: 'Norwegian (nb)', value: 'nb' },
38
+ { name: 'Danish (da)', value: 'da' },
39
+ { name: 'Finnish (fi)', value: 'fi' },
40
+ { name: 'Czech (cs)', value: 'cs' },
41
+ { name: 'Romanian (ro)', value: 'ro' },
42
+ { name: 'Hungarian (hu)', value: 'hu' },
43
+ ];
44
+
45
+ const SOURCE_LANGUAGES = [
46
+ { name: 'English (en)', value: 'en' },
47
+ { name: 'French (fr)', value: 'fr' },
48
+ { name: 'Spanish (es)', value: 'es' },
49
+ { name: 'German (de)', value: 'de' },
50
+ { name: 'Italian (it)', value: 'it' },
51
+ { name: 'Portuguese(pt)', value: 'pt' },
52
+ { name: 'Chinese (zh)', value: 'zh' },
53
+ { name: 'Other (type below)', value: '__other__' },
54
+ ];
55
+
56
+ // ── File patterns per project type ───────────────────────────────────────────
57
+ const FILE_PATTERNS = {
58
+ vue: ['**/*.vue', 'src/**/*.js', 'src/**/*.ts'],
59
+ react: ['**/*.jsx', '**/*.tsx', 'src/**/*.js', 'src/**/*.ts'],
60
+ vanilla: ['**/*.html', '**/*.htm', '**/*.js'],
61
+ };
62
+
63
+ const IGNORE_PATTERNS = [
64
+ '**/node_modules/**',
65
+ '**/dist/**',
66
+ '**/build/**',
67
+ '**/.git/**',
68
+ '**/coverage/**',
69
+ '**/*.min.js',
70
+ ];
71
+
72
+ // ── Resolve files from a path arg ────────────────────────────────────────────
73
+ async function resolveFiles(pathArg, cwd, project) {
74
+ const patterns = FILE_PATTERNS[project.type] || FILE_PATTERNS.vanilla;
75
+
76
+ if (!pathArg) {
77
+ const files = [];
78
+ for (const p of patterns) {
79
+ const matched = await glob(p, { cwd, absolute: true, ignore: IGNORE_PATTERNS });
80
+ files.push(...matched);
81
+ }
82
+ return [...new Set(files)];
83
+ }
84
+
85
+ const resolved = resolve(cwd, pathArg);
86
+
87
+ if (existsSync(resolved)) {
88
+ if (statSync(resolved).isDirectory()) {
89
+ const files = [];
90
+ for (const p of patterns) {
91
+ const matched = await glob(p, { cwd: resolved, absolute: true, ignore: IGNORE_PATTERNS });
92
+ files.push(...matched);
93
+ }
94
+ return [...new Set(files)];
95
+ }
96
+ return [resolved];
97
+ }
98
+
99
+ // Treat as glob pattern
100
+ return glob(pathArg, { cwd, absolute: true, ignore: IGNORE_PATTERNS });
101
+ }
102
+
103
+ // ── Core translation runner ───────────────────────────────────────────────────
104
+ async function runTranslation({ pathArg, from, to, localesDir, dryRun, outdir, verbose, jsonMode, setup, cwd }) {
105
+ const project = detectProject(cwd);
106
+ console.log(chalk.green(` ✓ Project type: ${chalk.bold(project.type)}`));
107
+
108
+ const files = await resolveFiles(pathArg, cwd, project);
109
+
110
+ if (files.length === 0) {
111
+ console.log(chalk.yellow(' ⚠ No files found to translate.'));
112
+ return { totalStrings: 0, totalFiles: 0 };
113
+ }
114
+
115
+ console.log(chalk.cyan(` → ${files.length} file(s) [${from} → ${to}]...\n`));
116
+
117
+ // Ensure i18n setup (locale files + framework config)
118
+ if (setup !== false) {
119
+ const resolvedLocalesDir = localesDir ? resolve(cwd, localesDir) : undefined;
120
+ await ensureI18nSetup(cwd, project, from, to, resolvedLocalesDir);
121
+ }
122
+
123
+ let totalStrings = 0;
124
+ let totalFiles = 0;
125
+
126
+ for (const file of files) {
127
+ try {
128
+ const result = await translateFile(file, {
129
+ from,
130
+ to,
131
+ dryRun,
132
+ outdir,
133
+ project,
134
+ cwd,
135
+ verbose,
136
+ jsonMode,
137
+ localesDir: localesDir ? resolve(cwd, localesDir) : undefined,
138
+ });
139
+
140
+ if (result.count > 0) {
141
+ totalFiles++;
142
+ totalStrings += result.count;
143
+ const label = dryRun ? chalk.yellow('[DRY RUN]') : chalk.green('[DONE] ');
144
+ const skip = result.skipped > 0 ? chalk.gray(` (${result.skipped} already translated)`) : '';
145
+ console.log(` ${label} ${chalk.white(result.relativePath)} — ${result.count} new string(s)${skip}`);
146
+ if (dryRun && verbose && result.diff) {
147
+ console.log(chalk.gray(result.diff));
148
+ }
149
+ } else if (verbose) {
150
+ console.log(` ${chalk.gray('[SKIP] ')} ${result.relativePath} — already up to date`);
151
+ }
152
+ } catch (err) {
153
+ console.error(chalk.red(` ✗ ${file}: ${err.message}`));
154
+ if (verbose) console.error(err.stack);
155
+ }
156
+ }
157
+
158
+ return { totalStrings, totalFiles };
159
+ }
160
+
161
+ // ── init subcommand ───────────────────────────────────────────────────────────
162
+ program
163
+ .command('init')
164
+ .description('Interactive setup wizard — configure languages, locales folder, and i18n file')
165
+ .action(async () => {
166
+ const { default: inquirer } = await import('inquirer');
167
+ const cwd = process.cwd();
168
+ const existing = loadConfig(cwd);
169
+ const project = detectProject(cwd);
170
+
171
+ const defaultLocalesDir =
172
+ project.type === 'vue' || project.type === 'react' ? './src/locales' : './locales';
173
+ const defaultI18nFile =
174
+ project.type === 'vue' || project.type === 'react' ? './src/i18n.js' : './i18n.js';
175
+
176
+ console.log(chalk.cyan.bold('\n ⚡ bctranslate init\n'));
177
+ if (existing) {
178
+ console.log(chalk.gray(` Existing config found — press Enter to keep current values.\n`));
179
+ }
180
+
181
+ const answers = await inquirer.prompt([
182
+ {
183
+ type: 'input',
184
+ name: 'localesDir',
185
+ message: 'Folder for locale files (en.json, fr.json, ...):',
186
+ default: existing?.localesDir || defaultLocalesDir,
187
+ },
188
+ {
189
+ type: 'list',
190
+ name: 'from',
191
+ message: 'Source language:',
192
+ choices: SOURCE_LANGUAGES,
193
+ default: existing?.from || 'en',
194
+ },
195
+ {
196
+ type: 'input',
197
+ name: 'fromCustom',
198
+ message: 'Source language code:',
199
+ when: (ans) => ans.from === '__other__',
200
+ validate: (v) => /^[a-z]{2,5}$/.test(v.trim()) || 'Enter a valid BCP 47 code (e.g. en, zh-TW)',
201
+ },
202
+ {
203
+ type: 'checkbox',
204
+ name: 'to',
205
+ message: 'Target language(s) — Space to select, Enter to confirm:',
206
+ choices: COMMON_LANGUAGES,
207
+ default: existing?.to,
208
+ validate: (v) =>
209
+ v.length > 0 || 'Select at least one target language (Space to select)',
210
+ },
211
+ {
212
+ type: 'input',
213
+ name: 'extraTo',
214
+ message: 'Additional target codes (comma-separated, e.g. zh-TW,sr — leave blank to skip):',
215
+ default: '',
216
+ },
217
+ {
218
+ type: 'input',
219
+ name: 'i18nFile',
220
+ message: 'i18n setup file path (will be created if it does not exist):',
221
+ default: existing?.i18nFile || defaultI18nFile,
222
+ },
223
+ ]);
224
+
225
+ // Resolve from language
226
+ const fromLang = answers.from === '__other__' ? answers.fromCustom.trim() : answers.from;
227
+
228
+ // Merge checkbox selection + extra codes
229
+ const extraTo = answers.extraTo
230
+ ? answers.extraTo.split(',').map((s) => s.trim()).filter(Boolean)
231
+ : [];
232
+ const toLangs = [...new Set([...answers.to, ...extraTo])].filter((l) => l !== fromLang);
233
+
234
+ if (toLangs.length === 0) {
235
+ console.log(chalk.red('\n ✗ No target languages selected. Aborting.\n'));
236
+ process.exit(1);
237
+ }
238
+
239
+ const config = {
240
+ from: fromLang,
241
+ to: toLangs,
242
+ localesDir: answers.localesDir.trim(),
243
+ i18nFile: answers.i18nFile.trim(),
244
+ };
245
+
246
+ const configPath = saveConfig(config, cwd);
247
+
248
+ console.log(chalk.green(`\n ✓ Config saved → ${basename(configPath)}`));
249
+ console.log(chalk.cyan(`\n Source : ${chalk.bold(config.from)}`));
250
+ console.log(chalk.cyan(` Targets : ${chalk.bold(config.to.join(', '))}`));
251
+ console.log(chalk.cyan(` Locales : ${chalk.bold(config.localesDir)}`));
252
+ console.log(chalk.cyan(` i18n : ${chalk.bold(config.i18nFile)}`));
253
+ console.log(chalk.gray('\n Run `bctranslate` to start translating your project.\n'));
254
+ });
255
+
256
+ // ── Main translation command ──────────────────────────────────────────────────
257
+ program
258
+ .argument('[path]', 'File, directory, or glob pattern to translate')
259
+ .argument('[from]', 'Source language code (e.g. en)')
260
+ .argument('[keyword]', '"to" keyword')
261
+ .argument('[lang]', 'Target language code (e.g. fr)')
262
+ .option('-t, --to <lang>', 'Target language (alternative to positional syntax, e.g. fr or fr,es)')
263
+ .option('-d, --dry-run', 'Preview changes without writing files', false)
264
+ .option('-o, --outdir <dir>', 'Output directory for translated files')
265
+ .option('--no-setup', 'Skip i18n setup file generation')
266
+ .option('--json-mode <mode>', 'JSON translation mode: values or full', 'values')
267
+ .option('-v, --verbose', 'Verbose output', false)
268
+ .action(async (pathArg, fromArg, keyword, langArg, opts) => {
269
+ console.log(chalk.cyan.bold('\n ⚡ bctranslate\n'));
270
+
271
+ const invokedCwd = process.cwd();
272
+ const config = loadConfig(invokedCwd);
273
+
274
+ // If the user targets a specific file/dir and there's no config in the current
275
+ // working directory, treat the target's directory as the project root.
276
+ let cwd = invokedCwd;
277
+ if (!config && pathArg) {
278
+ const resolvedTarget = resolve(invokedCwd, pathArg);
279
+ if (existsSync(resolvedTarget)) {
280
+ try {
281
+ const st = statSync(resolvedTarget);
282
+ cwd = st.isDirectory() ? resolvedTarget : dirname(resolvedTarget);
283
+ } catch {
284
+ cwd = invokedCwd;
285
+ }
286
+ }
287
+ }
288
+
289
+ // ── Determine from/to/targets ─────────────────────────────────────────────
290
+ const hasExplicitArgs = !!(pathArg || fromArg || keyword || langArg);
291
+
292
+ if (!hasExplicitArgs && !config) {
293
+ console.log(chalk.yellow(' No config found. Run `bctranslate init` to set up your project.'));
294
+ console.log(chalk.gray('\n Or pass arguments directly:'));
295
+ console.log(chalk.gray(' bctranslate ./src en to fr'));
296
+ console.log(chalk.gray(' bctranslate ./App.vue en to fr,es\n'));
297
+ process.exit(0);
298
+ }
299
+
300
+ // Resolve from language
301
+ const from = fromArg || config?.from || 'en';
302
+
303
+ // Resolve target language(s)
304
+ let targets;
305
+ if (keyword === 'to' && langArg) {
306
+ // bctranslate ./src en to fr — or bctranslate ./src en to fr,es
307
+ targets = langArg.split(',').map((s) => s.trim()).filter(Boolean);
308
+ } else if (opts.to) {
309
+ // --to fr or --to fr,es
310
+ targets = opts.to.split(',').map((s) => s.trim()).filter(Boolean);
311
+ } else if (config?.to) {
312
+ targets = Array.isArray(config.to) ? config.to : [config.to];
313
+ } else {
314
+ targets = ['fr']; // ultimate fallback
315
+ }
316
+
317
+ // ── Check Python bridge for all language pairs ────────────────────────────
318
+ for (const to of targets) {
319
+ try {
320
+ await checkPythonBridge(from, to);
321
+ console.log(chalk.green(` ✓ Python bridge ready (${from} → ${to})`));
322
+ } catch (err) {
323
+ console.error(chalk.red(` ✗ ${err.message}`));
324
+ process.exit(1);
325
+ }
326
+ }
327
+
328
+ // ── Run translation for each target language ──────────────────────────────
329
+ let grandTotal = 0;
330
+ let grandFiles = 0;
331
+
332
+ for (const to of targets) {
333
+ if (targets.length > 1) {
334
+ console.log(chalk.cyan.bold(`\n ── Translating to ${to} ──`));
335
+ }
336
+
337
+ const { totalStrings, totalFiles } = await runTranslation({
338
+ pathArg,
339
+ from,
340
+ to,
341
+ localesDir: config?.localesDir,
342
+ dryRun: opts.dryRun,
343
+ outdir: opts.outdir,
344
+ verbose: opts.verbose,
345
+ jsonMode: opts.jsonMode,
346
+ setup: opts.setup,
347
+ cwd,
348
+ });
349
+
350
+ grandTotal += totalStrings;
351
+ grandFiles += totalFiles;
352
+ }
353
+
354
+ const langStr = targets.join(', ');
355
+ console.log(
356
+ chalk.cyan.bold(
357
+ `\n Done: ${grandTotal} new string(s) across ${grandFiles} file(s) [→ ${langStr}]\n`
358
+ )
359
+ );
360
+
361
+ console.log(chalk.gray(' Summary:'));
362
+ console.log(chalk.gray(` Root : ${cwd}`));
363
+ console.log(chalk.gray(` Source : ${from}`));
364
+ console.log(chalk.gray(` Target : ${langStr}\n`));
365
+ });
366
+
367
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "bctranslate",
3
+ "version": "1.0.0",
4
+ "description": "CLI to transform source files into i18n-ready code with automatic translation via argostranslate",
5
+ "type": "module",
6
+ "bin": {
7
+ "bctranslate": "./bin/bctranslate.js"
8
+ },
9
+ "main": "src/index.js",
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "python/",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "node test/run.js"
18
+ },
19
+ "dependencies": {
20
+ "@babel/generator": "^7.24.0",
21
+ "@babel/parser": "^7.24.0",
22
+ "@babel/traverse": "^7.24.0",
23
+ "@babel/types": "^7.24.0",
24
+ "@vue/compiler-dom": "^3.4.0",
25
+ "chalk": "^5.3.0",
26
+ "commander": "^12.1.0",
27
+ "glob": "^10.3.0",
28
+ "inquirer": "^9.3.8",
29
+ "magic-string": "^0.30.10"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "keywords": [
35
+ "i18n",
36
+ "translate",
37
+ "internationalization",
38
+ "vue",
39
+ "react",
40
+ "cli",
41
+ "argostranslate",
42
+ "localization",
43
+ "l10n",
44
+ "vue-i18n",
45
+ "react-i18next",
46
+ "offline"
47
+ ],
48
+ "license": "MIT",
49
+ "preferGlobal": true
50
+ }
package/python/t2.py ADDED
@@ -0,0 +1,76 @@
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()
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ bctranslate Python worker — translates a batch of strings using argostranslate.
4
+
5
+ Usage: python translator.py <from_code> <to_code>
6
+ Reads JSON array from stdin: [{"key": "t_abc123", "text": "Hello"}, ...]
7
+ Writes JSON array to stdout: [{"key": "t_abc123", "text": "Bonjour"}, ...]
8
+ """
9
+
10
+ import sys
11
+ import json
12
+
13
+ def ensure_model(from_code, to_code):
14
+ """Ensure the translation model is installed, downloading if needed."""
15
+ import argostranslate.package
16
+ import argostranslate.translate
17
+
18
+ installed_languages = argostranslate.translate.get_installed_languages()
19
+ from_lang = next((l for l in installed_languages if l.code == from_code), None)
20
+ to_lang = next((l for l in installed_languages if l.code == to_code), None)
21
+
22
+ if from_lang and to_lang:
23
+ translation = from_lang.get_translation(to_lang)
24
+ if translation:
25
+ return translation
26
+
27
+ # Try to install the package
28
+ print(f"Downloading language model {from_code}->{to_code}...", file=sys.stderr)
29
+ argostranslate.package.update_package_index()
30
+ available = argostranslate.package.get_available_packages()
31
+ pkg = next(
32
+ (p for p in available if p.from_code == from_code and p.to_code == to_code),
33
+ None
34
+ )
35
+
36
+ if not pkg:
37
+ print(
38
+ json.dumps({"error": f"No package available for {from_code}->{to_code}"}),
39
+ file=sys.stderr
40
+ )
41
+ sys.exit(1)
42
+
43
+ argostranslate.package.install_from_path(pkg.download())
44
+
45
+ # Reload
46
+ installed_languages = argostranslate.translate.get_installed_languages()
47
+ from_lang = next((l for l in installed_languages if l.code == from_code), None)
48
+ to_lang = next((l for l in installed_languages if l.code == to_code), None)
49
+
50
+ if not from_lang or not to_lang:
51
+ print(json.dumps({"error": "Failed to load model after install"}), file=sys.stderr)
52
+ sys.exit(1)
53
+
54
+ return from_lang.get_translation(to_lang)
55
+
56
+
57
+ def main():
58
+ if len(sys.argv) != 3:
59
+ print("Usage: translator.py <from_code> <to_code>", file=sys.stderr)
60
+ sys.exit(1)
61
+
62
+ from_code = sys.argv[1]
63
+ to_code = sys.argv[2]
64
+
65
+ # Load model once
66
+ try:
67
+ translator = ensure_model(from_code, to_code)
68
+ except Exception as e:
69
+ print(json.dumps({"error": f"Model loading failed: {str(e)}"}), file=sys.stderr)
70
+ sys.exit(1)
71
+
72
+ # Read batch from stdin
73
+ try:
74
+ raw = sys.stdin.read()
75
+ batch = json.loads(raw)
76
+ except json.JSONDecodeError as e:
77
+ print(json.dumps({"error": f"Invalid JSON input: {str(e)}"}), file=sys.stderr)
78
+ sys.exit(1)
79
+
80
+ # Translate each item
81
+ results = []
82
+ for item in batch:
83
+ key = item.get("key", "")
84
+ text = item.get("text", "")
85
+
86
+ if not text.strip():
87
+ results.append({"key": key, "text": text})
88
+ continue
89
+
90
+ try:
91
+ translated = translator.translate(text)
92
+ results.append({"key": key, "text": translated})
93
+ except Exception as e:
94
+ # On error, preserve original
95
+ print(f"Warning: failed to translate '{text[:50]}': {e}", file=sys.stderr)
96
+ results.append({"key": key, "text": text})
97
+
98
+ # Output result as JSON to stdout
99
+ print(json.dumps(results, ensure_ascii=False))
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()