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 +88 -0
- package/bin/bctranslate.js +367 -0
- package/package.json +50 -0
- package/python/t2.py +76 -0
- package/python/translator.py +103 -0
- package/src/bridges/python.js +183 -0
- package/src/config.js +28 -0
- package/src/detect.js +58 -0
- package/src/generators/locales.js +62 -0
- package/src/generators/setup.js +188 -0
- package/src/index.js +155 -0
- package/src/parsers/html.js +118 -0
- package/src/parsers/js.js +128 -0
- package/src/parsers/json.js +93 -0
- package/src/parsers/react.js +153 -0
- package/src/parsers/vue.js +211 -0
- package/src/utils.js +91 -0
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()
|