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