bctranslate 1.0.5 → 1.0.7

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.
@@ -92,9 +92,9 @@ async function resolveFiles(pathArg, cwd, project) {
92
92
  }
93
93
 
94
94
  // ── Translation runner — uses global batching (Python spawned once) ───────────
95
- async function runTranslation({ pathArg, from, to, localesDir, dryRun, outdir, verbose, jsonMode, setup, cwd }) {
96
- const project = detectProject(cwd);
97
- console.log(chalk.green(` ✓ Project type: ${chalk.bold(project.type)}`));
95
+ async function runTranslation({ pathArg, from, to, localesDir, dryRun, outdir, verbose, jsonMode, setup, autoImport, cwd }) {
96
+ const project = detectProject(cwd);
97
+ console.log(chalk.green(` ✓ Project type: ${chalk.bold(project.type)}`));
98
98
 
99
99
  const files = await resolveFiles(pathArg, cwd, project);
100
100
  if (files.length === 0) {
@@ -104,10 +104,10 @@ async function runTranslation({ pathArg, from, to, localesDir, dryRun, outdir, v
104
104
 
105
105
  console.log(chalk.cyan(` → ${files.length} file(s) [${from} → ${to}]...\n`));
106
106
 
107
- if (setup !== false) {
108
- const resolvedLocalesDir = localesDir ? resolve(cwd, localesDir) : undefined;
109
- await ensureI18nSetup(cwd, project, from, to, resolvedLocalesDir);
110
- }
107
+ if (setup !== false) {
108
+ const resolvedLocalesDir = localesDir ? resolve(cwd, localesDir) : undefined;
109
+ await ensureI18nSetup(cwd, project, from, to, resolvedLocalesDir, { autoImport });
110
+ }
111
111
 
112
112
  // Global batch: all files parsed first, ONE Python call, then all writes
113
113
  const results = await translateAllFiles(files, {
@@ -277,17 +277,19 @@ program
277
277
  .argument('[lang]', 'Target language code (e.g. fr)')
278
278
  .option('-t, --to <lang>', 'Target language(s), comma-separated (e.g. fr or fr,es)')
279
279
  .option('-d, --dry-run', 'Preview changes without writing files', false)
280
- .option('-o, --outdir <dir>', 'Output directory for translated files')
281
- .option('--no-setup', 'Skip i18n setup file generation')
282
- .option('--json-mode <mode>', 'JSON translation mode: values or full', 'values')
283
- .option('-v, --verbose', 'Show per-file diffs and skipped files', false)
280
+ .option('-o, --outdir <dir>', 'Output directory for translated files')
281
+ .option('--no-setup', 'Skip i18n setup file generation')
282
+ .option('--no-import', 'Do not auto-inject i18n imports/script tags')
283
+ .option('--json-mode <mode>', 'JSON translation mode: values or full', 'values')
284
+ .option('-v, --verbose', 'Show per-file diffs and skipped files', false)
284
285
  .action(async (pathArg, fromArg, keyword, langArg, opts) => {
285
286
  console.log(chalk.cyan.bold('\n ⚡ bctranslate\n'));
286
287
 
287
- const invokedCwd = process.cwd();
288
- const config = loadConfig(invokedCwd);
289
-
290
- let cwd = invokedCwd;
288
+ const invokedCwd = process.cwd();
289
+ const config = loadConfig(invokedCwd);
290
+ const autoImport = opts.import !== false && (config?.autoImport !== false);
291
+
292
+ let cwd = invokedCwd;
291
293
  if (!config && pathArg) {
292
294
  const resolvedTarget = resolve(invokedCwd, pathArg);
293
295
  if (existsSync(resolvedTarget)) {
@@ -338,18 +340,19 @@ program
338
340
  for (const to of targets) {
339
341
  if (targets.length > 1) console.log(chalk.cyan.bold(`\n ── Translating to ${to} ──`));
340
342
 
341
- const { totalStrings, totalFiles } = await runTranslation({
342
- pathArg,
343
- from,
344
- to,
345
- localesDir: config?.localesDir,
346
- dryRun: opts.dryRun,
347
- outdir: opts.outdir,
348
- verbose: opts.verbose,
349
- jsonMode: opts.jsonMode,
350
- setup: opts.setup,
351
- cwd,
352
- });
343
+ const { totalStrings, totalFiles } = await runTranslation({
344
+ pathArg,
345
+ from,
346
+ to,
347
+ localesDir: config?.localesDir,
348
+ dryRun: opts.dryRun,
349
+ outdir: opts.outdir,
350
+ verbose: opts.verbose,
351
+ jsonMode: opts.jsonMode,
352
+ setup: opts.setup,
353
+ autoImport,
354
+ cwd,
355
+ });
353
356
 
354
357
  grandTotal += totalStrings;
355
358
  grandFiles += totalFiles;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bctranslate",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "CLI to transform source files into i18n-ready code with automatic translation via argostranslate",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,9 +13,10 @@ import { getLocaleDir } from './locales.js';
13
13
  * @param {string} to - Target language code
14
14
  * @param {string} [customLocaleDir] - Optional override for locale directory
15
15
  */
16
- export async function ensureI18nSetup(cwd, project, from, to, customLocaleDir) {
17
- const localeDir = customLocaleDir || getLocaleDir(cwd, project);
18
- mkdirSync(localeDir, { recursive: true });
16
+ export async function ensureI18nSetup(cwd, project, from, to, customLocaleDir, options = {}) {
17
+ const { autoImport = true } = options;
18
+ const localeDir = customLocaleDir || getLocaleDir(cwd, project);
19
+ mkdirSync(localeDir, { recursive: true });
19
20
 
20
21
  // Ensure locale JSON files exist
21
22
  for (const lang of [from, to]) {
@@ -24,18 +25,18 @@ export async function ensureI18nSetup(cwd, project, from, to, customLocaleDir) {
24
25
  writeFileSync(localePath, '{}\n', 'utf-8');
25
26
  console.log(chalk.green(` ✓ Created ${localePath}`));
26
27
  }
27
- }
28
-
29
- if (project.type === 'vue') {
30
- await ensureVueI18n(cwd, project, from, to, localeDir);
31
- } else if (project.type === 'react') {
32
- await ensureReactI18n(cwd, project, from, to, localeDir);
33
- } else {
34
- await ensureVanillaI18n(cwd, from, to, localeDir);
35
- }
36
- }
28
+ }
29
+
30
+ if (project.type === 'vue') {
31
+ await ensureVueI18n(cwd, project, from, to, localeDir, { autoImport });
32
+ } else if (project.type === 'react') {
33
+ await ensureReactI18n(cwd, project, from, to, localeDir, { autoImport });
34
+ } else {
35
+ await ensureVanillaI18n(cwd, from, to, localeDir, { autoImport });
36
+ }
37
+ }
37
38
 
38
- async function ensureVueI18n(cwd, project, from, to, localeDir) {
39
+ async function ensureVueI18n(cwd, project, from, to, localeDir, { autoImport }) {
39
40
  const i18nFile = join(cwd, 'src', 'i18n.js');
40
41
 
41
42
  if (existsSync(i18nFile)) {
@@ -48,51 +49,105 @@ async function ensureVueI18n(cwd, project, from, to, localeDir) {
48
49
 
49
50
  let content;
50
51
  if (isVue3) {
51
- content = `import { createI18n } from 'vue-i18n';
52
- import ${from} from '${relLocale}/${from}.json';
53
- import ${to} from '${relLocale}/${to}.json';
54
-
55
- const i18n = createI18n({
56
- legacy: false,
57
- locale: '${from}',
58
- fallbackLocale: '${from}',
59
- messages: {
60
- ${from},
61
- ${to},
62
- },
63
- });
52
+ content = `import { createI18n } from 'vue-i18n';
53
+ import ${from} from '${relLocale}/${from}.json';
54
+ import ${to} from '${relLocale}/${to}.json';
55
+
56
+ function nestMessages(input) {
57
+ if (!input || typeof input !== 'object') return {};
58
+ const out = Array.isArray(input) ? [] : {};
59
+ for (const [rawKey, value] of Object.entries(input)) {
60
+ if (!rawKey.includes('.')) {
61
+ out[rawKey] = value;
62
+ continue;
63
+ }
64
+ const parts = rawKey.split('.').filter(Boolean);
65
+ let cur = out;
66
+ for (let i = 0; i < parts.length; i++) {
67
+ const p = parts[i];
68
+ if (i === parts.length - 1) {
69
+ cur[p] = value;
70
+ } else {
71
+ if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
72
+ cur = cur[p];
73
+ }
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+
79
+ const i18n = createI18n({
80
+ legacy: false,
81
+ locale: '${from}',
82
+ fallbackLocale: '${from}',
83
+ messages: {
84
+ ${from}: nestMessages(${from}),
85
+ ${to}: nestMessages(${to}),
86
+ },
87
+ });
64
88
 
65
89
  export default i18n;
66
90
  `;
67
91
  } else {
68
- content = `import Vue from 'vue';
69
- import VueI18n from 'vue-i18n';
70
- import ${from} from '${relLocale}/${from}.json';
71
- import ${to} from '${relLocale}/${to}.json';
72
-
73
- Vue.use(VueI18n);
74
-
75
- const i18n = new VueI18n({
76
- locale: '${from}',
77
- fallbackLocale: '${from}',
78
- messages: {
79
- ${from},
80
- ${to},
81
- },
82
- });
92
+ content = `import Vue from 'vue';
93
+ import VueI18n from 'vue-i18n';
94
+ import ${from} from '${relLocale}/${from}.json';
95
+ import ${to} from '${relLocale}/${to}.json';
96
+
97
+ Vue.use(VueI18n);
98
+
99
+ function nestMessages(input) {
100
+ if (!input || typeof input !== 'object') return {};
101
+ const out = Array.isArray(input) ? [] : {};
102
+ for (const [rawKey, value] of Object.entries(input)) {
103
+ if (!rawKey.includes('.')) {
104
+ out[rawKey] = value;
105
+ continue;
106
+ }
107
+ const parts = rawKey.split('.').filter(Boolean);
108
+ let cur = out;
109
+ for (let i = 0; i < parts.length; i++) {
110
+ const p = parts[i];
111
+ if (i === parts.length - 1) {
112
+ cur[p] = value;
113
+ } else {
114
+ if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
115
+ cur = cur[p];
116
+ }
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+
122
+ const i18n = new VueI18n({
123
+ locale: '${from}',
124
+ fallbackLocale: '${from}',
125
+ messages: {
126
+ ${from}: nestMessages(${from}),
127
+ ${to}: nestMessages(${to}),
128
+ },
129
+ });
83
130
 
84
131
  export default i18n;
85
132
  `;
86
133
  }
87
134
 
88
- mkdirSync(join(cwd, 'src'), { recursive: true });
89
- writeFileSync(i18nFile, content, 'utf-8');
90
- console.log(chalk.green(` ✓ Created ${i18nFile}`));
91
- console.log(chalk.yellow(` ⚠ Don't forget to install vue-i18n: npm install vue-i18n`));
92
- console.log(chalk.yellow(` ⚠ Import and use i18n in your main.js/main.ts`));
93
- }
135
+ mkdirSync(join(cwd, 'src'), { recursive: true });
136
+ writeFileSync(i18nFile, content, 'utf-8');
137
+ console.log(chalk.green(` ✓ Created ${i18nFile}`));
138
+ console.log(chalk.yellow(` ⚠ Don't forget to install vue-i18n: npm install vue-i18n`));
139
+
140
+ if (autoImport) {
141
+ const injected = injectVueEntrypoint(cwd, isVue3);
142
+ if (!injected) {
143
+ console.log(chalk.yellow(` ⚠ Import and use i18n in your main.js/main.ts`));
144
+ }
145
+ } else {
146
+ console.log(chalk.yellow(` ⚠ Import and use i18n in your main.js/main.ts`));
147
+ }
148
+ }
94
149
 
95
- async function ensureReactI18n(cwd, project, from, to, localeDir) {
150
+ async function ensureReactI18n(cwd, project, from, to, localeDir, { autoImport }) {
96
151
  const i18nFile = join(cwd, 'src', 'i18n.js');
97
152
 
98
153
  if (existsSync(i18nFile)) {
@@ -102,16 +157,39 @@ async function ensureReactI18n(cwd, project, from, to, localeDir) {
102
157
 
103
158
  const relLocale = localeDir.replace(cwd, '.').replace(/\\/g, '/');
104
159
 
105
- const content = `import i18n from 'i18next';
106
- import { initReactI18next } from 'react-i18next';
107
- import ${from} from '${relLocale}/${from}.json';
108
- import ${to} from '${relLocale}/${to}.json';
109
-
110
- i18n.use(initReactI18next).init({
111
- resources: {
112
- ${from}: { translation: ${from} },
113
- ${to}: { translation: ${to} },
114
- },
160
+ const content = `import i18n from 'i18next';
161
+ import { initReactI18next } from 'react-i18next';
162
+ import ${from} from '${relLocale}/${from}.json';
163
+ import ${to} from '${relLocale}/${to}.json';
164
+
165
+ function nestMessages(input) {
166
+ if (!input || typeof input !== 'object') return {};
167
+ const out = Array.isArray(input) ? [] : {};
168
+ for (const [rawKey, value] of Object.entries(input)) {
169
+ if (!rawKey.includes('.')) {
170
+ out[rawKey] = value;
171
+ continue;
172
+ }
173
+ const parts = rawKey.split('.').filter(Boolean);
174
+ let cur = out;
175
+ for (let i = 0; i < parts.length; i++) {
176
+ const p = parts[i];
177
+ if (i === parts.length - 1) {
178
+ cur[p] = value;
179
+ } else {
180
+ if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
181
+ cur = cur[p];
182
+ }
183
+ }
184
+ }
185
+ return out;
186
+ }
187
+
188
+ i18n.use(initReactI18next).init({
189
+ resources: {
190
+ ${from}: { translation: nestMessages(${from}) },
191
+ ${to}: { translation: nestMessages(${to}) },
192
+ },
115
193
  lng: '${from}',
116
194
  fallbackLng: '${from}',
117
195
  interpolation: {
@@ -122,14 +200,22 @@ i18n.use(initReactI18next).init({
122
200
  export default i18n;
123
201
  `;
124
202
 
125
- mkdirSync(join(cwd, 'src'), { recursive: true });
126
- writeFileSync(i18nFile, content, 'utf-8');
127
- console.log(chalk.green(` ✓ Created ${i18nFile}`));
128
- console.log(chalk.yellow(` ⚠ Don't forget to install: npm install i18next react-i18next`));
129
- console.log(chalk.yellow(` ⚠ Import './i18n' in your index.js/App.js`));
130
- }
203
+ mkdirSync(join(cwd, 'src'), { recursive: true });
204
+ writeFileSync(i18nFile, content, 'utf-8');
205
+ console.log(chalk.green(` ✓ Created ${i18nFile}`));
206
+ console.log(chalk.yellow(` ⚠ Don't forget to install: npm install i18next react-i18next`));
207
+
208
+ if (autoImport) {
209
+ const injected = injectReactEntrypoint(cwd);
210
+ if (!injected) {
211
+ console.log(chalk.yellow(` ⚠ Import './i18n' in your index.js/App.js`));
212
+ }
213
+ } else {
214
+ console.log(chalk.yellow(` ⚠ Import './i18n' in your index.js/App.js`));
215
+ }
216
+ }
131
217
 
132
- async function ensureVanillaI18n(cwd, from, to, localeDir) {
218
+ async function ensureVanillaI18n(cwd, from, to, localeDir, { autoImport }) {
133
219
  const i18nFile = join(cwd, 'i18n.js');
134
220
 
135
221
  if (existsSync(i18nFile)) {
@@ -194,21 +280,123 @@ async function ensureVanillaI18n(cwd, from, to, localeDir) {
194
280
 
195
281
  // Auto-init
196
282
  loadLocale('${from}');
197
- setLocale('${to}');
198
-
199
- window.i18n = { t: t, setLocale: setLocale, loadLocale: loadLocale };
200
- })();
201
- `;
283
+
284
+ const api = { t: t, setLocale: setLocale, loadLocale: loadLocale, ready: null };
285
+ window.i18n = api;
286
+ api.ready = setLocale('${to}');
287
+ })();
288
+ `;
202
289
 
203
290
  writeFileSync(i18nFile, content, 'utf-8');
204
291
  console.log(chalk.green(` ✓ Created ${i18nFile}`));
205
292
 
206
- const injected = injectVanillaI18nEntrypoint(cwd);
207
- if (!injected) {
293
+ if (autoImport) {
294
+ const injected = injectVanillaI18nEntrypoint(cwd);
295
+ if (!injected) {
296
+ console.log(chalk.yellow(` ⚠ Add <script src="i18n.js"></script> to your HTML`));
297
+ }
298
+ } else {
208
299
  console.log(chalk.yellow(` ⚠ Add <script src="i18n.js"></script> to your HTML`));
209
300
  }
210
301
  }
211
302
 
303
+ function injectVueEntrypoint(cwd, isVue3) {
304
+ const candidates = [
305
+ join(cwd, 'src', 'main.ts'),
306
+ join(cwd, 'src', 'main.js'),
307
+ join(cwd, 'src', 'main.tsx'),
308
+ join(cwd, 'src', 'main.jsx'),
309
+ ];
310
+
311
+ const mainFile = candidates.find((p) => existsSync(p));
312
+ if (!mainFile) return false;
313
+
314
+ let code = readFileSync(mainFile, 'utf-8');
315
+
316
+ const hasI18nImport =
317
+ code.includes("from './i18n'") ||
318
+ code.includes('from "./i18n"') ||
319
+ code.includes("from './i18n.js'") ||
320
+ code.includes('from "./i18n.js"') ||
321
+ code.includes("import './i18n'") ||
322
+ code.includes('import "./i18n"');
323
+
324
+ if (!hasI18nImport) {
325
+ const importRe = /^import\s.+?;\s*$/gm;
326
+ let lastImportEnd = 0;
327
+ let m;
328
+ while ((m = importRe.exec(code)) !== null) lastImportEnd = m.index + m[0].length;
329
+ code =
330
+ code.slice(0, lastImportEnd) +
331
+ `\nimport i18n from './i18n';\n` +
332
+ code.slice(lastImportEnd);
333
+ }
334
+
335
+ if (isVue3) {
336
+ if (!code.includes('.use(i18n)') && !code.includes('use(i18n)')) {
337
+ const chained = /createApp\(([^)]*)\)\s*\.mount\(/;
338
+ if (chained.test(code)) {
339
+ code = code.replace(chained, 'createApp($1).use(i18n).mount(');
340
+ } else {
341
+ const hasAppVar =
342
+ /\bconst\s+app\s*=\s*createApp\(/.test(code) ||
343
+ /\blet\s+app\s*=\s*createApp\(/.test(code);
344
+ const mountIdx = code.indexOf('app.mount(');
345
+ if (hasAppVar && mountIdx !== -1 && !code.includes('app.use(i18n')) {
346
+ code = code.slice(0, mountIdx) + `app.use(i18n);\n` + code.slice(mountIdx);
347
+ } else {
348
+ return false;
349
+ }
350
+ }
351
+ }
352
+ } else {
353
+ if (!/\bi18n\s*[:,]/.test(code)) {
354
+ const newVue = /new\s+Vue\s*\(\s*\{/;
355
+ if (newVue.test(code)) {
356
+ code = code.replace(newVue, (m) => m + '\n i18n,');
357
+ } else {
358
+ return false;
359
+ }
360
+ }
361
+ }
362
+
363
+ writeFileSync(mainFile, code, 'utf-8');
364
+ console.log(chalk.green(` ✓ Updated ${mainFile} (wired i18n)`));
365
+ return true;
366
+ }
367
+
368
+ function injectReactEntrypoint(cwd) {
369
+ const candidates = [
370
+ join(cwd, 'src', 'index.tsx'),
371
+ join(cwd, 'src', 'index.js'),
372
+ join(cwd, 'src', 'main.tsx'),
373
+ join(cwd, 'src', 'main.jsx'),
374
+ ];
375
+
376
+ const entryFile = candidates.find((p) => existsSync(p));
377
+ if (!entryFile) return false;
378
+
379
+ let code = readFileSync(entryFile, 'utf-8');
380
+ if (
381
+ code.includes("from './i18n'") ||
382
+ code.includes('from "./i18n"') ||
383
+ code.includes("import './i18n'") ||
384
+ code.includes('import "./i18n"')
385
+ ) {
386
+ return true;
387
+ }
388
+
389
+ const importRe = /^import\s.+?;\s*$/gm;
390
+ let lastImportEnd = 0;
391
+ let m;
392
+ while ((m = importRe.exec(code)) !== null) lastImportEnd = m.index + m[0].length;
393
+ code = code.slice(0, lastImportEnd) + `\nimport './i18n';\n` + code.slice(lastImportEnd);
394
+
395
+ writeFileSync(entryFile, code, 'utf-8');
396
+ console.log(chalk.green(` ✓ Updated ${entryFile} (imported i18n)`));
397
+ return true;
398
+ }
399
+
212
400
  function injectVanillaI18nEntrypoint(cwd) {
213
401
  // Prefer HTML entrypoint if present
214
402
  const htmlPath = join(cwd, 'index.html');