@vocoder/cli 0.1.3 → 0.1.6
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 +43 -91
- package/dist/bin.mjs +456 -1381
- package/dist/bin.mjs.map +1 -1
- package/package.json +1 -2
package/dist/bin.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
|
+
import chalk from "chalk";
|
|
8
9
|
|
|
9
10
|
// src/utils/api.ts
|
|
10
11
|
function isLimitErrorResponse(value) {
|
|
@@ -254,11 +255,294 @@ var VocoderAPI = class {
|
|
|
254
255
|
}
|
|
255
256
|
return payload;
|
|
256
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Look up whether a project already exists for a given repo + scope.
|
|
260
|
+
* Returns { projectId, projectName, organizationName } or null if not found.
|
|
261
|
+
*/
|
|
262
|
+
async lookupProjectByRepo(params) {
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(`${this.apiUrl}/api/cli/init/lookup`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: { "Content-Type": "application/json" },
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
repo: params.repoCanonical,
|
|
269
|
+
scopePath: params.scopePath
|
|
270
|
+
})
|
|
271
|
+
});
|
|
272
|
+
if (response.status === 404) return null;
|
|
273
|
+
if (!response.ok) return null;
|
|
274
|
+
return await response.json();
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
257
279
|
};
|
|
258
280
|
|
|
259
|
-
// src/
|
|
260
|
-
import { existsSync, readFileSync
|
|
281
|
+
// src/utils/detect-local.ts
|
|
282
|
+
import { existsSync, readFileSync } from "fs";
|
|
261
283
|
import { join } from "path";
|
|
284
|
+
function detectLocalEcosystem(cwd = process.cwd()) {
|
|
285
|
+
const packageManager = detectPackageManager(cwd);
|
|
286
|
+
const pkg = readPackageJson(cwd);
|
|
287
|
+
if (!pkg) {
|
|
288
|
+
return {
|
|
289
|
+
ecosystem: null,
|
|
290
|
+
framework: null,
|
|
291
|
+
packageManager,
|
|
292
|
+
uiPackage: null,
|
|
293
|
+
hasUnplugin: false,
|
|
294
|
+
hasUiPackage: false,
|
|
295
|
+
sourceLocale: null
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const allDeps = {
|
|
299
|
+
...pkg.dependencies ?? {},
|
|
300
|
+
...pkg.devDependencies ?? {}
|
|
301
|
+
};
|
|
302
|
+
const hasUnplugin = "@vocoder/unplugin" in allDeps;
|
|
303
|
+
const { ecosystem, framework, uiPackage } = detectFromDeps(allDeps, cwd);
|
|
304
|
+
const hasUiPackage = uiPackage !== null && uiPackage in allDeps;
|
|
305
|
+
return {
|
|
306
|
+
ecosystem,
|
|
307
|
+
framework,
|
|
308
|
+
packageManager,
|
|
309
|
+
uiPackage,
|
|
310
|
+
hasUnplugin,
|
|
311
|
+
hasUiPackage,
|
|
312
|
+
sourceLocale: null
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function detectPackageManager(cwd) {
|
|
316
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
317
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
|
|
318
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
319
|
+
return "npm";
|
|
320
|
+
}
|
|
321
|
+
function readPackageJson(cwd) {
|
|
322
|
+
const pkgPath = join(cwd, "package.json");
|
|
323
|
+
if (!existsSync(pkgPath)) return null;
|
|
324
|
+
try {
|
|
325
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function detectFromDeps(allDeps, cwd) {
|
|
331
|
+
if ("vue" in allDeps) {
|
|
332
|
+
const framework = "nuxt" in allDeps ? "nuxt" : null;
|
|
333
|
+
return { ecosystem: "vue", framework, uiPackage: "@vocoder/vue" };
|
|
334
|
+
}
|
|
335
|
+
if ("svelte" in allDeps) {
|
|
336
|
+
const framework = "@sveltejs/kit" in allDeps ? "sveltekit" : null;
|
|
337
|
+
return { ecosystem: "svelte", framework, uiPackage: "@vocoder/svelte" };
|
|
338
|
+
}
|
|
339
|
+
if ("@angular/core" in allDeps || existsSync(join(cwd, "angular.json"))) {
|
|
340
|
+
return { ecosystem: "angular", framework: "angular", uiPackage: "@vocoder/angular" };
|
|
341
|
+
}
|
|
342
|
+
if ("react" in allDeps) {
|
|
343
|
+
let framework = null;
|
|
344
|
+
if ("next" in allDeps) framework = "nextjs";
|
|
345
|
+
else if ("@remix-run/react" in allDeps) framework = "remix";
|
|
346
|
+
else if ("gatsby" in allDeps) framework = "gatsby";
|
|
347
|
+
else if ("vite" in allDeps) framework = "vite";
|
|
348
|
+
return { ecosystem: "react", framework, uiPackage: "@vocoder/react" };
|
|
349
|
+
}
|
|
350
|
+
return { ecosystem: null, framework: null, uiPackage: null };
|
|
351
|
+
}
|
|
352
|
+
function buildInstallCommand(packageManager, packages) {
|
|
353
|
+
if (packages.length === 0) return "";
|
|
354
|
+
const pkgList = packages.join(" ");
|
|
355
|
+
switch (packageManager) {
|
|
356
|
+
case "pnpm":
|
|
357
|
+
return `pnpm add ${pkgList}`;
|
|
358
|
+
case "yarn":
|
|
359
|
+
return `yarn add ${pkgList}`;
|
|
360
|
+
case "bun":
|
|
361
|
+
return `bun add ${pkgList}`;
|
|
362
|
+
default:
|
|
363
|
+
return `npm install ${pkgList}`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function getPackagesToInstall(detection) {
|
|
367
|
+
const packages = [];
|
|
368
|
+
if (!detection.hasUnplugin) packages.push("@vocoder/unplugin");
|
|
369
|
+
if (detection.uiPackage && !detection.hasUiPackage) packages.push(detection.uiPackage);
|
|
370
|
+
return packages;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/utils/setup-snippets.ts
|
|
374
|
+
function getSetupSnippets(params) {
|
|
375
|
+
const { framework, ecosystem, sourceLocale, translationTriggers } = params;
|
|
376
|
+
return {
|
|
377
|
+
pluginStep: getPluginSnippet(framework, ecosystem),
|
|
378
|
+
providerStep: getProviderSnippet(ecosystem, sourceLocale),
|
|
379
|
+
wrapStep: getWrapSnippet(ecosystem),
|
|
380
|
+
whatsNext: getWhatsNextMessage(translationTriggers)
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function getPluginSnippet(framework, ecosystem) {
|
|
384
|
+
switch (framework) {
|
|
385
|
+
case "nextjs":
|
|
386
|
+
return {
|
|
387
|
+
file: "next.config.ts",
|
|
388
|
+
code: `import { withVocoder } from '@vocoder/unplugin/next';
|
|
389
|
+
|
|
390
|
+
export default withVocoder({
|
|
391
|
+
// your existing Next.js config
|
|
392
|
+
});`
|
|
393
|
+
};
|
|
394
|
+
case "vite":
|
|
395
|
+
case "remix":
|
|
396
|
+
return {
|
|
397
|
+
file: "vite.config.ts",
|
|
398
|
+
code: `import vocoder from '@vocoder/unplugin/vite';
|
|
399
|
+
|
|
400
|
+
export default defineConfig({
|
|
401
|
+
plugins: [
|
|
402
|
+
vocoder(),
|
|
403
|
+
// your other plugins
|
|
404
|
+
],
|
|
405
|
+
});`
|
|
406
|
+
};
|
|
407
|
+
case "nuxt":
|
|
408
|
+
return {
|
|
409
|
+
file: "nuxt.config.ts",
|
|
410
|
+
code: `import vocoder from '@vocoder/unplugin/vite';
|
|
411
|
+
|
|
412
|
+
export default defineNuxtConfig({
|
|
413
|
+
vite: {
|
|
414
|
+
plugins: [vocoder()],
|
|
415
|
+
},
|
|
416
|
+
});`
|
|
417
|
+
};
|
|
418
|
+
case "sveltekit":
|
|
419
|
+
return {
|
|
420
|
+
file: "vite.config.ts",
|
|
421
|
+
code: `import vocoder from '@vocoder/unplugin/vite';
|
|
422
|
+
import { sveltekit } from '@sveltejs/kit/vite';
|
|
423
|
+
|
|
424
|
+
export default defineConfig({
|
|
425
|
+
plugins: [
|
|
426
|
+
sveltekit(),
|
|
427
|
+
vocoder(),
|
|
428
|
+
],
|
|
429
|
+
});`
|
|
430
|
+
};
|
|
431
|
+
case "gatsby":
|
|
432
|
+
return {
|
|
433
|
+
file: "gatsby-node.js",
|
|
434
|
+
code: `const vocoder = require('@vocoder/unplugin/webpack');
|
|
435
|
+
|
|
436
|
+
exports.onCreateWebpackConfig = ({ actions }) => {
|
|
437
|
+
actions.setWebpackConfig({
|
|
438
|
+
plugins: [vocoder()],
|
|
439
|
+
});
|
|
440
|
+
};`
|
|
441
|
+
};
|
|
442
|
+
case "angular":
|
|
443
|
+
return null;
|
|
444
|
+
// Angular CLI doesn't expose plugin config easily
|
|
445
|
+
default:
|
|
446
|
+
if (ecosystem) {
|
|
447
|
+
return {
|
|
448
|
+
file: "your bundler config",
|
|
449
|
+
code: `// Vite
|
|
450
|
+
import vocoder from '@vocoder/unplugin/vite';
|
|
451
|
+
// Webpack
|
|
452
|
+
const vocoder = require('@vocoder/unplugin/webpack');
|
|
453
|
+
|
|
454
|
+
// Add vocoder() to your plugins array`
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function getProviderSnippet(ecosystem, sourceLocale) {
|
|
461
|
+
switch (ecosystem) {
|
|
462
|
+
case "react":
|
|
463
|
+
return {
|
|
464
|
+
file: "your root layout or App component",
|
|
465
|
+
code: `import { VocoderProvider } from '@vocoder/react';
|
|
466
|
+
|
|
467
|
+
<VocoderProvider defaultLocale="${sourceLocale}">
|
|
468
|
+
{children}
|
|
469
|
+
</VocoderProvider>`
|
|
470
|
+
};
|
|
471
|
+
case "vue":
|
|
472
|
+
return {
|
|
473
|
+
file: "your app entry",
|
|
474
|
+
code: `import { createVocoder } from '@vocoder/vue';
|
|
475
|
+
|
|
476
|
+
const vocoder = createVocoder({
|
|
477
|
+
defaultLocale: '${sourceLocale}',
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
app.use(vocoder);`
|
|
481
|
+
};
|
|
482
|
+
case "svelte":
|
|
483
|
+
return {
|
|
484
|
+
file: "your root layout",
|
|
485
|
+
code: `<script>
|
|
486
|
+
import { VocoderProvider } from '@vocoder/svelte';
|
|
487
|
+
</script>
|
|
488
|
+
|
|
489
|
+
<VocoderProvider defaultLocale="${sourceLocale}">
|
|
490
|
+
<slot />
|
|
491
|
+
</VocoderProvider>`
|
|
492
|
+
};
|
|
493
|
+
default:
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function getWrapSnippet(ecosystem) {
|
|
498
|
+
switch (ecosystem) {
|
|
499
|
+
case "react":
|
|
500
|
+
return {
|
|
501
|
+
code: `import { T } from '@vocoder/react';
|
|
502
|
+
|
|
503
|
+
<T>Hello, world!</T>`
|
|
504
|
+
};
|
|
505
|
+
case "vue":
|
|
506
|
+
return {
|
|
507
|
+
code: `<template>
|
|
508
|
+
<T>Hello, world!</T>
|
|
509
|
+
</template>
|
|
510
|
+
|
|
511
|
+
<script setup>
|
|
512
|
+
import { T } from '@vocoder/vue';
|
|
513
|
+
</script>`
|
|
514
|
+
};
|
|
515
|
+
case "svelte":
|
|
516
|
+
return {
|
|
517
|
+
code: `<script>
|
|
518
|
+
import { T } from '@vocoder/svelte';
|
|
519
|
+
</script>
|
|
520
|
+
|
|
521
|
+
<T>Hello, world!</T>`
|
|
522
|
+
};
|
|
523
|
+
default:
|
|
524
|
+
return {
|
|
525
|
+
code: `// Wrap translatable strings with <T>
|
|
526
|
+
<T>Hello, world!</T>`
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function getWhatsNextMessage(triggers) {
|
|
531
|
+
const parts = [];
|
|
532
|
+
if (triggers.includes("push")) {
|
|
533
|
+
parts.push("Push to a target branch to trigger translations.");
|
|
534
|
+
}
|
|
535
|
+
if (triggers.includes("pull_request")) {
|
|
536
|
+
parts.push("Open a pull request to trigger translations.");
|
|
537
|
+
}
|
|
538
|
+
if (triggers.includes("manual")) {
|
|
539
|
+
parts.push("Run `vocoder sync` to extract and translate.");
|
|
540
|
+
}
|
|
541
|
+
if (parts.length === 0) {
|
|
542
|
+
parts.push("Push to a target branch to trigger translations.");
|
|
543
|
+
}
|
|
544
|
+
return parts.join("\n");
|
|
545
|
+
}
|
|
262
546
|
|
|
263
547
|
// src/utils/git-identity.ts
|
|
264
548
|
import { execSync } from "child_process";
|
|
@@ -357,57 +641,16 @@ function resolveGitContext() {
|
|
|
357
641
|
}
|
|
358
642
|
|
|
359
643
|
// src/commands/init.ts
|
|
644
|
+
import { config as loadEnv } from "dotenv";
|
|
645
|
+
import { execSync as execSync2 } from "child_process";
|
|
360
646
|
import { spawn } from "child_process";
|
|
647
|
+
loadEnv();
|
|
361
648
|
var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
|
|
362
|
-
function escapeRegExp(value) {
|
|
363
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
364
|
-
}
|
|
365
649
|
function parseTargetLocales(value) {
|
|
366
650
|
if (!value) return void 0;
|
|
367
651
|
const locales = value.split(",").map((locale) => locale.trim()).filter(Boolean);
|
|
368
652
|
return locales.length > 0 ? locales : void 0;
|
|
369
653
|
}
|
|
370
|
-
function getEnvLine(filePath, key) {
|
|
371
|
-
if (!existsSync(filePath)) {
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
const current = readFileSync(filePath, "utf-8");
|
|
375
|
-
const pattern = new RegExp(`^${escapeRegExp(key)}=.*$`, "m");
|
|
376
|
-
const existingMatch = current.match(pattern);
|
|
377
|
-
return existingMatch?.[0] ?? null;
|
|
378
|
-
}
|
|
379
|
-
function getEnvValue(filePath, key) {
|
|
380
|
-
const line = getEnvLine(filePath, key);
|
|
381
|
-
if (!line) return null;
|
|
382
|
-
const eqIndex = line.indexOf("=");
|
|
383
|
-
if (eqIndex === -1) return null;
|
|
384
|
-
return line.slice(eqIndex + 1);
|
|
385
|
-
}
|
|
386
|
-
function upsertEnvValue(params) {
|
|
387
|
-
const lineValue = `${params.key}=${params.value}`;
|
|
388
|
-
if (!existsSync(params.filePath)) {
|
|
389
|
-
writeFileSync(params.filePath, `${lineValue}
|
|
390
|
-
`, "utf-8");
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
const current = readFileSync(params.filePath, "utf-8");
|
|
394
|
-
const pattern = new RegExp(`^${escapeRegExp(params.key)}=.*$`, "m");
|
|
395
|
-
const existingMatch = current.match(pattern);
|
|
396
|
-
if (existingMatch && existingMatch[0] !== lineValue && !params.allowOverwrite) {
|
|
397
|
-
throw new Error(
|
|
398
|
-
`${params.key} already exists in ${params.filePath}. Re-run with --yes to overwrite.`
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
if (existingMatch) {
|
|
402
|
-
const updated = current.replace(pattern, lineValue);
|
|
403
|
-
writeFileSync(params.filePath, updated.endsWith("\n") ? updated : `${updated}
|
|
404
|
-
`, "utf-8");
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const prefix = current.endsWith("\n") || current.length === 0 ? "" : "\n";
|
|
408
|
-
writeFileSync(params.filePath, `${current}${prefix}${lineValue}
|
|
409
|
-
`, "utf-8");
|
|
410
|
-
}
|
|
411
654
|
async function sleep(ms) {
|
|
412
655
|
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
413
656
|
}
|
|
@@ -468,76 +711,107 @@ function printPlanLimitMessage(apiUrl, message) {
|
|
|
468
711
|
${message}`);
|
|
469
712
|
p.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
|
|
470
713
|
}
|
|
471
|
-
function
|
|
472
|
-
|
|
473
|
-
|
|
714
|
+
function runScaffold(params) {
|
|
715
|
+
const { projectName, organizationName, sourceLocale, translationTriggers } = params;
|
|
716
|
+
p.log.info(`Project: ${chalk.bold(projectName)}`);
|
|
717
|
+
p.log.info(`Workspace: ${chalk.bold(organizationName)}`);
|
|
718
|
+
const detection = detectLocalEcosystem();
|
|
719
|
+
if (detection.ecosystem) {
|
|
720
|
+
const frameworkLabel = detection.framework ?? detection.ecosystem;
|
|
721
|
+
const pmLabel = detection.packageManager;
|
|
722
|
+
p.log.info(`Detected: ${chalk.bold(frameworkLabel)} (${pmLabel})`);
|
|
723
|
+
}
|
|
724
|
+
const packagesToInstall = getPackagesToInstall(detection);
|
|
725
|
+
if (packagesToInstall.length > 0) {
|
|
726
|
+
const installCmd = buildInstallCommand(detection.packageManager, packagesToInstall);
|
|
727
|
+
p.log.info("");
|
|
728
|
+
const installSpinner = p.spinner();
|
|
729
|
+
installSpinner.start(`Installing ${packagesToInstall.join(", ")}...`);
|
|
730
|
+
try {
|
|
731
|
+
execSync2(installCmd, { stdio: "pipe", cwd: process.cwd() });
|
|
732
|
+
installSpinner.stop(`Installed ${packagesToInstall.join(", ")}`);
|
|
733
|
+
} catch {
|
|
734
|
+
installSpinner.stop("Package installation failed");
|
|
735
|
+
p.log.warn(`Run manually: ${chalk.cyan(installCmd)}`);
|
|
736
|
+
}
|
|
737
|
+
} else if (detection.ecosystem) {
|
|
738
|
+
p.log.info(`Packages: ${chalk.green("already installed")}`);
|
|
739
|
+
}
|
|
740
|
+
const snippets = getSetupSnippets({
|
|
741
|
+
framework: detection.framework,
|
|
742
|
+
ecosystem: detection.ecosystem,
|
|
743
|
+
sourceLocale,
|
|
744
|
+
translationTriggers
|
|
745
|
+
});
|
|
746
|
+
let stepNum = 1;
|
|
747
|
+
if (snippets.pluginStep) {
|
|
748
|
+
p.log.message("");
|
|
749
|
+
p.log.step(`${chalk.bold(`Step ${stepNum}:`)} Add the plugin to ${chalk.cyan(snippets.pluginStep.file)}`);
|
|
750
|
+
printCodeBlock(snippets.pluginStep.code);
|
|
751
|
+
stepNum++;
|
|
752
|
+
}
|
|
753
|
+
if (snippets.providerStep) {
|
|
754
|
+
p.log.step(`${chalk.bold(`Step ${stepNum}:`)} Add the provider to ${chalk.cyan(snippets.providerStep.file)}`);
|
|
755
|
+
printCodeBlock(snippets.providerStep.code);
|
|
756
|
+
stepNum++;
|
|
757
|
+
}
|
|
758
|
+
p.log.step(`${chalk.bold(`Step ${stepNum}:`)} Wrap translatable strings`);
|
|
759
|
+
printCodeBlock(snippets.wrapStep.code);
|
|
760
|
+
p.log.message("");
|
|
761
|
+
for (const line of snippets.whatsNext.split("\n")) {
|
|
762
|
+
p.log.success(line);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function printCodeBlock(code) {
|
|
766
|
+
const lines = code.split("\n");
|
|
767
|
+
const maxLen = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
768
|
+
const bar = chalk.gray("\u2502");
|
|
769
|
+
const pad = (s) => s + " ".repeat(maxLen - s.length);
|
|
770
|
+
process.stdout.write(`${chalk.gray("\u2502")}
|
|
771
|
+
`);
|
|
772
|
+
process.stdout.write(`${chalk.gray("\u2502")} ${chalk.gray("\u250C" + "\u2500".repeat(maxLen + 2) + "\u2510")}
|
|
773
|
+
`);
|
|
774
|
+
for (const line of lines) {
|
|
775
|
+
process.stdout.write(`${chalk.gray("\u2502")} ${bar} ${pad(line)} ${bar}
|
|
776
|
+
`);
|
|
777
|
+
}
|
|
778
|
+
process.stdout.write(`${chalk.gray("\u2502")} ${chalk.gray("\u2514" + "\u2500".repeat(maxLen + 2) + "\u2518")}
|
|
779
|
+
`);
|
|
474
780
|
}
|
|
475
781
|
async function init(options = {}) {
|
|
476
|
-
const projectRoot = process.cwd();
|
|
477
782
|
const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
|
|
478
|
-
const envPath = join(projectRoot, ".env");
|
|
479
783
|
p.intro("Vocoder Setup");
|
|
480
|
-
const
|
|
784
|
+
const spinner3 = p.spinner();
|
|
481
785
|
try {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
{ value: "keep", label: "Keep current configuration" },
|
|
505
|
-
{ value: "reconfigure", label: "Reconfigure (new browser setup)" }
|
|
506
|
-
]
|
|
786
|
+
const gitContext = resolveGitContext();
|
|
787
|
+
const identity = gitContext.identity;
|
|
788
|
+
if (gitContext.warnings.length > 0) {
|
|
789
|
+
for (const warning of gitContext.warnings) {
|
|
790
|
+
p.log.warn(warning);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (identity) {
|
|
794
|
+
spinner3.start("Checking for existing project...");
|
|
795
|
+
const api2 = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
796
|
+
const existing = await api2.lookupProjectByRepo({
|
|
797
|
+
repoCanonical: identity.repoCanonical,
|
|
798
|
+
scopePath: identity.repoScopePath
|
|
799
|
+
});
|
|
800
|
+
if (existing) {
|
|
801
|
+
spinner3.stop("Found existing project!");
|
|
802
|
+
p.outro("Vocoder is already set up for this repository.");
|
|
803
|
+
runScaffold({
|
|
804
|
+
projectName: existing.projectName,
|
|
805
|
+
organizationName: existing.organizationName,
|
|
806
|
+
sourceLocale: existing.sourceLocale ?? "en",
|
|
807
|
+
translationTriggers: existing.translationTriggers ?? ["push"]
|
|
507
808
|
});
|
|
508
|
-
|
|
509
|
-
p.cancel("Setup cancelled.");
|
|
510
|
-
return 1;
|
|
511
|
-
}
|
|
512
|
-
if (action === "keep") {
|
|
513
|
-
p.outro("Configuration unchanged. You're all set!");
|
|
514
|
-
return 0;
|
|
515
|
-
}
|
|
516
|
-
} catch {
|
|
517
|
-
p.log.warn("Found VOCODER_API_KEY in .env but it appears to be invalid or expired.");
|
|
518
|
-
if (!options.yes) {
|
|
519
|
-
const action = await p.select({
|
|
520
|
-
message: "What would you like to do?",
|
|
521
|
-
options: [
|
|
522
|
-
{ value: "reconfigure", label: "Reconfigure (new browser setup)" },
|
|
523
|
-
{ value: "keep", label: "Keep current key anyway" }
|
|
524
|
-
]
|
|
525
|
-
});
|
|
526
|
-
if (p.isCancel(action)) {
|
|
527
|
-
p.cancel("Setup cancelled.");
|
|
528
|
-
return 1;
|
|
529
|
-
}
|
|
530
|
-
if (action === "keep") {
|
|
531
|
-
p.outro("Keeping existing key. You may encounter errors if the key is invalid.");
|
|
532
|
-
return 0;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
809
|
+
return 0;
|
|
535
810
|
}
|
|
811
|
+
spinner3.stop("No existing project found for this repo.");
|
|
536
812
|
}
|
|
537
|
-
|
|
813
|
+
spinner3.start("Creating setup session");
|
|
538
814
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
539
|
-
const gitContext = resolveGitContext();
|
|
540
|
-
const identity = gitContext.identity;
|
|
541
815
|
const start = await api.startInitSession({
|
|
542
816
|
projectName: options.projectName,
|
|
543
817
|
sourceLocale: options.sourceLocale,
|
|
@@ -545,14 +819,10 @@ async function init(options = {}) {
|
|
|
545
819
|
...identity?.repoCanonical ? { repoCanonical: identity.repoCanonical } : {},
|
|
546
820
|
...identity ? { repoScopePath: identity.repoScopePath } : {}
|
|
547
821
|
});
|
|
548
|
-
|
|
822
|
+
spinner3.stop("Setup session created");
|
|
549
823
|
const verificationUrlString = start.verificationUrl;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
p.log.warn(warning);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
p.note(verificationUrlString, "Authorize in your browser");
|
|
824
|
+
p.log.info("Create a project in your browser to continue.");
|
|
825
|
+
p.note(verificationUrlString, "Setup URL");
|
|
556
826
|
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
557
827
|
const shouldOpen = options.yes ? true : await p.confirm({ message: "Open this URL in your browser?" });
|
|
558
828
|
if (p.isCancel(shouldOpen)) {
|
|
@@ -562,14 +832,14 @@ async function init(options = {}) {
|
|
|
562
832
|
if (shouldOpen) {
|
|
563
833
|
const opened = await tryOpenBrowser(verificationUrlString);
|
|
564
834
|
if (opened) {
|
|
565
|
-
p.log.info("Opened your browser
|
|
835
|
+
p.log.info("Opened your browser.");
|
|
566
836
|
} else {
|
|
567
837
|
p.log.info("Could not open a browser automatically. Use the URL above.");
|
|
568
838
|
}
|
|
569
839
|
}
|
|
570
840
|
}
|
|
571
841
|
const expiresAt = new Date(start.expiresAt).getTime();
|
|
572
|
-
|
|
842
|
+
spinner3.start("Waiting for setup to complete...");
|
|
573
843
|
while (Date.now() < expiresAt) {
|
|
574
844
|
const status = await api.getInitSessionStatus({
|
|
575
845
|
sessionId: start.sessionId,
|
|
@@ -578,13 +848,13 @@ async function init(options = {}) {
|
|
|
578
848
|
if (status.status === "pending") {
|
|
579
849
|
const pendingMessage = status.message?.trim();
|
|
580
850
|
if (pendingMessage) {
|
|
581
|
-
|
|
851
|
+
spinner3.message(`Waiting for setup to complete... (${pendingMessage})`);
|
|
582
852
|
}
|
|
583
853
|
await sleep((status.pollIntervalSeconds || start.poll.intervalSeconds) * 1e3);
|
|
584
854
|
continue;
|
|
585
855
|
}
|
|
586
856
|
if (status.status === "failed") {
|
|
587
|
-
|
|
857
|
+
spinner3.stop("Setup failed");
|
|
588
858
|
if (isPlanLimitFailure(status.message)) {
|
|
589
859
|
printPlanLimitMessage(apiUrl, status.message);
|
|
590
860
|
} else {
|
|
@@ -594,70 +864,24 @@ async function init(options = {}) {
|
|
|
594
864
|
return 1;
|
|
595
865
|
}
|
|
596
866
|
if (status.status === "completed") {
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
const desiredLine = `${key}=${status.credentials.apiKey}`;
|
|
600
|
-
const existingLine = getEnvLine(envPath, key);
|
|
601
|
-
const isAlreadyCurrent = existingLine === desiredLine;
|
|
602
|
-
let didOverwrite = false;
|
|
603
|
-
if (!isAlreadyCurrent) {
|
|
604
|
-
try {
|
|
605
|
-
upsertEnvValue({
|
|
606
|
-
filePath: envPath,
|
|
607
|
-
key,
|
|
608
|
-
value: status.credentials.apiKey,
|
|
609
|
-
allowOverwrite: Boolean(options.yes)
|
|
610
|
-
});
|
|
611
|
-
didOverwrite = Boolean(existingLine);
|
|
612
|
-
} catch (error) {
|
|
613
|
-
const overwriteConflict = error instanceof Error && error.message.includes(`${key} already exists in ${envPath}`);
|
|
614
|
-
if (!overwriteConflict) {
|
|
615
|
-
throw error;
|
|
616
|
-
}
|
|
617
|
-
const shouldOverwrite = await p.confirm({
|
|
618
|
-
message: `${key} already exists in ${envPath}. Overwrite it?`
|
|
619
|
-
});
|
|
620
|
-
if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
621
|
-
p.log.warn("Existing VOCODER_API_KEY was not changed.");
|
|
622
|
-
p.log.info("Re-run with --yes to overwrite it without prompting.");
|
|
623
|
-
p.cancel("Setup cancelled.");
|
|
624
|
-
return 1;
|
|
625
|
-
}
|
|
626
|
-
upsertEnvValue({
|
|
627
|
-
filePath: envPath,
|
|
628
|
-
key,
|
|
629
|
-
value: status.credentials.apiKey,
|
|
630
|
-
allowOverwrite: true
|
|
631
|
-
});
|
|
632
|
-
didOverwrite = true;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (isAlreadyCurrent) {
|
|
636
|
-
p.log.info(`VOCODER_API_KEY already matches your .env file`);
|
|
637
|
-
} else if (didOverwrite) {
|
|
638
|
-
p.log.success(`Updated VOCODER_API_KEY in .env`);
|
|
639
|
-
} else {
|
|
640
|
-
p.log.success(`Wrote VOCODER_API_KEY to .env`);
|
|
641
|
-
}
|
|
867
|
+
spinner3.stop("Setup complete!");
|
|
868
|
+
const { credentials } = status;
|
|
642
869
|
p.outro("Vocoder initialized successfully!");
|
|
643
|
-
|
|
644
|
-
|
|
870
|
+
runScaffold({
|
|
871
|
+
projectName: credentials.projectName,
|
|
872
|
+
organizationName: credentials.organizationName,
|
|
873
|
+
sourceLocale: credentials.sourceLocale,
|
|
874
|
+
translationTriggers: credentials.translationTriggers ?? ["push"]
|
|
875
|
+
});
|
|
645
876
|
return 0;
|
|
646
877
|
}
|
|
647
878
|
}
|
|
648
|
-
|
|
649
|
-
p.log.error("
|
|
879
|
+
spinner3.stop("Setup timed out");
|
|
880
|
+
p.log.error("Setup timed out. Run `vocoder init` again.");
|
|
650
881
|
p.cancel("Setup could not be completed.");
|
|
651
882
|
return 1;
|
|
652
883
|
} catch (error) {
|
|
653
|
-
|
|
654
|
-
if (error instanceof VocoderAPIError && error.limitError) {
|
|
655
|
-
printPlanLimitMessage(apiUrl, error.limitError.message);
|
|
656
|
-
p.log.info(`Current: ${error.limitError.current}`);
|
|
657
|
-
p.log.info(`Required: ${error.limitError.required}`);
|
|
658
|
-
p.log.info(`Upgrade: ${error.limitError.upgradeUrl}`);
|
|
659
|
-
return 1;
|
|
660
|
-
}
|
|
884
|
+
spinner3.stop();
|
|
661
885
|
if (error instanceof Error) {
|
|
662
886
|
if (isPlanLimitFailure(error.message)) {
|
|
663
887
|
printPlanLimitMessage(apiUrl, error.message);
|
|
@@ -676,7 +900,7 @@ import * as p2 from "@clack/prompts";
|
|
|
676
900
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
677
901
|
|
|
678
902
|
// src/utils/branch.ts
|
|
679
|
-
import { execSync as
|
|
903
|
+
import { execSync as execSync3 } from "child_process";
|
|
680
904
|
var REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
|
|
681
905
|
function escapeRegexChar(value) {
|
|
682
906
|
return value.replace(REGEX_SPECIAL_CHARS, "\\$&");
|
|
@@ -698,7 +922,7 @@ function detectBranch(override) {
|
|
|
698
922
|
return envBranch;
|
|
699
923
|
}
|
|
700
924
|
try {
|
|
701
|
-
const branch =
|
|
925
|
+
const branch = execSync3("git rev-parse --abbrev-ref HEAD", {
|
|
702
926
|
encoding: "utf-8",
|
|
703
927
|
stdio: ["pipe", "pipe", "ignore"]
|
|
704
928
|
}).trim();
|
|
@@ -742,12 +966,12 @@ function matchBranchPattern(branch, pattern) {
|
|
|
742
966
|
}
|
|
743
967
|
|
|
744
968
|
// src/commands/sync.ts
|
|
745
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync
|
|
969
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
746
970
|
|
|
747
971
|
// src/utils/config.ts
|
|
748
|
-
import
|
|
749
|
-
import { config as
|
|
750
|
-
|
|
972
|
+
import chalk2 from "chalk";
|
|
973
|
+
import { config as loadEnv2 } from "dotenv";
|
|
974
|
+
loadEnv2();
|
|
751
975
|
function validateLocalConfig(config) {
|
|
752
976
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
753
977
|
throw new Error("VOCODER_API_KEY is required. Set it in your .env file.");
|
|
@@ -837,19 +1061,19 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
837
1061
|
configSources.noFallback = "environment";
|
|
838
1062
|
}
|
|
839
1063
|
if (verbose) {
|
|
840
|
-
console.log(
|
|
841
|
-
console.log(
|
|
1064
|
+
console.log(chalk2.dim("\n Configuration sources:"));
|
|
1065
|
+
console.log(chalk2.dim(` Include patterns: ${configSources.extractionPattern}`));
|
|
842
1066
|
if (excludePattern.length > 0) {
|
|
843
|
-
console.log(
|
|
1067
|
+
console.log(chalk2.dim(` Exclude patterns: ${configSources.excludePattern}`));
|
|
844
1068
|
}
|
|
845
|
-
console.log(
|
|
846
|
-
console.log(
|
|
1069
|
+
console.log(chalk2.dim(` API key: ${configSources.apiKey}`));
|
|
1070
|
+
console.log(chalk2.dim(` API URL: ${configSources.apiUrl}
|
|
847
1071
|
`));
|
|
848
|
-
console.log(
|
|
1072
|
+
console.log(chalk2.dim(` Sync mode: ${configSources.mode}`));
|
|
849
1073
|
if (maxWaitMs) {
|
|
850
|
-
console.log(
|
|
1074
|
+
console.log(chalk2.dim(` Max wait: ${configSources.maxWaitMs}`));
|
|
851
1075
|
}
|
|
852
|
-
console.log(
|
|
1076
|
+
console.log(chalk2.dim(` No fallback: ${configSources.noFallback}
|
|
853
1077
|
`));
|
|
854
1078
|
}
|
|
855
1079
|
return {
|
|
@@ -1137,7 +1361,7 @@ var StringExtractor = class {
|
|
|
1137
1361
|
};
|
|
1138
1362
|
|
|
1139
1363
|
// src/commands/sync.ts
|
|
1140
|
-
import
|
|
1364
|
+
import chalk3 from "chalk";
|
|
1141
1365
|
import { join as join2 } from "path";
|
|
1142
1366
|
function isRecord(value) {
|
|
1143
1367
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1186,7 +1410,7 @@ function getCacheFilePath(projectRoot, branch) {
|
|
|
1186
1410
|
const slug = branch.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
|
|
1187
1411
|
const branchHash = createHash2("sha1").update(branch).digest("hex").slice(0, 12);
|
|
1188
1412
|
const filename = `${slug || "branch"}-${branchHash}.json`;
|
|
1189
|
-
return join2(projectRoot, ".vocoder", "cache", "sync", filename);
|
|
1413
|
+
return join2(projectRoot, "node_modules", ".vocoder", "cache", "sync", filename);
|
|
1190
1414
|
}
|
|
1191
1415
|
function readLocalSnapshotCache(params) {
|
|
1192
1416
|
const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
|
|
@@ -1222,7 +1446,7 @@ function readLocalSnapshotCache(params) {
|
|
|
1222
1446
|
}
|
|
1223
1447
|
function writeLocalSnapshotCache(params) {
|
|
1224
1448
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
|
|
1225
|
-
mkdirSync(join2(params.projectRoot, ".vocoder", "cache", "sync"), {
|
|
1449
|
+
mkdirSync(join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"), {
|
|
1226
1450
|
recursive: true
|
|
1227
1451
|
});
|
|
1228
1452
|
const payload = {
|
|
@@ -1236,7 +1460,7 @@ function writeLocalSnapshotCache(params) {
|
|
|
1236
1460
|
...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
|
|
1237
1461
|
translations: params.translations
|
|
1238
1462
|
};
|
|
1239
|
-
|
|
1463
|
+
writeFileSync(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
1240
1464
|
return cacheFilePath;
|
|
1241
1465
|
}
|
|
1242
1466
|
function resolveEffectiveModeFromPolicy(params) {
|
|
@@ -1392,12 +1616,12 @@ async function sync(options = {}) {
|
|
|
1392
1616
|
const startTime = Date.now();
|
|
1393
1617
|
const projectRoot = process.cwd();
|
|
1394
1618
|
p2.intro("Vocoder Sync");
|
|
1395
|
-
const
|
|
1619
|
+
const spinner3 = p2.spinner();
|
|
1396
1620
|
try {
|
|
1397
|
-
|
|
1621
|
+
spinner3.start("Detecting branch");
|
|
1398
1622
|
const branch = detectBranch(options.branch);
|
|
1399
|
-
|
|
1400
|
-
|
|
1623
|
+
spinner3.stop(`Branch: ${chalk3.cyan(branch)}`);
|
|
1624
|
+
spinner3.start("Loading project configuration");
|
|
1401
1625
|
const mergedConfig = await getMergedConfig(options, options.verbose);
|
|
1402
1626
|
const localConfig = {
|
|
1403
1627
|
apiKey: mergedConfig.apiKey || "",
|
|
@@ -1419,10 +1643,10 @@ async function sync(options = {}) {
|
|
|
1419
1643
|
excludePattern: mergedConfig.excludePattern,
|
|
1420
1644
|
timeout: waitTimeoutMs
|
|
1421
1645
|
};
|
|
1422
|
-
|
|
1646
|
+
spinner3.stop("Project configuration loaded");
|
|
1423
1647
|
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
1424
1648
|
p2.log.warn(
|
|
1425
|
-
`Skipping translations (${
|
|
1649
|
+
`Skipping translations (${chalk3.cyan(branch)} is not a target branch)`
|
|
1426
1650
|
);
|
|
1427
1651
|
p2.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
|
|
1428
1652
|
p2.log.info("Use --force to translate anyway");
|
|
@@ -1430,7 +1654,7 @@ async function sync(options = {}) {
|
|
|
1430
1654
|
return 0;
|
|
1431
1655
|
}
|
|
1432
1656
|
const patternsDisplay = Array.isArray(config.extractionPattern) ? config.extractionPattern.join(", ") : config.extractionPattern;
|
|
1433
|
-
|
|
1657
|
+
spinner3.start(`Extracting strings from ${patternsDisplay}`);
|
|
1434
1658
|
const extractor = new StringExtractor();
|
|
1435
1659
|
const extractedStrings = await extractor.extractFromProject(
|
|
1436
1660
|
config.extractionPattern,
|
|
@@ -1438,13 +1662,13 @@ async function sync(options = {}) {
|
|
|
1438
1662
|
config.excludePattern
|
|
1439
1663
|
);
|
|
1440
1664
|
if (extractedStrings.length === 0) {
|
|
1441
|
-
|
|
1442
|
-
p2.log.warn("Make sure you are
|
|
1665
|
+
spinner3.stop("No translatable strings found");
|
|
1666
|
+
p2.log.warn("Make sure you are wrapping translatable strings with Vocoder");
|
|
1443
1667
|
p2.outro("");
|
|
1444
1668
|
return 0;
|
|
1445
1669
|
}
|
|
1446
|
-
|
|
1447
|
-
`Extracted ${
|
|
1670
|
+
spinner3.stop(
|
|
1671
|
+
`Extracted ${chalk3.cyan(extractedStrings.length)} strings from ${chalk3.cyan(patternsDisplay)}`
|
|
1448
1672
|
);
|
|
1449
1673
|
if (options.verbose) {
|
|
1450
1674
|
const sampleLines = extractedStrings.slice(0, 5).map((s) => ` "${s.text}" (${s.file}:${s.line})`);
|
|
@@ -1481,7 +1705,7 @@ async function sync(options = {}) {
|
|
|
1481
1705
|
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
|
|
1482
1706
|
);
|
|
1483
1707
|
}
|
|
1484
|
-
|
|
1708
|
+
spinner3.start("Submitting strings to Vocoder API");
|
|
1485
1709
|
const batchResponse = await api.submitTranslation(
|
|
1486
1710
|
branch,
|
|
1487
1711
|
stringEntries,
|
|
@@ -1493,7 +1717,7 @@ async function sync(options = {}) {
|
|
|
1493
1717
|
},
|
|
1494
1718
|
repoIdentity ?? void 0
|
|
1495
1719
|
);
|
|
1496
|
-
|
|
1720
|
+
spinner3.stop(`Submitted to API - Batch ${chalk3.cyan(batchResponse.batchId)}`);
|
|
1497
1721
|
const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
|
|
1498
1722
|
branch,
|
|
1499
1723
|
requestedMode,
|
|
@@ -1510,13 +1734,13 @@ async function sync(options = {}) {
|
|
|
1510
1734
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
1511
1735
|
p2.log.success("No changes detected - strings are up to date");
|
|
1512
1736
|
}
|
|
1513
|
-
p2.log.info(`New strings: ${
|
|
1737
|
+
p2.log.info(`New strings: ${chalk3.cyan(batchResponse.newStrings)}`);
|
|
1514
1738
|
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
1515
1739
|
p2.log.info(
|
|
1516
|
-
`Deleted strings: ${
|
|
1740
|
+
`Deleted strings: ${chalk3.yellow(batchResponse.deletedStrings)} (archived)`
|
|
1517
1741
|
);
|
|
1518
1742
|
}
|
|
1519
|
-
p2.log.info(`Total strings: ${
|
|
1743
|
+
p2.log.info(`Total strings: ${chalk3.cyan(batchResponse.totalStrings)}`);
|
|
1520
1744
|
if (batchResponse.newStrings === 0) {
|
|
1521
1745
|
p2.log.success("No new strings - using existing translations");
|
|
1522
1746
|
} else {
|
|
@@ -1536,7 +1760,7 @@ async function sync(options = {}) {
|
|
|
1536
1760
|
}
|
|
1537
1761
|
let waitError = null;
|
|
1538
1762
|
if (!artifacts && (effectiveMode === "required" || effectiveMode === "best-effort")) {
|
|
1539
|
-
|
|
1763
|
+
spinner3.start(`Waiting for translations (max ${waitTimeoutMs}ms)`);
|
|
1540
1764
|
let lastProgress = 0;
|
|
1541
1765
|
try {
|
|
1542
1766
|
const completion = await api.waitForCompletion(
|
|
@@ -1545,7 +1769,7 @@ async function sync(options = {}) {
|
|
|
1545
1769
|
(progress) => {
|
|
1546
1770
|
const percent = Math.round(progress * 100);
|
|
1547
1771
|
if (percent > lastProgress) {
|
|
1548
|
-
|
|
1772
|
+
spinner3.message(`Translating... ${percent}%`);
|
|
1549
1773
|
lastProgress = percent;
|
|
1550
1774
|
}
|
|
1551
1775
|
}
|
|
@@ -1555,9 +1779,9 @@ async function sync(options = {}) {
|
|
|
1555
1779
|
translations: completion.translations,
|
|
1556
1780
|
localeMetadata: completion.localeMetadata
|
|
1557
1781
|
};
|
|
1558
|
-
|
|
1782
|
+
spinner3.stop("Translations complete");
|
|
1559
1783
|
} catch (error) {
|
|
1560
|
-
|
|
1784
|
+
spinner3.stop("Translation wait incomplete");
|
|
1561
1785
|
waitError = error instanceof Error ? error : new Error(String(error));
|
|
1562
1786
|
if (effectiveMode === "required") {
|
|
1563
1787
|
throw waitError;
|
|
@@ -1571,7 +1795,7 @@ async function sync(options = {}) {
|
|
|
1571
1795
|
"Fresh translations are not available and fallback is disabled (--no-fallback)."
|
|
1572
1796
|
);
|
|
1573
1797
|
}
|
|
1574
|
-
|
|
1798
|
+
spinner3.start("Loading fallback translations");
|
|
1575
1799
|
const localFallback = readLocalSnapshotCache({
|
|
1576
1800
|
projectRoot,
|
|
1577
1801
|
branch
|
|
@@ -1579,7 +1803,7 @@ async function sync(options = {}) {
|
|
|
1579
1803
|
if (localFallback) {
|
|
1580
1804
|
artifacts = localFallback;
|
|
1581
1805
|
const cacheBranchLabel = localFallback.cacheBranch && localFallback.cacheBranch !== branch ? `${localFallback.cacheBranch} fallback` : localFallback.cacheBranch || branch;
|
|
1582
|
-
|
|
1806
|
+
spinner3.stop(`Using local cached snapshot (${cacheBranchLabel})`);
|
|
1583
1807
|
} else {
|
|
1584
1808
|
try {
|
|
1585
1809
|
const apiSnapshot = await fetchApiSnapshot(api, {
|
|
@@ -1588,12 +1812,12 @@ async function sync(options = {}) {
|
|
|
1588
1812
|
});
|
|
1589
1813
|
if (apiSnapshot) {
|
|
1590
1814
|
artifacts = apiSnapshot;
|
|
1591
|
-
|
|
1815
|
+
spinner3.stop("Using latest completed API snapshot");
|
|
1592
1816
|
} else {
|
|
1593
|
-
|
|
1817
|
+
spinner3.stop("No completed API snapshot available");
|
|
1594
1818
|
}
|
|
1595
1819
|
} catch (error) {
|
|
1596
|
-
|
|
1820
|
+
spinner3.stop("Failed to fetch API snapshot");
|
|
1597
1821
|
if (options.verbose) {
|
|
1598
1822
|
const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
|
|
1599
1823
|
p2.log.warn(`Snapshot fetch error: ${message}`);
|
|
@@ -1649,7 +1873,7 @@ async function sync(options = {}) {
|
|
|
1649
1873
|
p2.log.info("Just use <VocoderProvider> and <T> \u2014 no manual imports needed.");
|
|
1650
1874
|
return 0;
|
|
1651
1875
|
} catch (error) {
|
|
1652
|
-
|
|
1876
|
+
spinner3.stop();
|
|
1653
1877
|
if (error instanceof VocoderAPIError && error.syncPolicyError) {
|
|
1654
1878
|
p2.log.error(error.syncPolicyError.message);
|
|
1655
1879
|
const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
|
|
@@ -1670,9 +1894,12 @@ async function sync(options = {}) {
|
|
|
1670
1894
|
if (error instanceof Error) {
|
|
1671
1895
|
p2.log.error(error.message);
|
|
1672
1896
|
if (error.message.includes("VOCODER_API_KEY")) {
|
|
1673
|
-
p2.log.warn("
|
|
1674
|
-
p2.log.info(
|
|
1675
|
-
p2.log.info("
|
|
1897
|
+
p2.log.warn("VOCODER_API_KEY is only needed for `vocoder sync` (CLI push).");
|
|
1898
|
+
p2.log.info(" Create one at: https://vocoder.app/dashboard");
|
|
1899
|
+
p2.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
|
|
1900
|
+
p2.log.info("");
|
|
1901
|
+
p2.log.info(" Note: If you use @vocoder/unplugin, `vocoder sync` is optional.");
|
|
1902
|
+
p2.log.info(" Translations are fetched automatically at build time.");
|
|
1676
1903
|
} else if (error.message.includes("git branch")) {
|
|
1677
1904
|
p2.log.warn("Run from a git repository, or use:");
|
|
1678
1905
|
p2.log.info(" vocoder sync --branch main");
|
|
@@ -1685,1159 +1912,6 @@ async function sync(options = {}) {
|
|
|
1685
1912
|
}
|
|
1686
1913
|
}
|
|
1687
1914
|
|
|
1688
|
-
// src/commands/wrap.ts
|
|
1689
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1690
|
-
import { relative as relative2 } from "path";
|
|
1691
|
-
import * as p3 from "@clack/prompts";
|
|
1692
|
-
import chalk3 from "chalk";
|
|
1693
|
-
|
|
1694
|
-
// src/utils/wrap/analyzer.ts
|
|
1695
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
1696
|
-
import { parse as parse2 } from "@babel/parser";
|
|
1697
|
-
import babelTraverse2 from "@babel/traverse";
|
|
1698
|
-
import { glob as glob2 } from "glob";
|
|
1699
|
-
|
|
1700
|
-
// src/utils/wrap/heuristics.ts
|
|
1701
|
-
var URL_REGEX = /^(https?:\/\/|\/\/|mailto:|tel:|ftp:\/\/)/i;
|
|
1702
|
-
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1703
|
-
var FILE_PATH_REGEX = /^(\.{0,2}\/|[a-zA-Z]:\\)/;
|
|
1704
|
-
var COLOR_HEX_REGEX = /^#([0-9a-fA-F]{3,8})$/;
|
|
1705
|
-
var COLOR_FUNC_REGEX = /^(rgb|rgba|hsl|hsla)\s*\(/i;
|
|
1706
|
-
var CAMEL_CASE_REGEX = /^[a-z][a-zA-Z0-9]*$/;
|
|
1707
|
-
var PASCAL_CASE_REGEX = /^[A-Z][a-zA-Z0-9]*$/;
|
|
1708
|
-
var SCREAMING_SNAKE_REGEX = /^[A-Z][A-Z0-9_]+$/;
|
|
1709
|
-
var KEBAB_CASE_REGEX = /^[a-z][a-z0-9-]+$/;
|
|
1710
|
-
var MIME_TYPE_REGEX = /^(application|text|image|audio|video|font|multipart)\//;
|
|
1711
|
-
var DATE_FORMAT_REGEX = /^[YMDHhmsaAZz\-\/\.\s:,]+$/;
|
|
1712
|
-
var CSS_UNIT_REGEX = /^\d+(\.\d+)?(px|em|rem|vh|vw|%|ch|ex|pt|pc|in|cm|mm)$/;
|
|
1713
|
-
var TAILWIND_REGEX = /^[a-z][\w-]*(\s+[a-z][\w-]*)*$/;
|
|
1714
|
-
var TAILWIND_PREFIXES = [
|
|
1715
|
-
"flex",
|
|
1716
|
-
"grid",
|
|
1717
|
-
"block",
|
|
1718
|
-
"inline",
|
|
1719
|
-
"hidden",
|
|
1720
|
-
"absolute",
|
|
1721
|
-
"relative",
|
|
1722
|
-
"fixed",
|
|
1723
|
-
"sticky",
|
|
1724
|
-
"top",
|
|
1725
|
-
"bottom",
|
|
1726
|
-
"left",
|
|
1727
|
-
"right",
|
|
1728
|
-
"inset",
|
|
1729
|
-
"w-",
|
|
1730
|
-
"h-",
|
|
1731
|
-
"min-",
|
|
1732
|
-
"max-",
|
|
1733
|
-
"p-",
|
|
1734
|
-
"px-",
|
|
1735
|
-
"py-",
|
|
1736
|
-
"pt-",
|
|
1737
|
-
"pb-",
|
|
1738
|
-
"pl-",
|
|
1739
|
-
"pr-",
|
|
1740
|
-
"m-",
|
|
1741
|
-
"mx-",
|
|
1742
|
-
"my-",
|
|
1743
|
-
"mt-",
|
|
1744
|
-
"mb-",
|
|
1745
|
-
"ml-",
|
|
1746
|
-
"mr-",
|
|
1747
|
-
"text-",
|
|
1748
|
-
"font-",
|
|
1749
|
-
"leading-",
|
|
1750
|
-
"tracking-",
|
|
1751
|
-
"bg-",
|
|
1752
|
-
"border-",
|
|
1753
|
-
"rounded-",
|
|
1754
|
-
"shadow-",
|
|
1755
|
-
"opacity-",
|
|
1756
|
-
"z-",
|
|
1757
|
-
"gap-",
|
|
1758
|
-
"space-",
|
|
1759
|
-
"items-",
|
|
1760
|
-
"justify-",
|
|
1761
|
-
"self-",
|
|
1762
|
-
"place-",
|
|
1763
|
-
"overflow-",
|
|
1764
|
-
"cursor-",
|
|
1765
|
-
"transition-",
|
|
1766
|
-
"duration-",
|
|
1767
|
-
"ease-",
|
|
1768
|
-
"sm:",
|
|
1769
|
-
"md:",
|
|
1770
|
-
"lg:",
|
|
1771
|
-
"xl:",
|
|
1772
|
-
"2xl:",
|
|
1773
|
-
"dark:",
|
|
1774
|
-
"hover:",
|
|
1775
|
-
"focus:",
|
|
1776
|
-
"active:",
|
|
1777
|
-
"group-",
|
|
1778
|
-
"peer-"
|
|
1779
|
-
];
|
|
1780
|
-
var NON_TRANSLATABLE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
1781
|
-
"className",
|
|
1782
|
-
"class",
|
|
1783
|
-
"href",
|
|
1784
|
-
"src",
|
|
1785
|
-
"id",
|
|
1786
|
-
"key",
|
|
1787
|
-
"ref",
|
|
1788
|
-
"style",
|
|
1789
|
-
"data-testid",
|
|
1790
|
-
"data-cy",
|
|
1791
|
-
"data-test",
|
|
1792
|
-
"type",
|
|
1793
|
-
"name",
|
|
1794
|
-
"value",
|
|
1795
|
-
"action",
|
|
1796
|
-
"method",
|
|
1797
|
-
"encType",
|
|
1798
|
-
"target",
|
|
1799
|
-
"rel",
|
|
1800
|
-
"role",
|
|
1801
|
-
"tabIndex",
|
|
1802
|
-
"htmlFor",
|
|
1803
|
-
"for",
|
|
1804
|
-
"width",
|
|
1805
|
-
"height",
|
|
1806
|
-
"viewBox",
|
|
1807
|
-
"xmlns",
|
|
1808
|
-
"fill",
|
|
1809
|
-
"stroke",
|
|
1810
|
-
"onClick",
|
|
1811
|
-
"onChange",
|
|
1812
|
-
"onSubmit",
|
|
1813
|
-
"onBlur",
|
|
1814
|
-
"onFocus",
|
|
1815
|
-
"onKeyDown",
|
|
1816
|
-
"onKeyUp",
|
|
1817
|
-
"onKeyPress",
|
|
1818
|
-
"onMouseEnter",
|
|
1819
|
-
"onMouseLeave"
|
|
1820
|
-
]);
|
|
1821
|
-
var TRANSLATABLE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
1822
|
-
"title",
|
|
1823
|
-
"placeholder",
|
|
1824
|
-
"alt",
|
|
1825
|
-
"aria-label",
|
|
1826
|
-
"aria-description",
|
|
1827
|
-
"aria-placeholder",
|
|
1828
|
-
"aria-roledescription",
|
|
1829
|
-
"aria-valuetext",
|
|
1830
|
-
"label",
|
|
1831
|
-
"description",
|
|
1832
|
-
"message",
|
|
1833
|
-
"heading",
|
|
1834
|
-
"caption",
|
|
1835
|
-
"helperText",
|
|
1836
|
-
"errorMessage",
|
|
1837
|
-
"successMessage",
|
|
1838
|
-
"tooltip"
|
|
1839
|
-
]);
|
|
1840
|
-
var NON_TRANSLATABLE_CALLS = /* @__PURE__ */ new Set([
|
|
1841
|
-
"console.log",
|
|
1842
|
-
"console.warn",
|
|
1843
|
-
"console.error",
|
|
1844
|
-
"console.info",
|
|
1845
|
-
"console.debug",
|
|
1846
|
-
"require",
|
|
1847
|
-
"import",
|
|
1848
|
-
"addEventListener",
|
|
1849
|
-
"removeEventListener",
|
|
1850
|
-
"querySelector",
|
|
1851
|
-
"querySelectorAll",
|
|
1852
|
-
"getElementById",
|
|
1853
|
-
"getAttribute",
|
|
1854
|
-
"setAttribute",
|
|
1855
|
-
"createElement",
|
|
1856
|
-
"JSON.parse",
|
|
1857
|
-
"JSON.stringify",
|
|
1858
|
-
"parseInt",
|
|
1859
|
-
"parseFloat",
|
|
1860
|
-
"encodeURIComponent",
|
|
1861
|
-
"decodeURIComponent",
|
|
1862
|
-
"encodeURI",
|
|
1863
|
-
"decodeURI",
|
|
1864
|
-
"RegExp"
|
|
1865
|
-
]);
|
|
1866
|
-
var TRANSLATABLE_VAR_NAMES = /* @__PURE__ */ new Set([
|
|
1867
|
-
"label",
|
|
1868
|
-
"message",
|
|
1869
|
-
"title",
|
|
1870
|
-
"description",
|
|
1871
|
-
"heading",
|
|
1872
|
-
"text",
|
|
1873
|
-
"caption",
|
|
1874
|
-
"subtitle",
|
|
1875
|
-
"tooltip",
|
|
1876
|
-
"errorMessage",
|
|
1877
|
-
"successMessage",
|
|
1878
|
-
"warningMessage",
|
|
1879
|
-
"infoMessage",
|
|
1880
|
-
"placeholder",
|
|
1881
|
-
"helperText",
|
|
1882
|
-
"hint",
|
|
1883
|
-
"buttonText",
|
|
1884
|
-
"linkText",
|
|
1885
|
-
"headerText",
|
|
1886
|
-
"footerText",
|
|
1887
|
-
"confirmText",
|
|
1888
|
-
"cancelText",
|
|
1889
|
-
"submitText",
|
|
1890
|
-
"greeting",
|
|
1891
|
-
"welcome",
|
|
1892
|
-
"instructions"
|
|
1893
|
-
]);
|
|
1894
|
-
function classifyString(text, context, metadata = {}) {
|
|
1895
|
-
const trimmed = text.trim();
|
|
1896
|
-
if (trimmed.length === 0) {
|
|
1897
|
-
return { translatable: false, confidence: "high", reason: "Empty or whitespace-only" };
|
|
1898
|
-
}
|
|
1899
|
-
if (trimmed.length === 1) {
|
|
1900
|
-
return { translatable: false, confidence: "high", reason: "Single character" };
|
|
1901
|
-
}
|
|
1902
|
-
if (!/[a-zA-Z]/.test(trimmed)) {
|
|
1903
|
-
return { translatable: false, confidence: "high", reason: "No alphabetic characters" };
|
|
1904
|
-
}
|
|
1905
|
-
if (URL_REGEX.test(trimmed)) {
|
|
1906
|
-
return { translatable: false, confidence: "high", reason: "URL" };
|
|
1907
|
-
}
|
|
1908
|
-
if (EMAIL_REGEX.test(trimmed)) {
|
|
1909
|
-
return { translatable: false, confidence: "high", reason: "Email address" };
|
|
1910
|
-
}
|
|
1911
|
-
if (FILE_PATH_REGEX.test(trimmed) && !trimmed.includes(" ")) {
|
|
1912
|
-
return { translatable: false, confidence: "high", reason: "File path" };
|
|
1913
|
-
}
|
|
1914
|
-
if (COLOR_HEX_REGEX.test(trimmed) || COLOR_FUNC_REGEX.test(trimmed)) {
|
|
1915
|
-
return { translatable: false, confidence: "high", reason: "Color code" };
|
|
1916
|
-
}
|
|
1917
|
-
if (CSS_UNIT_REGEX.test(trimmed)) {
|
|
1918
|
-
return { translatable: false, confidence: "high", reason: "CSS unit value" };
|
|
1919
|
-
}
|
|
1920
|
-
if (MIME_TYPE_REGEX.test(trimmed)) {
|
|
1921
|
-
return { translatable: false, confidence: "high", reason: "MIME type" };
|
|
1922
|
-
}
|
|
1923
|
-
if (DATE_FORMAT_REGEX.test(trimmed) && trimmed.length > 1) {
|
|
1924
|
-
return { translatable: false, confidence: "high", reason: "Date format string" };
|
|
1925
|
-
}
|
|
1926
|
-
if (context === "jsx-attribute" && metadata.attributeName) {
|
|
1927
|
-
if (NON_TRANSLATABLE_ATTRIBUTES.has(metadata.attributeName)) {
|
|
1928
|
-
return { translatable: false, confidence: "high", reason: `Non-translatable attribute: ${metadata.attributeName}` };
|
|
1929
|
-
}
|
|
1930
|
-
if (metadata.attributeName.startsWith("data-") && !TRANSLATABLE_ATTRIBUTES.has(metadata.attributeName)) {
|
|
1931
|
-
return { translatable: false, confidence: "high", reason: "data-* attribute" };
|
|
1932
|
-
}
|
|
1933
|
-
if (metadata.attributeName.startsWith("on") && metadata.attributeName.length > 2) {
|
|
1934
|
-
const thirdChar = metadata.attributeName[2];
|
|
1935
|
-
if (thirdChar && thirdChar === thirdChar.toUpperCase()) {
|
|
1936
|
-
return { translatable: false, confidence: "high", reason: "Event handler attribute" };
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
if (TRANSLATABLE_ATTRIBUTES.has(metadata.attributeName)) {
|
|
1940
|
-
return { translatable: true, confidence: "high", reason: `Translatable attribute: ${metadata.attributeName}` };
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
if (context === "jsx-text") {
|
|
1944
|
-
const hasWords = /[a-zA-Z]{2,}/.test(trimmed);
|
|
1945
|
-
if (hasWords) {
|
|
1946
|
-
return { translatable: true, confidence: "high", reason: "JSX text with words" };
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
if (!trimmed.includes(" ") && (CAMEL_CASE_REGEX.test(trimmed) || PASCAL_CASE_REGEX.test(trimmed) || SCREAMING_SNAKE_REGEX.test(trimmed) || KEBAB_CASE_REGEX.test(trimmed))) {
|
|
1950
|
-
return { translatable: false, confidence: "high", reason: "Code identifier" };
|
|
1951
|
-
}
|
|
1952
|
-
if (isTailwindClasses(trimmed)) {
|
|
1953
|
-
return { translatable: false, confidence: "high", reason: "CSS/Tailwind classes" };
|
|
1954
|
-
}
|
|
1955
|
-
if (metadata.isInsideCallExpression) {
|
|
1956
|
-
if (NON_TRANSLATABLE_CALLS.has(metadata.isInsideCallExpression)) {
|
|
1957
|
-
return { translatable: false, confidence: "high", reason: `Inside ${metadata.isInsideCallExpression}()` };
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
if (metadata.parentType === "ThrowStatement" || metadata.isInsideCallExpression === "Error") {
|
|
1961
|
-
return { translatable: false, confidence: "high", reason: "Error message" };
|
|
1962
|
-
}
|
|
1963
|
-
if ((context === "string-literal" || context === "template-literal") && metadata.parentType === "VariableDeclarator") {
|
|
1964
|
-
return { translatable: true, confidence: "medium", reason: "String in variable declaration" };
|
|
1965
|
-
}
|
|
1966
|
-
const wordCount = trimmed.split(/\s+/).length;
|
|
1967
|
-
if (wordCount >= 3) {
|
|
1968
|
-
return { translatable: true, confidence: "medium", reason: `Multi-word string (${wordCount} words)` };
|
|
1969
|
-
}
|
|
1970
|
-
if (wordCount === 2 && /[a-zA-Z]{2,}/.test(trimmed)) {
|
|
1971
|
-
return { translatable: true, confidence: "low", reason: "Short phrase (2 words)" };
|
|
1972
|
-
}
|
|
1973
|
-
if (/^[A-Z][a-z]/.test(trimmed) && context !== "string-literal") {
|
|
1974
|
-
return { translatable: true, confidence: "low", reason: "Capitalized word, possibly UI text" };
|
|
1975
|
-
}
|
|
1976
|
-
return { translatable: false, confidence: "low", reason: "Ambiguous single-word string" };
|
|
1977
|
-
}
|
|
1978
|
-
function isTranslatableVarName(name) {
|
|
1979
|
-
const lower = name.toLowerCase();
|
|
1980
|
-
for (const varName of TRANSLATABLE_VAR_NAMES) {
|
|
1981
|
-
if (lower === varName.toLowerCase() || lower.endsWith(varName.toLowerCase())) {
|
|
1982
|
-
return true;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
return false;
|
|
1986
|
-
}
|
|
1987
|
-
function isTailwindClasses(text) {
|
|
1988
|
-
if (!TAILWIND_REGEX.test(text)) return false;
|
|
1989
|
-
const parts = text.split(/\s+/);
|
|
1990
|
-
let tailwindCount = 0;
|
|
1991
|
-
for (const part of parts) {
|
|
1992
|
-
if (TAILWIND_PREFIXES.some((prefix) => part.startsWith(prefix))) {
|
|
1993
|
-
tailwindCount++;
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
return tailwindCount > parts.length / 2;
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
// src/utils/wrap/analyzer.ts
|
|
2000
|
-
var traverse2 = babelTraverse2.default || babelTraverse2;
|
|
2001
|
-
var StringAnalyzer = class {
|
|
2002
|
-
constructor(adapter) {
|
|
2003
|
-
this.adapter = adapter;
|
|
2004
|
-
}
|
|
2005
|
-
/**
|
|
2006
|
-
* Analyze all files matching the given patterns and return wrap candidates.
|
|
2007
|
-
*/
|
|
2008
|
-
async analyzeProject(options, projectRoot = process.cwd()) {
|
|
2009
|
-
const includePatterns = options.include?.length ? options.include : ["src/**/*.{tsx,jsx,ts,js}"];
|
|
2010
|
-
const defaultIgnore = [
|
|
2011
|
-
"**/node_modules/**",
|
|
2012
|
-
"**/.next/**",
|
|
2013
|
-
"**/dist/**",
|
|
2014
|
-
"**/build/**",
|
|
2015
|
-
"**/*.test.*",
|
|
2016
|
-
"**/*.spec.*",
|
|
2017
|
-
"**/*.stories.*",
|
|
2018
|
-
"**/__tests__/**"
|
|
2019
|
-
];
|
|
2020
|
-
const ignorePatterns = options.exclude ? [...defaultIgnore, ...options.exclude] : defaultIgnore;
|
|
2021
|
-
const allFiles = /* @__PURE__ */ new Set();
|
|
2022
|
-
for (const pattern of includePatterns) {
|
|
2023
|
-
const files = await glob2(pattern, {
|
|
2024
|
-
cwd: projectRoot,
|
|
2025
|
-
absolute: true,
|
|
2026
|
-
ignore: ignorePatterns
|
|
2027
|
-
});
|
|
2028
|
-
files.forEach((file) => allFiles.add(file));
|
|
2029
|
-
}
|
|
2030
|
-
const allCandidates = [];
|
|
2031
|
-
for (const file of allFiles) {
|
|
2032
|
-
try {
|
|
2033
|
-
const candidates = this.analyzeFile(file);
|
|
2034
|
-
allCandidates.push(...candidates);
|
|
2035
|
-
} catch (error) {
|
|
2036
|
-
if (options.verbose) {
|
|
2037
|
-
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
2038
|
-
console.warn(`Warning: Failed to analyze ${file}: ${msg}`);
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
return allCandidates;
|
|
2043
|
-
}
|
|
2044
|
-
/**
|
|
2045
|
-
* Analyze a single file and return wrap candidates.
|
|
2046
|
-
*/
|
|
2047
|
-
analyzeFile(filePath) {
|
|
2048
|
-
const code = readFileSync4(filePath, "utf-8");
|
|
2049
|
-
return this.analyzeCode(code, filePath);
|
|
2050
|
-
}
|
|
2051
|
-
/**
|
|
2052
|
-
* Analyze source code and return wrap candidates.
|
|
2053
|
-
*/
|
|
2054
|
-
analyzeCode(code, filePath = "<input>") {
|
|
2055
|
-
const candidates = [];
|
|
2056
|
-
const ast = parse2(code, {
|
|
2057
|
-
sourceType: "module",
|
|
2058
|
-
plugins: ["jsx", "typescript"]
|
|
2059
|
-
});
|
|
2060
|
-
const vocoderImports = /* @__PURE__ */ new Map();
|
|
2061
|
-
const tFunctionNames = /* @__PURE__ */ new Set();
|
|
2062
|
-
traverse2(ast, {
|
|
2063
|
-
// Track imports from @vocoder/react
|
|
2064
|
-
ImportDeclaration: (path) => {
|
|
2065
|
-
const source = path.node.source.value;
|
|
2066
|
-
if (source === this.adapter.importSource) {
|
|
2067
|
-
path.node.specifiers.forEach((spec) => {
|
|
2068
|
-
if (spec.type === "ImportSpecifier") {
|
|
2069
|
-
const imported = spec.imported.type === "Identifier" ? spec.imported.name : null;
|
|
2070
|
-
const local = spec.local.name;
|
|
2071
|
-
if (imported === this.adapter.componentName) {
|
|
2072
|
-
vocoderImports.set(local, this.adapter.componentName);
|
|
2073
|
-
}
|
|
2074
|
-
if (imported === this.adapter.functionName) {
|
|
2075
|
-
tFunctionNames.add(local);
|
|
2076
|
-
}
|
|
2077
|
-
if (imported === this.adapter.hookName) {
|
|
2078
|
-
vocoderImports.set(local, this.adapter.hookName);
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
});
|
|
2082
|
-
}
|
|
2083
|
-
},
|
|
2084
|
-
// Track destructured t from useVocoder()
|
|
2085
|
-
VariableDeclarator: (path) => {
|
|
2086
|
-
const init2 = path.node.init;
|
|
2087
|
-
if (init2 && init2.type === "CallExpression" && init2.callee.type === "Identifier" && init2.callee.name === this.adapter.hookName && path.node.id.type === "ObjectPattern") {
|
|
2088
|
-
path.node.id.properties.forEach((prop) => {
|
|
2089
|
-
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === this.adapter.functionName) {
|
|
2090
|
-
const localName = prop.value.type === "Identifier" ? prop.value.name : this.adapter.functionName;
|
|
2091
|
-
tFunctionNames.add(localName);
|
|
2092
|
-
}
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2095
|
-
},
|
|
2096
|
-
// Find bare JSX text
|
|
2097
|
-
JSXText: (path) => {
|
|
2098
|
-
const text = path.node.value;
|
|
2099
|
-
const trimmed = text.trim();
|
|
2100
|
-
if (!trimmed) return;
|
|
2101
|
-
const ancestors = path.getAncestry().map((a) => a.node);
|
|
2102
|
-
if (this.adapter.isAlreadyWrapped(ancestors, vocoderImports)) return;
|
|
2103
|
-
const classification = classifyString(trimmed, "jsx-text", {
|
|
2104
|
-
isInsideComponent: true
|
|
2105
|
-
});
|
|
2106
|
-
if (classification.translatable) {
|
|
2107
|
-
candidates.push({
|
|
2108
|
-
file: filePath,
|
|
2109
|
-
line: path.node.loc?.start.line || 0,
|
|
2110
|
-
column: path.node.loc?.start.column || 0,
|
|
2111
|
-
text: trimmed,
|
|
2112
|
-
confidence: classification.confidence,
|
|
2113
|
-
strategy: "T-component",
|
|
2114
|
-
context: "jsx-text",
|
|
2115
|
-
reason: classification.reason
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
},
|
|
2119
|
-
// Find translatable JSX attributes
|
|
2120
|
-
JSXAttribute: (path) => {
|
|
2121
|
-
const attrName = path.node.name?.name;
|
|
2122
|
-
if (!attrName) return;
|
|
2123
|
-
const value = path.node.value;
|
|
2124
|
-
if (!value) return;
|
|
2125
|
-
let text = null;
|
|
2126
|
-
let context = "jsx-attribute";
|
|
2127
|
-
if (value.type === "StringLiteral") {
|
|
2128
|
-
text = value.value;
|
|
2129
|
-
} else if (value.type === "JSXExpressionContainer" && value.expression.type === "StringLiteral") {
|
|
2130
|
-
text = value.expression.value;
|
|
2131
|
-
}
|
|
2132
|
-
if (!text || !text.trim()) return;
|
|
2133
|
-
if (value.type === "JSXExpressionContainer" && value.expression.type === "CallExpression") {
|
|
2134
|
-
if (this.adapter.isAlreadyWrappedCall(value.expression, tFunctionNames)) return;
|
|
2135
|
-
}
|
|
2136
|
-
const classification = classifyString(text.trim(), context, {
|
|
2137
|
-
attributeName: attrName,
|
|
2138
|
-
isInsideComponent: true
|
|
2139
|
-
});
|
|
2140
|
-
if (classification.translatable) {
|
|
2141
|
-
candidates.push({
|
|
2142
|
-
file: filePath,
|
|
2143
|
-
line: path.node.loc?.start.line || 0,
|
|
2144
|
-
column: path.node.loc?.start.column || 0,
|
|
2145
|
-
text: text.trim(),
|
|
2146
|
-
confidence: classification.confidence,
|
|
2147
|
-
strategy: "t-function",
|
|
2148
|
-
context,
|
|
2149
|
-
reason: classification.reason
|
|
2150
|
-
});
|
|
2151
|
-
}
|
|
2152
|
-
},
|
|
2153
|
-
// Find string literals in non-JSX contexts
|
|
2154
|
-
StringLiteral: (path) => {
|
|
2155
|
-
if (path.parent.type === "ImportDeclaration") return;
|
|
2156
|
-
if (path.parent.type === "ExportDeclaration") return;
|
|
2157
|
-
if (path.parent.type === "JSXAttribute") return;
|
|
2158
|
-
if (path.parent.type === "JSXExpressionContainer" && path.parentPath?.parent?.type === "JSXAttribute") return;
|
|
2159
|
-
if (path.parent.type === "JSXExpressionContainer") return;
|
|
2160
|
-
if (path.parent.type === "ObjectProperty" && path.parent.key === path.node) return;
|
|
2161
|
-
if (path.parent.type === "TSLiteralType") return;
|
|
2162
|
-
if (isInsideTCall(path, tFunctionNames)) return;
|
|
2163
|
-
const text = path.node.value;
|
|
2164
|
-
if (!text.trim()) return;
|
|
2165
|
-
const callExpr = getEnclosingCallExpression(path);
|
|
2166
|
-
const parentType = path.parent.type;
|
|
2167
|
-
const classification = classifyString(text.trim(), "string-literal", {
|
|
2168
|
-
parentType,
|
|
2169
|
-
isInsideCallExpression: callExpr,
|
|
2170
|
-
isInsideComponent: false
|
|
2171
|
-
});
|
|
2172
|
-
let { confidence } = classification;
|
|
2173
|
-
if (parentType === "VariableDeclarator" && path.parent.id?.type === "Identifier") {
|
|
2174
|
-
const varName = path.parent.id.name;
|
|
2175
|
-
if (isTranslatableVarName(varName) && classification.translatable) {
|
|
2176
|
-
confidence = "high";
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
if (classification.translatable) {
|
|
2180
|
-
candidates.push({
|
|
2181
|
-
file: filePath,
|
|
2182
|
-
line: path.node.loc?.start.line || 0,
|
|
2183
|
-
column: path.node.loc?.start.column || 0,
|
|
2184
|
-
text: text.trim(),
|
|
2185
|
-
confidence,
|
|
2186
|
-
strategy: "t-function",
|
|
2187
|
-
context: "string-literal",
|
|
2188
|
-
reason: classification.reason
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
|
-
},
|
|
2192
|
-
// Find template literals
|
|
2193
|
-
TemplateLiteral: (path) => {
|
|
2194
|
-
if (path.parent.type === "ImportDeclaration") return;
|
|
2195
|
-
if (path.parent.type === "TaggedTemplateExpression") return;
|
|
2196
|
-
if (isInsideTCall(path, tFunctionNames)) return;
|
|
2197
|
-
const quasis = path.node.quasis;
|
|
2198
|
-
if (quasis.length === 0) return;
|
|
2199
|
-
const parts = [];
|
|
2200
|
-
for (let i = 0; i < quasis.length; i++) {
|
|
2201
|
-
const quasi = quasis[i];
|
|
2202
|
-
parts.push(quasi.value.raw);
|
|
2203
|
-
if (i < path.node.expressions.length) {
|
|
2204
|
-
const expr = path.node.expressions[i];
|
|
2205
|
-
if (expr.type === "Identifier") {
|
|
2206
|
-
parts.push(`{${expr.name}}`);
|
|
2207
|
-
} else {
|
|
2208
|
-
parts.push("{value}");
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
const text = parts.join("").trim();
|
|
2213
|
-
if (!text) return;
|
|
2214
|
-
const callExpr = getEnclosingCallExpression(path);
|
|
2215
|
-
const parentType = path.parent.type;
|
|
2216
|
-
const classification = classifyString(text, "template-literal", {
|
|
2217
|
-
parentType,
|
|
2218
|
-
isInsideCallExpression: callExpr,
|
|
2219
|
-
isInsideComponent: false
|
|
2220
|
-
});
|
|
2221
|
-
if (classification.translatable) {
|
|
2222
|
-
candidates.push({
|
|
2223
|
-
file: filePath,
|
|
2224
|
-
line: path.node.loc?.start.line || 0,
|
|
2225
|
-
column: path.node.loc?.start.column || 0,
|
|
2226
|
-
text,
|
|
2227
|
-
confidence: classification.confidence,
|
|
2228
|
-
strategy: "t-function",
|
|
2229
|
-
context: "template-literal",
|
|
2230
|
-
reason: classification.reason
|
|
2231
|
-
});
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
});
|
|
2235
|
-
return candidates;
|
|
2236
|
-
}
|
|
2237
|
-
};
|
|
2238
|
-
function isInsideTCall(path, tNames) {
|
|
2239
|
-
let current = path.parentPath;
|
|
2240
|
-
while (current) {
|
|
2241
|
-
if (current.node.type === "CallExpression") {
|
|
2242
|
-
const callee = current.node.callee;
|
|
2243
|
-
if (callee.type === "Identifier" && tNames.has(callee.name)) {
|
|
2244
|
-
return true;
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
current = current.parentPath;
|
|
2248
|
-
}
|
|
2249
|
-
return false;
|
|
2250
|
-
}
|
|
2251
|
-
function getEnclosingCallExpression(path) {
|
|
2252
|
-
let current = path.parentPath;
|
|
2253
|
-
while (current) {
|
|
2254
|
-
if (current.node.type === "CallExpression") {
|
|
2255
|
-
const callee = current.node.callee;
|
|
2256
|
-
if (callee.type === "Identifier") {
|
|
2257
|
-
return callee.name;
|
|
2258
|
-
}
|
|
2259
|
-
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.property.type === "Identifier") {
|
|
2260
|
-
return `${callee.object.name}.${callee.property.name}`;
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
if (current.node.type === "NewExpression") {
|
|
2264
|
-
const callee = current.node.callee;
|
|
2265
|
-
if (callee.type === "Identifier") {
|
|
2266
|
-
return callee.name;
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
current = current.parentPath;
|
|
2270
|
-
}
|
|
2271
|
-
return void 0;
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
// src/utils/wrap/transformer.ts
|
|
2275
|
-
import * as recast from "recast";
|
|
2276
|
-
import { parse as babelParse } from "@babel/parser";
|
|
2277
|
-
var babelParser = {
|
|
2278
|
-
parse(source) {
|
|
2279
|
-
return babelParse(source, {
|
|
2280
|
-
sourceType: "module",
|
|
2281
|
-
plugins: ["jsx", "typescript"],
|
|
2282
|
-
tokens: true
|
|
2283
|
-
});
|
|
2284
|
-
}
|
|
2285
|
-
};
|
|
2286
|
-
var StringTransformer = class {
|
|
2287
|
-
constructor(adapter) {
|
|
2288
|
-
this.adapter = adapter;
|
|
2289
|
-
}
|
|
2290
|
-
/**
|
|
2291
|
-
* Transform a file by wrapping the given candidates.
|
|
2292
|
-
* Returns the transformed source code.
|
|
2293
|
-
*/
|
|
2294
|
-
transform(code, candidates, filePath = "<input>") {
|
|
2295
|
-
const ast = recast.parse(code, { parser: babelParser });
|
|
2296
|
-
const b = recast.types.builders;
|
|
2297
|
-
const wrapped = [];
|
|
2298
|
-
const skipped = [];
|
|
2299
|
-
const usedStrategies = /* @__PURE__ */ new Set();
|
|
2300
|
-
const componentsNeedingHook = /* @__PURE__ */ new Set();
|
|
2301
|
-
const candidatesByLocation = /* @__PURE__ */ new Map();
|
|
2302
|
-
for (const c of candidates) {
|
|
2303
|
-
candidatesByLocation.set(`${c.line}:${c.column}`, c);
|
|
2304
|
-
}
|
|
2305
|
-
let existingImportDecl = null;
|
|
2306
|
-
const existingSpecifiers = /* @__PURE__ */ new Set();
|
|
2307
|
-
const adapter = this.adapter;
|
|
2308
|
-
recast.visit(ast, {
|
|
2309
|
-
visitImportDeclaration(path) {
|
|
2310
|
-
const source = path.node.source.value;
|
|
2311
|
-
if (source === adapter.importSource) {
|
|
2312
|
-
existingImportDecl = path;
|
|
2313
|
-
for (const spec of path.node.specifiers || []) {
|
|
2314
|
-
if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") {
|
|
2315
|
-
existingSpecifiers.add(spec.imported.name);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
this.traverse(path);
|
|
2320
|
-
},
|
|
2321
|
-
visitJSXText(path) {
|
|
2322
|
-
const loc = path.node.loc;
|
|
2323
|
-
if (!loc) {
|
|
2324
|
-
this.traverse(path);
|
|
2325
|
-
return;
|
|
2326
|
-
}
|
|
2327
|
-
const key = `${loc.start.line}:${loc.start.column}`;
|
|
2328
|
-
const candidate = candidatesByLocation.get(key);
|
|
2329
|
-
if (!candidate || candidate.strategy !== "T-component") {
|
|
2330
|
-
this.traverse(path);
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
const tOpen = b.jsxOpeningElement(
|
|
2334
|
-
b.jsxIdentifier(adapter.componentName),
|
|
2335
|
-
[]
|
|
2336
|
-
);
|
|
2337
|
-
const tClose = b.jsxClosingElement(
|
|
2338
|
-
b.jsxIdentifier(adapter.componentName)
|
|
2339
|
-
);
|
|
2340
|
-
const tElement = b.jsxElement(
|
|
2341
|
-
tOpen,
|
|
2342
|
-
tClose,
|
|
2343
|
-
[b.jsxText(candidate.text)]
|
|
2344
|
-
);
|
|
2345
|
-
path.replace(tElement);
|
|
2346
|
-
wrapped.push(candidate);
|
|
2347
|
-
usedStrategies.add("T-component");
|
|
2348
|
-
candidatesByLocation.delete(key);
|
|
2349
|
-
return false;
|
|
2350
|
-
},
|
|
2351
|
-
visitJSXAttribute(path) {
|
|
2352
|
-
const loc = path.node.loc;
|
|
2353
|
-
if (!loc) {
|
|
2354
|
-
this.traverse(path);
|
|
2355
|
-
return;
|
|
2356
|
-
}
|
|
2357
|
-
const key = `${loc.start.line}:${loc.start.column}`;
|
|
2358
|
-
const candidate = candidatesByLocation.get(key);
|
|
2359
|
-
if (!candidate || candidate.strategy !== "t-function") {
|
|
2360
|
-
this.traverse(path);
|
|
2361
|
-
return;
|
|
2362
|
-
}
|
|
2363
|
-
const value = path.node.value;
|
|
2364
|
-
if (!value) {
|
|
2365
|
-
this.traverse(path);
|
|
2366
|
-
return;
|
|
2367
|
-
}
|
|
2368
|
-
const tCall = b.callExpression(
|
|
2369
|
-
b.identifier(adapter.functionName),
|
|
2370
|
-
[b.stringLiteral(candidate.text)]
|
|
2371
|
-
);
|
|
2372
|
-
const exprContainer = b.jsxExpressionContainer(tCall);
|
|
2373
|
-
path.node.value = exprContainer;
|
|
2374
|
-
const componentFunc = findEnclosingComponent(path);
|
|
2375
|
-
if (componentFunc) {
|
|
2376
|
-
componentsNeedingHook.add(componentFunc);
|
|
2377
|
-
}
|
|
2378
|
-
wrapped.push(candidate);
|
|
2379
|
-
usedStrategies.add("t-function");
|
|
2380
|
-
candidatesByLocation.delete(key);
|
|
2381
|
-
this.traverse(path);
|
|
2382
|
-
},
|
|
2383
|
-
visitStringLiteral(path) {
|
|
2384
|
-
const loc = path.node.loc;
|
|
2385
|
-
if (!loc) {
|
|
2386
|
-
this.traverse(path);
|
|
2387
|
-
return;
|
|
2388
|
-
}
|
|
2389
|
-
const key = `${loc.start.line}:${loc.start.column}`;
|
|
2390
|
-
const candidate = candidatesByLocation.get(key);
|
|
2391
|
-
if (!candidate || candidate.strategy !== "t-function") {
|
|
2392
|
-
this.traverse(path);
|
|
2393
|
-
return;
|
|
2394
|
-
}
|
|
2395
|
-
if (path.parent.node.type === "JSXAttribute") {
|
|
2396
|
-
this.traverse(path);
|
|
2397
|
-
return;
|
|
2398
|
-
}
|
|
2399
|
-
const tCall = b.callExpression(
|
|
2400
|
-
b.identifier(adapter.functionName),
|
|
2401
|
-
[b.stringLiteral(candidate.text)]
|
|
2402
|
-
);
|
|
2403
|
-
path.replace(tCall);
|
|
2404
|
-
const componentFunc = findEnclosingComponent(path);
|
|
2405
|
-
if (componentFunc) {
|
|
2406
|
-
componentsNeedingHook.add(componentFunc);
|
|
2407
|
-
}
|
|
2408
|
-
wrapped.push(candidate);
|
|
2409
|
-
usedStrategies.add("t-function");
|
|
2410
|
-
candidatesByLocation.delete(key);
|
|
2411
|
-
return false;
|
|
2412
|
-
}
|
|
2413
|
-
});
|
|
2414
|
-
for (const candidate of candidatesByLocation.values()) {
|
|
2415
|
-
skipped.push(candidate);
|
|
2416
|
-
}
|
|
2417
|
-
if (componentsNeedingHook.size > 0) {
|
|
2418
|
-
this.injectUseVocoderHooks(ast, componentsNeedingHook, b);
|
|
2419
|
-
}
|
|
2420
|
-
this.manageImports(ast, usedStrategies, existingImportDecl, existingSpecifiers, componentsNeedingHook.size > 0, b);
|
|
2421
|
-
const output = recast.print(ast).code;
|
|
2422
|
-
return {
|
|
2423
|
-
file: filePath,
|
|
2424
|
-
output,
|
|
2425
|
-
wrappedCount: wrapped.length,
|
|
2426
|
-
wrapped,
|
|
2427
|
-
skipped
|
|
2428
|
-
};
|
|
2429
|
-
}
|
|
2430
|
-
/**
|
|
2431
|
-
* Inject `const { t } = useVocoder();` at the top of component functions.
|
|
2432
|
-
*/
|
|
2433
|
-
injectUseVocoderHooks(ast, componentFuncs, b) {
|
|
2434
|
-
const adapterFunctionName = this.adapter.functionName;
|
|
2435
|
-
const adapterHookName = this.adapter.hookName;
|
|
2436
|
-
const buildHookDecl = () => b.variableDeclaration("const", [
|
|
2437
|
-
b.variableDeclarator(
|
|
2438
|
-
b.objectPattern([
|
|
2439
|
-
b.property.from({
|
|
2440
|
-
kind: "init",
|
|
2441
|
-
key: b.identifier(adapterFunctionName),
|
|
2442
|
-
value: b.identifier(adapterFunctionName),
|
|
2443
|
-
shorthand: true
|
|
2444
|
-
})
|
|
2445
|
-
]),
|
|
2446
|
-
b.callExpression(b.identifier(adapterHookName), [])
|
|
2447
|
-
)
|
|
2448
|
-
]);
|
|
2449
|
-
recast.visit(ast, {
|
|
2450
|
-
visitFunction(path) {
|
|
2451
|
-
if (componentFuncs.has(path.node)) {
|
|
2452
|
-
const body = path.node.body;
|
|
2453
|
-
if (body.type === "BlockStatement") {
|
|
2454
|
-
const alreadyHasHook = body.body.some((stmt) => {
|
|
2455
|
-
if (stmt.type !== "VariableDeclaration") return false;
|
|
2456
|
-
return stmt.declarations.some(
|
|
2457
|
-
(decl) => decl.init?.type === "CallExpression" && decl.init.callee?.type === "Identifier" && decl.init.callee.name === "useVocoder"
|
|
2458
|
-
);
|
|
2459
|
-
});
|
|
2460
|
-
if (!alreadyHasHook) {
|
|
2461
|
-
body.body.unshift(buildHookDecl());
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
this.traverse(path);
|
|
2466
|
-
},
|
|
2467
|
-
visitArrowFunctionExpression(path) {
|
|
2468
|
-
if (componentFuncs.has(path.node)) {
|
|
2469
|
-
const body = path.node.body;
|
|
2470
|
-
if (body.type === "BlockStatement") {
|
|
2471
|
-
const alreadyHasHook = body.body.some((stmt) => {
|
|
2472
|
-
if (stmt.type !== "VariableDeclaration") return false;
|
|
2473
|
-
return stmt.declarations.some(
|
|
2474
|
-
(decl) => decl.init?.type === "CallExpression" && decl.init.callee?.type === "Identifier" && decl.init.callee.name === "useVocoder"
|
|
2475
|
-
);
|
|
2476
|
-
});
|
|
2477
|
-
if (!alreadyHasHook) {
|
|
2478
|
-
body.body.unshift(buildHookDecl());
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
this.traverse(path);
|
|
2483
|
-
}
|
|
2484
|
-
});
|
|
2485
|
-
}
|
|
2486
|
-
/**
|
|
2487
|
-
* Add or update @vocoder/react imports.
|
|
2488
|
-
*/
|
|
2489
|
-
manageImports(ast, usedStrategies, existingImportPath, existingSpecifiers, needsHook, b) {
|
|
2490
|
-
if (usedStrategies.size === 0) return;
|
|
2491
|
-
const neededSpecifiers = /* @__PURE__ */ new Set();
|
|
2492
|
-
if (usedStrategies.has("T-component")) {
|
|
2493
|
-
neededSpecifiers.add(this.adapter.componentName);
|
|
2494
|
-
}
|
|
2495
|
-
if (usedStrategies.has("t-function") && needsHook) {
|
|
2496
|
-
neededSpecifiers.add(this.adapter.hookName);
|
|
2497
|
-
}
|
|
2498
|
-
const missingSpecifiers = [];
|
|
2499
|
-
for (const spec of neededSpecifiers) {
|
|
2500
|
-
if (!existingSpecifiers.has(spec)) {
|
|
2501
|
-
missingSpecifiers.push(spec);
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
if (missingSpecifiers.length === 0) return;
|
|
2505
|
-
if (existingImportPath) {
|
|
2506
|
-
for (const name of missingSpecifiers) {
|
|
2507
|
-
const specifier = b.importSpecifier(b.identifier(name), b.identifier(name));
|
|
2508
|
-
existingImportPath.node.specifiers.push(specifier);
|
|
2509
|
-
}
|
|
2510
|
-
} else {
|
|
2511
|
-
const specifiers = missingSpecifiers.map(
|
|
2512
|
-
(name) => b.importSpecifier(b.identifier(name), b.identifier(name))
|
|
2513
|
-
);
|
|
2514
|
-
const importDecl = b.importDeclaration(
|
|
2515
|
-
specifiers,
|
|
2516
|
-
b.stringLiteral(this.adapter.importSource)
|
|
2517
|
-
);
|
|
2518
|
-
const body = ast.program.body;
|
|
2519
|
-
let lastImportIndex = -1;
|
|
2520
|
-
for (let i = 0; i < body.length; i++) {
|
|
2521
|
-
if (body[i].type === "ImportDeclaration") {
|
|
2522
|
-
lastImportIndex = i;
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
if (lastImportIndex >= 0) {
|
|
2526
|
-
body.splice(lastImportIndex + 1, 0, importDecl);
|
|
2527
|
-
} else {
|
|
2528
|
-
body.unshift(importDecl);
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
};
|
|
2533
|
-
function findEnclosingComponent(path) {
|
|
2534
|
-
let current = path.parent;
|
|
2535
|
-
while (current) {
|
|
2536
|
-
const node = current.node;
|
|
2537
|
-
if (node.type === "FunctionDeclaration" && node.id?.name) {
|
|
2538
|
-
const name = node.id.name;
|
|
2539
|
-
if (/^[A-Z]/.test(name)) return node;
|
|
2540
|
-
}
|
|
2541
|
-
if (node.type === "ArrowFunctionExpression") {
|
|
2542
|
-
const parent = current.parent?.node;
|
|
2543
|
-
if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
|
|
2544
|
-
const name = parent.id.name;
|
|
2545
|
-
if (/^[A-Z]/.test(name)) return node;
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
if (node.type === "FunctionExpression") {
|
|
2549
|
-
const parent = current.parent?.node;
|
|
2550
|
-
if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
|
|
2551
|
-
const name = parent.id.name;
|
|
2552
|
-
if (/^[A-Z]/.test(name)) return node;
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
current = current.parent;
|
|
2556
|
-
}
|
|
2557
|
-
return null;
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
|
-
// src/utils/wrap/adapters/react.ts
|
|
2561
|
-
var reactAdapter = {
|
|
2562
|
-
name: "react",
|
|
2563
|
-
extensions: [".tsx", ".jsx", ".ts", ".js"],
|
|
2564
|
-
importSource: "@vocoder/react",
|
|
2565
|
-
componentName: "T",
|
|
2566
|
-
functionName: "t",
|
|
2567
|
-
hookName: "useVocoder",
|
|
2568
|
-
translatableAttributes: [
|
|
2569
|
-
"title",
|
|
2570
|
-
"placeholder",
|
|
2571
|
-
"alt",
|
|
2572
|
-
"aria-label",
|
|
2573
|
-
"aria-description",
|
|
2574
|
-
"aria-placeholder",
|
|
2575
|
-
"aria-roledescription",
|
|
2576
|
-
"aria-valuetext",
|
|
2577
|
-
"label",
|
|
2578
|
-
"description",
|
|
2579
|
-
"message",
|
|
2580
|
-
"heading",
|
|
2581
|
-
"caption",
|
|
2582
|
-
"helperText",
|
|
2583
|
-
"errorMessage",
|
|
2584
|
-
"successMessage",
|
|
2585
|
-
"tooltip"
|
|
2586
|
-
],
|
|
2587
|
-
nonTranslatableAttributes: [
|
|
2588
|
-
"className",
|
|
2589
|
-
"class",
|
|
2590
|
-
"href",
|
|
2591
|
-
"src",
|
|
2592
|
-
"id",
|
|
2593
|
-
"key",
|
|
2594
|
-
"ref",
|
|
2595
|
-
"style",
|
|
2596
|
-
"data-testid",
|
|
2597
|
-
"data-cy",
|
|
2598
|
-
"data-test",
|
|
2599
|
-
"type",
|
|
2600
|
-
"name",
|
|
2601
|
-
"value",
|
|
2602
|
-
"action",
|
|
2603
|
-
"method",
|
|
2604
|
-
"encType",
|
|
2605
|
-
"target",
|
|
2606
|
-
"rel",
|
|
2607
|
-
"role",
|
|
2608
|
-
"tabIndex",
|
|
2609
|
-
"htmlFor",
|
|
2610
|
-
"for",
|
|
2611
|
-
"width",
|
|
2612
|
-
"height",
|
|
2613
|
-
"viewBox",
|
|
2614
|
-
"xmlns",
|
|
2615
|
-
"fill",
|
|
2616
|
-
"stroke"
|
|
2617
|
-
],
|
|
2618
|
-
isAlreadyWrapped(ancestors, imports) {
|
|
2619
|
-
for (const ancestor of ancestors) {
|
|
2620
|
-
if (ancestor.type === "JSXElement") {
|
|
2621
|
-
const opening = ancestor.openingElement;
|
|
2622
|
-
if (opening && opening.name && opening.name.type === "JSXIdentifier") {
|
|
2623
|
-
const tagName = opening.name.name;
|
|
2624
|
-
if (imports.has(tagName) && imports.get(tagName) === "T") {
|
|
2625
|
-
return true;
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
return false;
|
|
2631
|
-
},
|
|
2632
|
-
isAlreadyWrappedCall(node, tNames) {
|
|
2633
|
-
if (node.type === "CallExpression") {
|
|
2634
|
-
const callee = node.callee;
|
|
2635
|
-
if (callee.type === "Identifier" && tNames.has(callee.name)) {
|
|
2636
|
-
return true;
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
return false;
|
|
2640
|
-
},
|
|
2641
|
-
getRequiredImports(strategies) {
|
|
2642
|
-
const specifiers = [];
|
|
2643
|
-
if (strategies.has("T-component")) {
|
|
2644
|
-
specifiers.push("T");
|
|
2645
|
-
}
|
|
2646
|
-
if (strategies.has("t-function")) {
|
|
2647
|
-
specifiers.push("useVocoder");
|
|
2648
|
-
}
|
|
2649
|
-
return { specifiers, source: "@vocoder/react" };
|
|
2650
|
-
}
|
|
2651
|
-
};
|
|
2652
|
-
|
|
2653
|
-
// src/commands/wrap.ts
|
|
2654
|
-
var CONFIDENCE_ORDER = ["high", "medium", "low"];
|
|
2655
|
-
function meetsConfidenceThreshold(candidate, threshold) {
|
|
2656
|
-
return CONFIDENCE_ORDER.indexOf(candidate) <= CONFIDENCE_ORDER.indexOf(threshold);
|
|
2657
|
-
}
|
|
2658
|
-
async function wrap(options = {}) {
|
|
2659
|
-
const startTime = Date.now();
|
|
2660
|
-
const projectRoot = process.cwd();
|
|
2661
|
-
const confidenceThreshold = options.confidence || "high";
|
|
2662
|
-
p3.intro("Vocoder Wrap");
|
|
2663
|
-
const spinner4 = p3.spinner();
|
|
2664
|
-
try {
|
|
2665
|
-
spinner4.start("Scanning files for unwrapped strings");
|
|
2666
|
-
const analyzer = new StringAnalyzer(reactAdapter);
|
|
2667
|
-
const allCandidates = await analyzer.analyzeProject(options, projectRoot);
|
|
2668
|
-
if (allCandidates.length === 0) {
|
|
2669
|
-
spinner4.stop("No unwrapped strings found");
|
|
2670
|
-
p3.log.info("All user-facing strings appear to be wrapped already.");
|
|
2671
|
-
p3.outro("");
|
|
2672
|
-
return 0;
|
|
2673
|
-
}
|
|
2674
|
-
spinner4.stop(
|
|
2675
|
-
`Found ${chalk3.cyan(allCandidates.length)} candidate strings`
|
|
2676
|
-
);
|
|
2677
|
-
const filtered = allCandidates.filter(
|
|
2678
|
-
(c) => meetsConfidenceThreshold(c.confidence, confidenceThreshold)
|
|
2679
|
-
);
|
|
2680
|
-
if (filtered.length === 0) {
|
|
2681
|
-
p3.log.warn(
|
|
2682
|
-
`No strings meet the ${chalk3.bold(confidenceThreshold)} confidence threshold.`
|
|
2683
|
-
);
|
|
2684
|
-
p3.log.info("Try --confidence medium or --confidence low to see more candidates.");
|
|
2685
|
-
p3.outro("");
|
|
2686
|
-
return 0;
|
|
2687
|
-
}
|
|
2688
|
-
p3.log.info(
|
|
2689
|
-
`${filtered.length} strings meet ${chalk3.bold(confidenceThreshold)} confidence threshold`
|
|
2690
|
-
);
|
|
2691
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
2692
|
-
for (const c of filtered) {
|
|
2693
|
-
const existing = byFile.get(c.file) || [];
|
|
2694
|
-
existing.push(c);
|
|
2695
|
-
byFile.set(c.file, existing);
|
|
2696
|
-
}
|
|
2697
|
-
if (options.dryRun) {
|
|
2698
|
-
const lines = [];
|
|
2699
|
-
for (const [file, candidates] of byFile) {
|
|
2700
|
-
const relPath = relative2(projectRoot, file);
|
|
2701
|
-
lines.push(chalk3.bold(relPath));
|
|
2702
|
-
for (const c of candidates) {
|
|
2703
|
-
const confidenceColor = c.confidence === "high" ? chalk3.green : c.confidence === "medium" ? chalk3.yellow : chalk3.red;
|
|
2704
|
-
const strategyLabel = c.strategy === "T-component" ? "<T>" : "t()";
|
|
2705
|
-
lines.push(
|
|
2706
|
-
` ${chalk3.dim(`L${c.line}`)} ${confidenceColor(`[${c.confidence}]`)} ${chalk3.cyan(strategyLabel)} "${truncate(c.text, 50)}"`
|
|
2707
|
-
);
|
|
2708
|
-
if (options.verbose) {
|
|
2709
|
-
lines.push(chalk3.dim(` ${c.reason}`));
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
lines.push("");
|
|
2713
|
-
}
|
|
2714
|
-
lines.push(summarizeCandidates(filtered));
|
|
2715
|
-
p3.note(lines.join("\n"), "Dry run \u2014 would wrap");
|
|
2716
|
-
p3.outro("Run without --dry-run to apply changes.");
|
|
2717
|
-
return 0;
|
|
2718
|
-
}
|
|
2719
|
-
let accepted;
|
|
2720
|
-
if (options.interactive) {
|
|
2721
|
-
accepted = await interactiveConfirm(byFile, projectRoot);
|
|
2722
|
-
if (accepted.length === 0) {
|
|
2723
|
-
p3.log.warn("No strings selected for wrapping.");
|
|
2724
|
-
p3.outro("");
|
|
2725
|
-
return 0;
|
|
2726
|
-
}
|
|
2727
|
-
} else {
|
|
2728
|
-
accepted = filtered;
|
|
2729
|
-
}
|
|
2730
|
-
spinner4.start("Wrapping strings");
|
|
2731
|
-
const transformer = new StringTransformer(reactAdapter);
|
|
2732
|
-
let totalWrapped = 0;
|
|
2733
|
-
let filesModified = 0;
|
|
2734
|
-
const acceptedByFile = /* @__PURE__ */ new Map();
|
|
2735
|
-
for (const c of accepted) {
|
|
2736
|
-
const existing = acceptedByFile.get(c.file) || [];
|
|
2737
|
-
existing.push(c);
|
|
2738
|
-
acceptedByFile.set(c.file, existing);
|
|
2739
|
-
}
|
|
2740
|
-
for (const [file, candidates] of acceptedByFile) {
|
|
2741
|
-
const code = readFileSync5(file, "utf-8");
|
|
2742
|
-
const result = transformer.transform(code, candidates, file);
|
|
2743
|
-
if (result.wrappedCount > 0) {
|
|
2744
|
-
writeFileSync3(file, result.output, "utf-8");
|
|
2745
|
-
totalWrapped += result.wrappedCount;
|
|
2746
|
-
filesModified++;
|
|
2747
|
-
}
|
|
2748
|
-
if (options.verbose && result.skipped.length > 0) {
|
|
2749
|
-
const relPath = relative2(projectRoot, file);
|
|
2750
|
-
p3.log.info(`Skipped ${result.skipped.length} strings in ${relPath}`);
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
spinner4.stop(
|
|
2754
|
-
`Wrapped ${chalk3.cyan(totalWrapped)} strings across ${chalk3.cyan(filesModified)} files`
|
|
2755
|
-
);
|
|
2756
|
-
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2757
|
-
p3.outro(`Done! (${duration}s)`);
|
|
2758
|
-
p3.log.info("Next steps:");
|
|
2759
|
-
p3.log.info(" 1. Review the changes (git diff)");
|
|
2760
|
-
p3.log.info(" 2. Run your tests to verify nothing broke");
|
|
2761
|
-
p3.log.info(' 3. Run "vocoder sync" to extract and translate');
|
|
2762
|
-
return 0;
|
|
2763
|
-
} catch (error) {
|
|
2764
|
-
spinner4.stop();
|
|
2765
|
-
if (error instanceof Error) {
|
|
2766
|
-
p3.log.error(error.message);
|
|
2767
|
-
if (options.verbose) {
|
|
2768
|
-
p3.log.info(`Full error: ${error.stack ?? error}`);
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
return 1;
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
async function interactiveConfirm(byFile, projectRoot) {
|
|
2775
|
-
const accepted = [];
|
|
2776
|
-
p3.log.info("Interactive mode \u2014 confirm each string:");
|
|
2777
|
-
for (const [file, candidates] of byFile) {
|
|
2778
|
-
const relPath = relative2(projectRoot, file);
|
|
2779
|
-
p3.log.step(chalk3.bold(relPath));
|
|
2780
|
-
let skipFile = false;
|
|
2781
|
-
for (const c of candidates) {
|
|
2782
|
-
if (skipFile) break;
|
|
2783
|
-
const strategyLabel = c.strategy === "T-component" ? "<T>" : "t()";
|
|
2784
|
-
const label = `L${c.line} ${strategyLabel} "${truncate(c.text, 50)}"`;
|
|
2785
|
-
const action = await p3.select({
|
|
2786
|
-
message: label,
|
|
2787
|
-
options: [
|
|
2788
|
-
{ value: "yes", label: "Yes, wrap this string" },
|
|
2789
|
-
{ value: "no", label: "No, skip" },
|
|
2790
|
-
{ value: "all", label: "Accept all remaining" },
|
|
2791
|
-
{ value: "skip", label: "Skip this file" },
|
|
2792
|
-
{ value: "quit", label: "Quit" }
|
|
2793
|
-
]
|
|
2794
|
-
});
|
|
2795
|
-
if (p3.isCancel(action) || action === "quit") {
|
|
2796
|
-
return accepted;
|
|
2797
|
-
}
|
|
2798
|
-
if (action === "yes") {
|
|
2799
|
-
accepted.push(c);
|
|
2800
|
-
} else if (action === "all") {
|
|
2801
|
-
accepted.push(c);
|
|
2802
|
-
const remaining = candidates.slice(candidates.indexOf(c) + 1);
|
|
2803
|
-
accepted.push(...remaining);
|
|
2804
|
-
for (const [, moreCandidates] of byFile) {
|
|
2805
|
-
if (moreCandidates !== candidates) {
|
|
2806
|
-
accepted.push(...moreCandidates);
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
return accepted;
|
|
2810
|
-
} else if (action === "skip") {
|
|
2811
|
-
skipFile = true;
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
return accepted;
|
|
2816
|
-
}
|
|
2817
|
-
function truncate(text, maxLen) {
|
|
2818
|
-
if (text.length <= maxLen) return text;
|
|
2819
|
-
return text.slice(0, maxLen - 3) + "...";
|
|
2820
|
-
}
|
|
2821
|
-
function summarizeCandidates(candidates) {
|
|
2822
|
-
let high = 0;
|
|
2823
|
-
let medium = 0;
|
|
2824
|
-
let low = 0;
|
|
2825
|
-
let tComponent = 0;
|
|
2826
|
-
let tFunction = 0;
|
|
2827
|
-
for (const c of candidates) {
|
|
2828
|
-
if (c.confidence === "high") high++;
|
|
2829
|
-
else if (c.confidence === "medium") medium++;
|
|
2830
|
-
else low++;
|
|
2831
|
-
if (c.strategy === "T-component") tComponent++;
|
|
2832
|
-
else tFunction++;
|
|
2833
|
-
}
|
|
2834
|
-
const parts = [];
|
|
2835
|
-
if (high > 0) parts.push(chalk3.green(`${high} high`));
|
|
2836
|
-
if (medium > 0) parts.push(chalk3.yellow(`${medium} medium`));
|
|
2837
|
-
if (low > 0) parts.push(chalk3.red(`${low} low`));
|
|
2838
|
-
return `${candidates.length} total (${parts.join(", ")}) | ${tComponent} <T>, ${tFunction} t()`;
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
1915
|
// src/bin.ts
|
|
2842
1916
|
function collect(value, previous = []) {
|
|
2843
1917
|
return previous.concat([value]);
|
|
@@ -2847,12 +1921,13 @@ async function runCommand(command, options) {
|
|
|
2847
1921
|
process.exitCode = exitCode;
|
|
2848
1922
|
}
|
|
2849
1923
|
var program = new Command();
|
|
2850
|
-
program.name("vocoder").description("Vocoder CLI -
|
|
2851
|
-
program.command("sync").description("Extract strings and sync translations").option("--include <pattern>", "Glob pattern(s) to include (can be used multiple times)", collect, []).option("--exclude <pattern>", "Glob pattern(s) to exclude (can be used multiple times)", collect, []).option("--branch <name>", "Override branch detection").option("--force", "Sync even if not a target branch").option("--mode <mode>", "Sync mode: auto|required|best-effort").option("--max-wait-ms <ms>", "Max wait time before fallback (ms)", (value) => Number.parseInt(value, 10)).option("--no-fallback", "Fail instead of using fallback artifacts").option("--dry-run", "Show what would be synced without doing it").option("--verbose", "Show detailed progress").action((options) => runCommand(sync, {
|
|
2852
|
-
...options,
|
|
2853
|
-
noFallback: options.noFallback ? true : void 0
|
|
2854
|
-
}));
|
|
2855
|
-
program.command("wrap").description("Auto-wrap strings with <T> and t() for translation").option("--include <pattern>", "Glob pattern(s) to include (can be used multiple times)", collect, []).option("--exclude <pattern>", "Glob pattern(s) to exclude (can be used multiple times)", collect, []).option("--dry-run", "Preview changes without modifying files").option("--interactive", "Confirm each string interactively").option("--confidence <level>", "Minimum confidence: high, medium, low", "high").option("--verbose", "Detailed output").action((options) => runCommand(wrap, options));
|
|
1924
|
+
program.name("vocoder").description("Vocoder CLI - Project setup and string extraction").version("0.1.5");
|
|
2856
1925
|
program.command("init").description("Authenticate and provision Vocoder for this project").option("--api-url <url>", "Override Vocoder API URL").option("--yes", "Allow overwriting existing local config values").option("--project-name <name>", "Starter project name to create").option("--source-locale <locale>", "Source locale for the starter project").option("--target-locales <list>", "Comma-separated target locales (e.g. es,fr,de)").action((options) => runCommand(init, options));
|
|
1926
|
+
program.command("sync").description("Extract strings and sync translations").option("--branch <branch>", "Override detected branch").option("--mode <mode>", "Sync mode: auto, required, best-effort", "auto").option("--max-wait <ms>", "Max wait for translations (ms)").option("--force", "Force re-extraction even if no changes").option("--dry-run", "Preview without syncing").option("--no-fallback", "Disable fallback to cached translations").option("--include <pattern>", "Include glob pattern", collect, []).option("--exclude <pattern>", "Exclude glob pattern", collect, []).option("--verbose", "Detailed output").action((options) => {
|
|
1927
|
+
const translated = { ...options };
|
|
1928
|
+
if (options.maxWait) translated.maxWaitMs = Number(options.maxWait);
|
|
1929
|
+
if (options.fallback === false) translated.noFallback = true;
|
|
1930
|
+
return runCommand(sync, translated);
|
|
1931
|
+
});
|
|
2857
1932
|
program.parse(process.argv);
|
|
2858
1933
|
//# sourceMappingURL=bin.mjs.map
|