@vocoder/cli 0.1.4 → 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/dist/bin.mjs CHANGED
@@ -278,6 +278,272 @@ var VocoderAPI = class {
278
278
  }
279
279
  };
280
280
 
281
+ // src/utils/detect-local.ts
282
+ import { existsSync, readFileSync } from "fs";
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
+ }
546
+
281
547
  // src/utils/git-identity.ts
282
548
  import { execSync } from "child_process";
283
549
  import { relative, resolve } from "path";
@@ -375,7 +641,10 @@ function resolveGitContext() {
375
641
  }
376
642
 
377
643
  // src/commands/init.ts
644
+ import { config as loadEnv } from "dotenv";
645
+ import { execSync as execSync2 } from "child_process";
378
646
  import { spawn } from "child_process";
647
+ loadEnv();
379
648
  var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
380
649
  function parseTargetLocales(value) {
381
650
  if (!value) return void 0;
@@ -442,19 +711,77 @@ function printPlanLimitMessage(apiUrl, message) {
442
711
  ${message}`);
443
712
  p.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
444
713
  }
445
- function printNextSteps(projectName, organizationName) {
714
+ function runScaffold(params) {
715
+ const { projectName, organizationName, sourceLocale, translationTriggers } = params;
446
716
  p.log.info(`Project: ${chalk.bold(projectName)}`);
447
717
  p.log.info(`Workspace: ${chalk.bold(organizationName)}`);
448
- p.log.info("");
449
- p.log.info("Next steps:");
450
- p.log.info(` 1. Add ${chalk.cyan("@vocoder/unplugin")} to your build config`);
451
- p.log.info(` 2. Wrap translatable strings with ${chalk.green("<T>")}...${chalk.green("</T>")}`);
452
- p.log.info(" 3. Push to trigger extraction + translation");
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
+ `);
453
780
  }
454
781
  async function init(options = {}) {
455
782
  const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
456
783
  p.intro("Vocoder Setup");
457
- const spinner4 = p.spinner();
784
+ const spinner3 = p.spinner();
458
785
  try {
459
786
  const gitContext = resolveGitContext();
460
787
  const identity = gitContext.identity;
@@ -464,21 +791,26 @@ async function init(options = {}) {
464
791
  }
465
792
  }
466
793
  if (identity) {
467
- spinner4.start("Checking for existing project...");
794
+ spinner3.start("Checking for existing project...");
468
795
  const api2 = new VocoderAPI({ apiUrl, apiKey: "" });
469
796
  const existing = await api2.lookupProjectByRepo({
470
797
  repoCanonical: identity.repoCanonical,
471
798
  scopePath: identity.repoScopePath
472
799
  });
473
800
  if (existing) {
474
- spinner4.stop("Found existing project!");
801
+ spinner3.stop("Found existing project!");
475
802
  p.outro("Vocoder is already set up for this repository.");
476
- printNextSteps(existing.projectName, existing.organizationName);
803
+ runScaffold({
804
+ projectName: existing.projectName,
805
+ organizationName: existing.organizationName,
806
+ sourceLocale: existing.sourceLocale ?? "en",
807
+ translationTriggers: existing.translationTriggers ?? ["push"]
808
+ });
477
809
  return 0;
478
810
  }
479
- spinner4.stop("No existing project found for this repo.");
811
+ spinner3.stop("No existing project found for this repo.");
480
812
  }
481
- spinner4.start("Creating setup session");
813
+ spinner3.start("Creating setup session");
482
814
  const api = new VocoderAPI({ apiUrl, apiKey: "" });
483
815
  const start = await api.startInitSession({
484
816
  projectName: options.projectName,
@@ -487,7 +819,7 @@ async function init(options = {}) {
487
819
  ...identity?.repoCanonical ? { repoCanonical: identity.repoCanonical } : {},
488
820
  ...identity ? { repoScopePath: identity.repoScopePath } : {}
489
821
  });
490
- spinner4.stop("Setup session created");
822
+ spinner3.stop("Setup session created");
491
823
  const verificationUrlString = start.verificationUrl;
492
824
  p.log.info("Create a project in your browser to continue.");
493
825
  p.note(verificationUrlString, "Setup URL");
@@ -507,7 +839,7 @@ async function init(options = {}) {
507
839
  }
508
840
  }
509
841
  const expiresAt = new Date(start.expiresAt).getTime();
510
- spinner4.start("Waiting for setup to complete...");
842
+ spinner3.start("Waiting for setup to complete...");
511
843
  while (Date.now() < expiresAt) {
512
844
  const status = await api.getInitSessionStatus({
513
845
  sessionId: start.sessionId,
@@ -516,13 +848,13 @@ async function init(options = {}) {
516
848
  if (status.status === "pending") {
517
849
  const pendingMessage = status.message?.trim();
518
850
  if (pendingMessage) {
519
- spinner4.message(`Waiting for setup to complete... (${pendingMessage})`);
851
+ spinner3.message(`Waiting for setup to complete... (${pendingMessage})`);
520
852
  }
521
853
  await sleep((status.pollIntervalSeconds || start.poll.intervalSeconds) * 1e3);
522
854
  continue;
523
855
  }
524
856
  if (status.status === "failed") {
525
- spinner4.stop("Setup failed");
857
+ spinner3.stop("Setup failed");
526
858
  if (isPlanLimitFailure(status.message)) {
527
859
  printPlanLimitMessage(apiUrl, status.message);
528
860
  } else {
@@ -532,19 +864,24 @@ async function init(options = {}) {
532
864
  return 1;
533
865
  }
534
866
  if (status.status === "completed") {
535
- spinner4.stop("Setup complete!");
867
+ spinner3.stop("Setup complete!");
536
868
  const { credentials } = status;
537
869
  p.outro("Vocoder initialized successfully!");
538
- printNextSteps(credentials.projectName, credentials.organizationName);
870
+ runScaffold({
871
+ projectName: credentials.projectName,
872
+ organizationName: credentials.organizationName,
873
+ sourceLocale: credentials.sourceLocale,
874
+ translationTriggers: credentials.translationTriggers ?? ["push"]
875
+ });
539
876
  return 0;
540
877
  }
541
878
  }
542
- spinner4.stop("Setup timed out");
879
+ spinner3.stop("Setup timed out");
543
880
  p.log.error("Setup timed out. Run `vocoder init` again.");
544
881
  p.cancel("Setup could not be completed.");
545
882
  return 1;
546
883
  } catch (error) {
547
- spinner4.stop();
884
+ spinner3.stop();
548
885
  if (error instanceof Error) {
549
886
  if (isPlanLimitFailure(error.message)) {
550
887
  printPlanLimitMessage(apiUrl, error.message);
@@ -563,7 +900,7 @@ import * as p2 from "@clack/prompts";
563
900
  import { createHash as createHash2, randomUUID } from "crypto";
564
901
 
565
902
  // src/utils/branch.ts
566
- import { execSync as execSync2 } from "child_process";
903
+ import { execSync as execSync3 } from "child_process";
567
904
  var REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
568
905
  function escapeRegexChar(value) {
569
906
  return value.replace(REGEX_SPECIAL_CHARS, "\\$&");
@@ -585,7 +922,7 @@ function detectBranch(override) {
585
922
  return envBranch;
586
923
  }
587
924
  try {
588
- const branch = execSync2("git rev-parse --abbrev-ref HEAD", {
925
+ const branch = execSync3("git rev-parse --abbrev-ref HEAD", {
589
926
  encoding: "utf-8",
590
927
  stdio: ["pipe", "pipe", "ignore"]
591
928
  }).trim();
@@ -629,12 +966,12 @@ function matchBranchPattern(branch, pattern) {
629
966
  }
630
967
 
631
968
  // src/commands/sync.ts
632
- import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
969
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
633
970
 
634
971
  // src/utils/config.ts
635
972
  import chalk2 from "chalk";
636
- import { config as loadEnv } from "dotenv";
637
- loadEnv();
973
+ import { config as loadEnv2 } from "dotenv";
974
+ loadEnv2();
638
975
  function validateLocalConfig(config) {
639
976
  if (!config.apiKey || config.apiKey.length === 0) {
640
977
  throw new Error("VOCODER_API_KEY is required. Set it in your .env file.");
@@ -753,7 +1090,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
753
1090
 
754
1091
  // src/utils/extract.ts
755
1092
  import { createHash } from "crypto";
756
- import { readFileSync } from "fs";
1093
+ import { readFileSync as readFileSync2 } from "fs";
757
1094
  import { parse } from "@babel/parser";
758
1095
  import babelTraverse from "@babel/traverse";
759
1096
  import { glob } from "glob";
@@ -797,7 +1134,7 @@ var StringExtractor = class {
797
1134
  * Extract strings from a single file
798
1135
  */
799
1136
  async extractFromFile(filePath, projectRoot) {
800
- const code = readFileSync(filePath, "utf-8");
1137
+ const code = readFileSync2(filePath, "utf-8");
801
1138
  const strings = [];
802
1139
  const relativeFilePath = pathRelative(projectRoot, filePath).split("\\").join("/");
803
1140
  try {
@@ -1025,7 +1362,7 @@ var StringExtractor = class {
1025
1362
 
1026
1363
  // src/commands/sync.ts
1027
1364
  import chalk3 from "chalk";
1028
- import { join } from "path";
1365
+ import { join as join2 } from "path";
1029
1366
  function isRecord(value) {
1030
1367
  return typeof value === "object" && value !== null && !Array.isArray(value);
1031
1368
  }
@@ -1073,17 +1410,17 @@ function getCacheFilePath(projectRoot, branch) {
1073
1410
  const slug = branch.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
1074
1411
  const branchHash = createHash2("sha1").update(branch).digest("hex").slice(0, 12);
1075
1412
  const filename = `${slug || "branch"}-${branchHash}.json`;
1076
- return join(projectRoot, ".vocoder", "cache", "sync", filename);
1413
+ return join2(projectRoot, "node_modules", ".vocoder", "cache", "sync", filename);
1077
1414
  }
1078
1415
  function readLocalSnapshotCache(params) {
1079
1416
  const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
1080
1417
  for (const candidateBranch of candidateBranches) {
1081
1418
  const cacheFilePath = getCacheFilePath(params.projectRoot, candidateBranch);
1082
- if (!existsSync(cacheFilePath)) {
1419
+ if (!existsSync2(cacheFilePath)) {
1083
1420
  continue;
1084
1421
  }
1085
1422
  try {
1086
- const raw = readFileSync2(cacheFilePath, "utf-8");
1423
+ const raw = readFileSync3(cacheFilePath, "utf-8");
1087
1424
  const parsed = JSON.parse(raw);
1088
1425
  if (!isRecord(parsed)) {
1089
1426
  continue;
@@ -1109,7 +1446,7 @@ function readLocalSnapshotCache(params) {
1109
1446
  }
1110
1447
  function writeLocalSnapshotCache(params) {
1111
1448
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
1112
- mkdirSync(join(params.projectRoot, ".vocoder", "cache", "sync"), {
1449
+ mkdirSync(join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"), {
1113
1450
  recursive: true
1114
1451
  });
1115
1452
  const payload = {
@@ -1279,12 +1616,12 @@ async function sync(options = {}) {
1279
1616
  const startTime = Date.now();
1280
1617
  const projectRoot = process.cwd();
1281
1618
  p2.intro("Vocoder Sync");
1282
- const spinner4 = p2.spinner();
1619
+ const spinner3 = p2.spinner();
1283
1620
  try {
1284
- spinner4.start("Detecting branch");
1621
+ spinner3.start("Detecting branch");
1285
1622
  const branch = detectBranch(options.branch);
1286
- spinner4.stop(`Branch: ${chalk3.cyan(branch)}`);
1287
- spinner4.start("Loading project configuration");
1623
+ spinner3.stop(`Branch: ${chalk3.cyan(branch)}`);
1624
+ spinner3.start("Loading project configuration");
1288
1625
  const mergedConfig = await getMergedConfig(options, options.verbose);
1289
1626
  const localConfig = {
1290
1627
  apiKey: mergedConfig.apiKey || "",
@@ -1306,7 +1643,7 @@ async function sync(options = {}) {
1306
1643
  excludePattern: mergedConfig.excludePattern,
1307
1644
  timeout: waitTimeoutMs
1308
1645
  };
1309
- spinner4.stop("Project configuration loaded");
1646
+ spinner3.stop("Project configuration loaded");
1310
1647
  if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
1311
1648
  p2.log.warn(
1312
1649
  `Skipping translations (${chalk3.cyan(branch)} is not a target branch)`
@@ -1317,7 +1654,7 @@ async function sync(options = {}) {
1317
1654
  return 0;
1318
1655
  }
1319
1656
  const patternsDisplay = Array.isArray(config.extractionPattern) ? config.extractionPattern.join(", ") : config.extractionPattern;
1320
- spinner4.start(`Extracting strings from ${patternsDisplay}`);
1657
+ spinner3.start(`Extracting strings from ${patternsDisplay}`);
1321
1658
  const extractor = new StringExtractor();
1322
1659
  const extractedStrings = await extractor.extractFromProject(
1323
1660
  config.extractionPattern,
@@ -1325,12 +1662,12 @@ async function sync(options = {}) {
1325
1662
  config.excludePattern
1326
1663
  );
1327
1664
  if (extractedStrings.length === 0) {
1328
- spinner4.stop("No translatable strings found");
1329
- p2.log.warn("Make sure you are using <T> components from @vocoder/react");
1665
+ spinner3.stop("No translatable strings found");
1666
+ p2.log.warn("Make sure you are wrapping translatable strings with Vocoder");
1330
1667
  p2.outro("");
1331
1668
  return 0;
1332
1669
  }
1333
- spinner4.stop(
1670
+ spinner3.stop(
1334
1671
  `Extracted ${chalk3.cyan(extractedStrings.length)} strings from ${chalk3.cyan(patternsDisplay)}`
1335
1672
  );
1336
1673
  if (options.verbose) {
@@ -1368,7 +1705,7 @@ async function sync(options = {}) {
1368
1705
  `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
1369
1706
  );
1370
1707
  }
1371
- spinner4.start("Submitting strings to Vocoder API");
1708
+ spinner3.start("Submitting strings to Vocoder API");
1372
1709
  const batchResponse = await api.submitTranslation(
1373
1710
  branch,
1374
1711
  stringEntries,
@@ -1380,7 +1717,7 @@ async function sync(options = {}) {
1380
1717
  },
1381
1718
  repoIdentity ?? void 0
1382
1719
  );
1383
- spinner4.stop(`Submitted to API - Batch ${chalk3.cyan(batchResponse.batchId)}`);
1720
+ spinner3.stop(`Submitted to API - Batch ${chalk3.cyan(batchResponse.batchId)}`);
1384
1721
  const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
1385
1722
  branch,
1386
1723
  requestedMode,
@@ -1423,7 +1760,7 @@ async function sync(options = {}) {
1423
1760
  }
1424
1761
  let waitError = null;
1425
1762
  if (!artifacts && (effectiveMode === "required" || effectiveMode === "best-effort")) {
1426
- spinner4.start(`Waiting for translations (max ${waitTimeoutMs}ms)`);
1763
+ spinner3.start(`Waiting for translations (max ${waitTimeoutMs}ms)`);
1427
1764
  let lastProgress = 0;
1428
1765
  try {
1429
1766
  const completion = await api.waitForCompletion(
@@ -1432,7 +1769,7 @@ async function sync(options = {}) {
1432
1769
  (progress) => {
1433
1770
  const percent = Math.round(progress * 100);
1434
1771
  if (percent > lastProgress) {
1435
- spinner4.message(`Translating... ${percent}%`);
1772
+ spinner3.message(`Translating... ${percent}%`);
1436
1773
  lastProgress = percent;
1437
1774
  }
1438
1775
  }
@@ -1442,9 +1779,9 @@ async function sync(options = {}) {
1442
1779
  translations: completion.translations,
1443
1780
  localeMetadata: completion.localeMetadata
1444
1781
  };
1445
- spinner4.stop("Translations complete");
1782
+ spinner3.stop("Translations complete");
1446
1783
  } catch (error) {
1447
- spinner4.stop("Translation wait incomplete");
1784
+ spinner3.stop("Translation wait incomplete");
1448
1785
  waitError = error instanceof Error ? error : new Error(String(error));
1449
1786
  if (effectiveMode === "required") {
1450
1787
  throw waitError;
@@ -1458,7 +1795,7 @@ async function sync(options = {}) {
1458
1795
  "Fresh translations are not available and fallback is disabled (--no-fallback)."
1459
1796
  );
1460
1797
  }
1461
- spinner4.start("Loading fallback translations");
1798
+ spinner3.start("Loading fallback translations");
1462
1799
  const localFallback = readLocalSnapshotCache({
1463
1800
  projectRoot,
1464
1801
  branch
@@ -1466,7 +1803,7 @@ async function sync(options = {}) {
1466
1803
  if (localFallback) {
1467
1804
  artifacts = localFallback;
1468
1805
  const cacheBranchLabel = localFallback.cacheBranch && localFallback.cacheBranch !== branch ? `${localFallback.cacheBranch} fallback` : localFallback.cacheBranch || branch;
1469
- spinner4.stop(`Using local cached snapshot (${cacheBranchLabel})`);
1806
+ spinner3.stop(`Using local cached snapshot (${cacheBranchLabel})`);
1470
1807
  } else {
1471
1808
  try {
1472
1809
  const apiSnapshot = await fetchApiSnapshot(api, {
@@ -1475,12 +1812,12 @@ async function sync(options = {}) {
1475
1812
  });
1476
1813
  if (apiSnapshot) {
1477
1814
  artifacts = apiSnapshot;
1478
- spinner4.stop("Using latest completed API snapshot");
1815
+ spinner3.stop("Using latest completed API snapshot");
1479
1816
  } else {
1480
- spinner4.stop("No completed API snapshot available");
1817
+ spinner3.stop("No completed API snapshot available");
1481
1818
  }
1482
1819
  } catch (error) {
1483
- spinner4.stop("Failed to fetch API snapshot");
1820
+ spinner3.stop("Failed to fetch API snapshot");
1484
1821
  if (options.verbose) {
1485
1822
  const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
1486
1823
  p2.log.warn(`Snapshot fetch error: ${message}`);
@@ -1536,7 +1873,7 @@ async function sync(options = {}) {
1536
1873
  p2.log.info("Just use <VocoderProvider> and <T> \u2014 no manual imports needed.");
1537
1874
  return 0;
1538
1875
  } catch (error) {
1539
- spinner4.stop();
1876
+ spinner3.stop();
1540
1877
  if (error instanceof VocoderAPIError && error.syncPolicyError) {
1541
1878
  p2.log.error(error.syncPolicyError.message);
1542
1879
  const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
@@ -1575,1159 +1912,6 @@ async function sync(options = {}) {
1575
1912
  }
1576
1913
  }
1577
1914
 
1578
- // src/commands/wrap.ts
1579
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1580
- import { relative as relative2 } from "path";
1581
- import * as p3 from "@clack/prompts";
1582
- import chalk4 from "chalk";
1583
-
1584
- // src/utils/wrap/analyzer.ts
1585
- import { readFileSync as readFileSync3 } from "fs";
1586
- import { parse as parse2 } from "@babel/parser";
1587
- import babelTraverse2 from "@babel/traverse";
1588
- import { glob as glob2 } from "glob";
1589
-
1590
- // src/utils/wrap/heuristics.ts
1591
- var URL_REGEX = /^(https?:\/\/|\/\/|mailto:|tel:|ftp:\/\/)/i;
1592
- var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1593
- var FILE_PATH_REGEX = /^(\.{0,2}\/|[a-zA-Z]:\\)/;
1594
- var COLOR_HEX_REGEX = /^#([0-9a-fA-F]{3,8})$/;
1595
- var COLOR_FUNC_REGEX = /^(rgb|rgba|hsl|hsla)\s*\(/i;
1596
- var CAMEL_CASE_REGEX = /^[a-z][a-zA-Z0-9]*$/;
1597
- var PASCAL_CASE_REGEX = /^[A-Z][a-zA-Z0-9]*$/;
1598
- var SCREAMING_SNAKE_REGEX = /^[A-Z][A-Z0-9_]+$/;
1599
- var KEBAB_CASE_REGEX = /^[a-z][a-z0-9-]+$/;
1600
- var MIME_TYPE_REGEX = /^(application|text|image|audio|video|font|multipart)\//;
1601
- var DATE_FORMAT_REGEX = /^[YMDHhmsaAZz\-\/\.\s:,]+$/;
1602
- var CSS_UNIT_REGEX = /^\d+(\.\d+)?(px|em|rem|vh|vw|%|ch|ex|pt|pc|in|cm|mm)$/;
1603
- var TAILWIND_REGEX = /^[a-z][\w-]*(\s+[a-z][\w-]*)*$/;
1604
- var TAILWIND_PREFIXES = [
1605
- "flex",
1606
- "grid",
1607
- "block",
1608
- "inline",
1609
- "hidden",
1610
- "absolute",
1611
- "relative",
1612
- "fixed",
1613
- "sticky",
1614
- "top",
1615
- "bottom",
1616
- "left",
1617
- "right",
1618
- "inset",
1619
- "w-",
1620
- "h-",
1621
- "min-",
1622
- "max-",
1623
- "p-",
1624
- "px-",
1625
- "py-",
1626
- "pt-",
1627
- "pb-",
1628
- "pl-",
1629
- "pr-",
1630
- "m-",
1631
- "mx-",
1632
- "my-",
1633
- "mt-",
1634
- "mb-",
1635
- "ml-",
1636
- "mr-",
1637
- "text-",
1638
- "font-",
1639
- "leading-",
1640
- "tracking-",
1641
- "bg-",
1642
- "border-",
1643
- "rounded-",
1644
- "shadow-",
1645
- "opacity-",
1646
- "z-",
1647
- "gap-",
1648
- "space-",
1649
- "items-",
1650
- "justify-",
1651
- "self-",
1652
- "place-",
1653
- "overflow-",
1654
- "cursor-",
1655
- "transition-",
1656
- "duration-",
1657
- "ease-",
1658
- "sm:",
1659
- "md:",
1660
- "lg:",
1661
- "xl:",
1662
- "2xl:",
1663
- "dark:",
1664
- "hover:",
1665
- "focus:",
1666
- "active:",
1667
- "group-",
1668
- "peer-"
1669
- ];
1670
- var NON_TRANSLATABLE_ATTRIBUTES = /* @__PURE__ */ new Set([
1671
- "className",
1672
- "class",
1673
- "href",
1674
- "src",
1675
- "id",
1676
- "key",
1677
- "ref",
1678
- "style",
1679
- "data-testid",
1680
- "data-cy",
1681
- "data-test",
1682
- "type",
1683
- "name",
1684
- "value",
1685
- "action",
1686
- "method",
1687
- "encType",
1688
- "target",
1689
- "rel",
1690
- "role",
1691
- "tabIndex",
1692
- "htmlFor",
1693
- "for",
1694
- "width",
1695
- "height",
1696
- "viewBox",
1697
- "xmlns",
1698
- "fill",
1699
- "stroke",
1700
- "onClick",
1701
- "onChange",
1702
- "onSubmit",
1703
- "onBlur",
1704
- "onFocus",
1705
- "onKeyDown",
1706
- "onKeyUp",
1707
- "onKeyPress",
1708
- "onMouseEnter",
1709
- "onMouseLeave"
1710
- ]);
1711
- var TRANSLATABLE_ATTRIBUTES = /* @__PURE__ */ new Set([
1712
- "title",
1713
- "placeholder",
1714
- "alt",
1715
- "aria-label",
1716
- "aria-description",
1717
- "aria-placeholder",
1718
- "aria-roledescription",
1719
- "aria-valuetext",
1720
- "label",
1721
- "description",
1722
- "message",
1723
- "heading",
1724
- "caption",
1725
- "helperText",
1726
- "errorMessage",
1727
- "successMessage",
1728
- "tooltip"
1729
- ]);
1730
- var NON_TRANSLATABLE_CALLS = /* @__PURE__ */ new Set([
1731
- "console.log",
1732
- "console.warn",
1733
- "console.error",
1734
- "console.info",
1735
- "console.debug",
1736
- "require",
1737
- "import",
1738
- "addEventListener",
1739
- "removeEventListener",
1740
- "querySelector",
1741
- "querySelectorAll",
1742
- "getElementById",
1743
- "getAttribute",
1744
- "setAttribute",
1745
- "createElement",
1746
- "JSON.parse",
1747
- "JSON.stringify",
1748
- "parseInt",
1749
- "parseFloat",
1750
- "encodeURIComponent",
1751
- "decodeURIComponent",
1752
- "encodeURI",
1753
- "decodeURI",
1754
- "RegExp"
1755
- ]);
1756
- var TRANSLATABLE_VAR_NAMES = /* @__PURE__ */ new Set([
1757
- "label",
1758
- "message",
1759
- "title",
1760
- "description",
1761
- "heading",
1762
- "text",
1763
- "caption",
1764
- "subtitle",
1765
- "tooltip",
1766
- "errorMessage",
1767
- "successMessage",
1768
- "warningMessage",
1769
- "infoMessage",
1770
- "placeholder",
1771
- "helperText",
1772
- "hint",
1773
- "buttonText",
1774
- "linkText",
1775
- "headerText",
1776
- "footerText",
1777
- "confirmText",
1778
- "cancelText",
1779
- "submitText",
1780
- "greeting",
1781
- "welcome",
1782
- "instructions"
1783
- ]);
1784
- function classifyString(text, context, metadata = {}) {
1785
- const trimmed = text.trim();
1786
- if (trimmed.length === 0) {
1787
- return { translatable: false, confidence: "high", reason: "Empty or whitespace-only" };
1788
- }
1789
- if (trimmed.length === 1) {
1790
- return { translatable: false, confidence: "high", reason: "Single character" };
1791
- }
1792
- if (!/[a-zA-Z]/.test(trimmed)) {
1793
- return { translatable: false, confidence: "high", reason: "No alphabetic characters" };
1794
- }
1795
- if (URL_REGEX.test(trimmed)) {
1796
- return { translatable: false, confidence: "high", reason: "URL" };
1797
- }
1798
- if (EMAIL_REGEX.test(trimmed)) {
1799
- return { translatable: false, confidence: "high", reason: "Email address" };
1800
- }
1801
- if (FILE_PATH_REGEX.test(trimmed) && !trimmed.includes(" ")) {
1802
- return { translatable: false, confidence: "high", reason: "File path" };
1803
- }
1804
- if (COLOR_HEX_REGEX.test(trimmed) || COLOR_FUNC_REGEX.test(trimmed)) {
1805
- return { translatable: false, confidence: "high", reason: "Color code" };
1806
- }
1807
- if (CSS_UNIT_REGEX.test(trimmed)) {
1808
- return { translatable: false, confidence: "high", reason: "CSS unit value" };
1809
- }
1810
- if (MIME_TYPE_REGEX.test(trimmed)) {
1811
- return { translatable: false, confidence: "high", reason: "MIME type" };
1812
- }
1813
- if (DATE_FORMAT_REGEX.test(trimmed) && trimmed.length > 1) {
1814
- return { translatable: false, confidence: "high", reason: "Date format string" };
1815
- }
1816
- if (context === "jsx-attribute" && metadata.attributeName) {
1817
- if (NON_TRANSLATABLE_ATTRIBUTES.has(metadata.attributeName)) {
1818
- return { translatable: false, confidence: "high", reason: `Non-translatable attribute: ${metadata.attributeName}` };
1819
- }
1820
- if (metadata.attributeName.startsWith("data-") && !TRANSLATABLE_ATTRIBUTES.has(metadata.attributeName)) {
1821
- return { translatable: false, confidence: "high", reason: "data-* attribute" };
1822
- }
1823
- if (metadata.attributeName.startsWith("on") && metadata.attributeName.length > 2) {
1824
- const thirdChar = metadata.attributeName[2];
1825
- if (thirdChar && thirdChar === thirdChar.toUpperCase()) {
1826
- return { translatable: false, confidence: "high", reason: "Event handler attribute" };
1827
- }
1828
- }
1829
- if (TRANSLATABLE_ATTRIBUTES.has(metadata.attributeName)) {
1830
- return { translatable: true, confidence: "high", reason: `Translatable attribute: ${metadata.attributeName}` };
1831
- }
1832
- }
1833
- if (context === "jsx-text") {
1834
- const hasWords = /[a-zA-Z]{2,}/.test(trimmed);
1835
- if (hasWords) {
1836
- return { translatable: true, confidence: "high", reason: "JSX text with words" };
1837
- }
1838
- }
1839
- if (!trimmed.includes(" ") && (CAMEL_CASE_REGEX.test(trimmed) || PASCAL_CASE_REGEX.test(trimmed) || SCREAMING_SNAKE_REGEX.test(trimmed) || KEBAB_CASE_REGEX.test(trimmed))) {
1840
- return { translatable: false, confidence: "high", reason: "Code identifier" };
1841
- }
1842
- if (isTailwindClasses(trimmed)) {
1843
- return { translatable: false, confidence: "high", reason: "CSS/Tailwind classes" };
1844
- }
1845
- if (metadata.isInsideCallExpression) {
1846
- if (NON_TRANSLATABLE_CALLS.has(metadata.isInsideCallExpression)) {
1847
- return { translatable: false, confidence: "high", reason: `Inside ${metadata.isInsideCallExpression}()` };
1848
- }
1849
- }
1850
- if (metadata.parentType === "ThrowStatement" || metadata.isInsideCallExpression === "Error") {
1851
- return { translatable: false, confidence: "high", reason: "Error message" };
1852
- }
1853
- if ((context === "string-literal" || context === "template-literal") && metadata.parentType === "VariableDeclarator") {
1854
- return { translatable: true, confidence: "medium", reason: "String in variable declaration" };
1855
- }
1856
- const wordCount = trimmed.split(/\s+/).length;
1857
- if (wordCount >= 3) {
1858
- return { translatable: true, confidence: "medium", reason: `Multi-word string (${wordCount} words)` };
1859
- }
1860
- if (wordCount === 2 && /[a-zA-Z]{2,}/.test(trimmed)) {
1861
- return { translatable: true, confidence: "low", reason: "Short phrase (2 words)" };
1862
- }
1863
- if (/^[A-Z][a-z]/.test(trimmed) && context !== "string-literal") {
1864
- return { translatable: true, confidence: "low", reason: "Capitalized word, possibly UI text" };
1865
- }
1866
- return { translatable: false, confidence: "low", reason: "Ambiguous single-word string" };
1867
- }
1868
- function isTranslatableVarName(name) {
1869
- const lower = name.toLowerCase();
1870
- for (const varName of TRANSLATABLE_VAR_NAMES) {
1871
- if (lower === varName.toLowerCase() || lower.endsWith(varName.toLowerCase())) {
1872
- return true;
1873
- }
1874
- }
1875
- return false;
1876
- }
1877
- function isTailwindClasses(text) {
1878
- if (!TAILWIND_REGEX.test(text)) return false;
1879
- const parts = text.split(/\s+/);
1880
- let tailwindCount = 0;
1881
- for (const part of parts) {
1882
- if (TAILWIND_PREFIXES.some((prefix) => part.startsWith(prefix))) {
1883
- tailwindCount++;
1884
- }
1885
- }
1886
- return tailwindCount > parts.length / 2;
1887
- }
1888
-
1889
- // src/utils/wrap/analyzer.ts
1890
- var traverse2 = babelTraverse2.default || babelTraverse2;
1891
- var StringAnalyzer = class {
1892
- constructor(adapter) {
1893
- this.adapter = adapter;
1894
- }
1895
- /**
1896
- * Analyze all files matching the given patterns and return wrap candidates.
1897
- */
1898
- async analyzeProject(options, projectRoot = process.cwd()) {
1899
- const includePatterns = options.include?.length ? options.include : ["src/**/*.{tsx,jsx,ts,js}"];
1900
- const defaultIgnore = [
1901
- "**/node_modules/**",
1902
- "**/.next/**",
1903
- "**/dist/**",
1904
- "**/build/**",
1905
- "**/*.test.*",
1906
- "**/*.spec.*",
1907
- "**/*.stories.*",
1908
- "**/__tests__/**"
1909
- ];
1910
- const ignorePatterns = options.exclude ? [...defaultIgnore, ...options.exclude] : defaultIgnore;
1911
- const allFiles = /* @__PURE__ */ new Set();
1912
- for (const pattern of includePatterns) {
1913
- const files = await glob2(pattern, {
1914
- cwd: projectRoot,
1915
- absolute: true,
1916
- ignore: ignorePatterns
1917
- });
1918
- files.forEach((file) => allFiles.add(file));
1919
- }
1920
- const allCandidates = [];
1921
- for (const file of allFiles) {
1922
- try {
1923
- const candidates = this.analyzeFile(file);
1924
- allCandidates.push(...candidates);
1925
- } catch (error) {
1926
- if (options.verbose) {
1927
- const msg = error instanceof Error ? error.message : "Unknown error";
1928
- console.warn(`Warning: Failed to analyze ${file}: ${msg}`);
1929
- }
1930
- }
1931
- }
1932
- return allCandidates;
1933
- }
1934
- /**
1935
- * Analyze a single file and return wrap candidates.
1936
- */
1937
- analyzeFile(filePath) {
1938
- const code = readFileSync3(filePath, "utf-8");
1939
- return this.analyzeCode(code, filePath);
1940
- }
1941
- /**
1942
- * Analyze source code and return wrap candidates.
1943
- */
1944
- analyzeCode(code, filePath = "<input>") {
1945
- const candidates = [];
1946
- const ast = parse2(code, {
1947
- sourceType: "module",
1948
- plugins: ["jsx", "typescript"]
1949
- });
1950
- const vocoderImports = /* @__PURE__ */ new Map();
1951
- const tFunctionNames = /* @__PURE__ */ new Set();
1952
- traverse2(ast, {
1953
- // Track imports from @vocoder/react
1954
- ImportDeclaration: (path) => {
1955
- const source = path.node.source.value;
1956
- if (source === this.adapter.importSource) {
1957
- path.node.specifiers.forEach((spec) => {
1958
- if (spec.type === "ImportSpecifier") {
1959
- const imported = spec.imported.type === "Identifier" ? spec.imported.name : null;
1960
- const local = spec.local.name;
1961
- if (imported === this.adapter.componentName) {
1962
- vocoderImports.set(local, this.adapter.componentName);
1963
- }
1964
- if (imported === this.adapter.functionName) {
1965
- tFunctionNames.add(local);
1966
- }
1967
- if (imported === this.adapter.hookName) {
1968
- vocoderImports.set(local, this.adapter.hookName);
1969
- }
1970
- }
1971
- });
1972
- }
1973
- },
1974
- // Track destructured t from useVocoder()
1975
- VariableDeclarator: (path) => {
1976
- const init2 = path.node.init;
1977
- if (init2 && init2.type === "CallExpression" && init2.callee.type === "Identifier" && init2.callee.name === this.adapter.hookName && path.node.id.type === "ObjectPattern") {
1978
- path.node.id.properties.forEach((prop) => {
1979
- if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === this.adapter.functionName) {
1980
- const localName = prop.value.type === "Identifier" ? prop.value.name : this.adapter.functionName;
1981
- tFunctionNames.add(localName);
1982
- }
1983
- });
1984
- }
1985
- },
1986
- // Find bare JSX text
1987
- JSXText: (path) => {
1988
- const text = path.node.value;
1989
- const trimmed = text.trim();
1990
- if (!trimmed) return;
1991
- const ancestors = path.getAncestry().map((a) => a.node);
1992
- if (this.adapter.isAlreadyWrapped(ancestors, vocoderImports)) return;
1993
- const classification = classifyString(trimmed, "jsx-text", {
1994
- isInsideComponent: true
1995
- });
1996
- if (classification.translatable) {
1997
- candidates.push({
1998
- file: filePath,
1999
- line: path.node.loc?.start.line || 0,
2000
- column: path.node.loc?.start.column || 0,
2001
- text: trimmed,
2002
- confidence: classification.confidence,
2003
- strategy: "T-component",
2004
- context: "jsx-text",
2005
- reason: classification.reason
2006
- });
2007
- }
2008
- },
2009
- // Find translatable JSX attributes
2010
- JSXAttribute: (path) => {
2011
- const attrName = path.node.name?.name;
2012
- if (!attrName) return;
2013
- const value = path.node.value;
2014
- if (!value) return;
2015
- let text = null;
2016
- let context = "jsx-attribute";
2017
- if (value.type === "StringLiteral") {
2018
- text = value.value;
2019
- } else if (value.type === "JSXExpressionContainer" && value.expression.type === "StringLiteral") {
2020
- text = value.expression.value;
2021
- }
2022
- if (!text || !text.trim()) return;
2023
- if (value.type === "JSXExpressionContainer" && value.expression.type === "CallExpression") {
2024
- if (this.adapter.isAlreadyWrappedCall(value.expression, tFunctionNames)) return;
2025
- }
2026
- const classification = classifyString(text.trim(), context, {
2027
- attributeName: attrName,
2028
- isInsideComponent: true
2029
- });
2030
- if (classification.translatable) {
2031
- candidates.push({
2032
- file: filePath,
2033
- line: path.node.loc?.start.line || 0,
2034
- column: path.node.loc?.start.column || 0,
2035
- text: text.trim(),
2036
- confidence: classification.confidence,
2037
- strategy: "t-function",
2038
- context,
2039
- reason: classification.reason
2040
- });
2041
- }
2042
- },
2043
- // Find string literals in non-JSX contexts
2044
- StringLiteral: (path) => {
2045
- if (path.parent.type === "ImportDeclaration") return;
2046
- if (path.parent.type === "ExportDeclaration") return;
2047
- if (path.parent.type === "JSXAttribute") return;
2048
- if (path.parent.type === "JSXExpressionContainer" && path.parentPath?.parent?.type === "JSXAttribute") return;
2049
- if (path.parent.type === "JSXExpressionContainer") return;
2050
- if (path.parent.type === "ObjectProperty" && path.parent.key === path.node) return;
2051
- if (path.parent.type === "TSLiteralType") return;
2052
- if (isInsideTCall(path, tFunctionNames)) return;
2053
- const text = path.node.value;
2054
- if (!text.trim()) return;
2055
- const callExpr = getEnclosingCallExpression(path);
2056
- const parentType = path.parent.type;
2057
- const classification = classifyString(text.trim(), "string-literal", {
2058
- parentType,
2059
- isInsideCallExpression: callExpr,
2060
- isInsideComponent: false
2061
- });
2062
- let { confidence } = classification;
2063
- if (parentType === "VariableDeclarator" && path.parent.id?.type === "Identifier") {
2064
- const varName = path.parent.id.name;
2065
- if (isTranslatableVarName(varName) && classification.translatable) {
2066
- confidence = "high";
2067
- }
2068
- }
2069
- if (classification.translatable) {
2070
- candidates.push({
2071
- file: filePath,
2072
- line: path.node.loc?.start.line || 0,
2073
- column: path.node.loc?.start.column || 0,
2074
- text: text.trim(),
2075
- confidence,
2076
- strategy: "t-function",
2077
- context: "string-literal",
2078
- reason: classification.reason
2079
- });
2080
- }
2081
- },
2082
- // Find template literals
2083
- TemplateLiteral: (path) => {
2084
- if (path.parent.type === "ImportDeclaration") return;
2085
- if (path.parent.type === "TaggedTemplateExpression") return;
2086
- if (isInsideTCall(path, tFunctionNames)) return;
2087
- const quasis = path.node.quasis;
2088
- if (quasis.length === 0) return;
2089
- const parts = [];
2090
- for (let i = 0; i < quasis.length; i++) {
2091
- const quasi = quasis[i];
2092
- parts.push(quasi.value.raw);
2093
- if (i < path.node.expressions.length) {
2094
- const expr = path.node.expressions[i];
2095
- if (expr.type === "Identifier") {
2096
- parts.push(`{${expr.name}}`);
2097
- } else {
2098
- parts.push("{value}");
2099
- }
2100
- }
2101
- }
2102
- const text = parts.join("").trim();
2103
- if (!text) return;
2104
- const callExpr = getEnclosingCallExpression(path);
2105
- const parentType = path.parent.type;
2106
- const classification = classifyString(text, "template-literal", {
2107
- parentType,
2108
- isInsideCallExpression: callExpr,
2109
- isInsideComponent: false
2110
- });
2111
- if (classification.translatable) {
2112
- candidates.push({
2113
- file: filePath,
2114
- line: path.node.loc?.start.line || 0,
2115
- column: path.node.loc?.start.column || 0,
2116
- text,
2117
- confidence: classification.confidence,
2118
- strategy: "t-function",
2119
- context: "template-literal",
2120
- reason: classification.reason
2121
- });
2122
- }
2123
- }
2124
- });
2125
- return candidates;
2126
- }
2127
- };
2128
- function isInsideTCall(path, tNames) {
2129
- let current = path.parentPath;
2130
- while (current) {
2131
- if (current.node.type === "CallExpression") {
2132
- const callee = current.node.callee;
2133
- if (callee.type === "Identifier" && tNames.has(callee.name)) {
2134
- return true;
2135
- }
2136
- }
2137
- current = current.parentPath;
2138
- }
2139
- return false;
2140
- }
2141
- function getEnclosingCallExpression(path) {
2142
- let current = path.parentPath;
2143
- while (current) {
2144
- if (current.node.type === "CallExpression") {
2145
- const callee = current.node.callee;
2146
- if (callee.type === "Identifier") {
2147
- return callee.name;
2148
- }
2149
- if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.property.type === "Identifier") {
2150
- return `${callee.object.name}.${callee.property.name}`;
2151
- }
2152
- }
2153
- if (current.node.type === "NewExpression") {
2154
- const callee = current.node.callee;
2155
- if (callee.type === "Identifier") {
2156
- return callee.name;
2157
- }
2158
- }
2159
- current = current.parentPath;
2160
- }
2161
- return void 0;
2162
- }
2163
-
2164
- // src/utils/wrap/transformer.ts
2165
- import * as recast from "recast";
2166
- import { parse as babelParse } from "@babel/parser";
2167
- var babelParser = {
2168
- parse(source) {
2169
- return babelParse(source, {
2170
- sourceType: "module",
2171
- plugins: ["jsx", "typescript"],
2172
- tokens: true
2173
- });
2174
- }
2175
- };
2176
- var StringTransformer = class {
2177
- constructor(adapter) {
2178
- this.adapter = adapter;
2179
- }
2180
- /**
2181
- * Transform a file by wrapping the given candidates.
2182
- * Returns the transformed source code.
2183
- */
2184
- transform(code, candidates, filePath = "<input>") {
2185
- const ast = recast.parse(code, { parser: babelParser });
2186
- const b = recast.types.builders;
2187
- const wrapped = [];
2188
- const skipped = [];
2189
- const usedStrategies = /* @__PURE__ */ new Set();
2190
- const componentsNeedingHook = /* @__PURE__ */ new Set();
2191
- const candidatesByLocation = /* @__PURE__ */ new Map();
2192
- for (const c of candidates) {
2193
- candidatesByLocation.set(`${c.line}:${c.column}`, c);
2194
- }
2195
- let existingImportDecl = null;
2196
- const existingSpecifiers = /* @__PURE__ */ new Set();
2197
- const adapter = this.adapter;
2198
- recast.visit(ast, {
2199
- visitImportDeclaration(path) {
2200
- const source = path.node.source.value;
2201
- if (source === adapter.importSource) {
2202
- existingImportDecl = path;
2203
- for (const spec of path.node.specifiers || []) {
2204
- if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") {
2205
- existingSpecifiers.add(spec.imported.name);
2206
- }
2207
- }
2208
- }
2209
- this.traverse(path);
2210
- },
2211
- visitJSXText(path) {
2212
- const loc = path.node.loc;
2213
- if (!loc) {
2214
- this.traverse(path);
2215
- return;
2216
- }
2217
- const key = `${loc.start.line}:${loc.start.column}`;
2218
- const candidate = candidatesByLocation.get(key);
2219
- if (!candidate || candidate.strategy !== "T-component") {
2220
- this.traverse(path);
2221
- return;
2222
- }
2223
- const tOpen = b.jsxOpeningElement(
2224
- b.jsxIdentifier(adapter.componentName),
2225
- []
2226
- );
2227
- const tClose = b.jsxClosingElement(
2228
- b.jsxIdentifier(adapter.componentName)
2229
- );
2230
- const tElement = b.jsxElement(
2231
- tOpen,
2232
- tClose,
2233
- [b.jsxText(candidate.text)]
2234
- );
2235
- path.replace(tElement);
2236
- wrapped.push(candidate);
2237
- usedStrategies.add("T-component");
2238
- candidatesByLocation.delete(key);
2239
- return false;
2240
- },
2241
- visitJSXAttribute(path) {
2242
- const loc = path.node.loc;
2243
- if (!loc) {
2244
- this.traverse(path);
2245
- return;
2246
- }
2247
- const key = `${loc.start.line}:${loc.start.column}`;
2248
- const candidate = candidatesByLocation.get(key);
2249
- if (!candidate || candidate.strategy !== "t-function") {
2250
- this.traverse(path);
2251
- return;
2252
- }
2253
- const value = path.node.value;
2254
- if (!value) {
2255
- this.traverse(path);
2256
- return;
2257
- }
2258
- const tCall = b.callExpression(
2259
- b.identifier(adapter.functionName),
2260
- [b.stringLiteral(candidate.text)]
2261
- );
2262
- const exprContainer = b.jsxExpressionContainer(tCall);
2263
- path.node.value = exprContainer;
2264
- const componentFunc = findEnclosingComponent(path);
2265
- if (componentFunc) {
2266
- componentsNeedingHook.add(componentFunc);
2267
- }
2268
- wrapped.push(candidate);
2269
- usedStrategies.add("t-function");
2270
- candidatesByLocation.delete(key);
2271
- this.traverse(path);
2272
- },
2273
- visitStringLiteral(path) {
2274
- const loc = path.node.loc;
2275
- if (!loc) {
2276
- this.traverse(path);
2277
- return;
2278
- }
2279
- const key = `${loc.start.line}:${loc.start.column}`;
2280
- const candidate = candidatesByLocation.get(key);
2281
- if (!candidate || candidate.strategy !== "t-function") {
2282
- this.traverse(path);
2283
- return;
2284
- }
2285
- if (path.parent.node.type === "JSXAttribute") {
2286
- this.traverse(path);
2287
- return;
2288
- }
2289
- const tCall = b.callExpression(
2290
- b.identifier(adapter.functionName),
2291
- [b.stringLiteral(candidate.text)]
2292
- );
2293
- path.replace(tCall);
2294
- const componentFunc = findEnclosingComponent(path);
2295
- if (componentFunc) {
2296
- componentsNeedingHook.add(componentFunc);
2297
- }
2298
- wrapped.push(candidate);
2299
- usedStrategies.add("t-function");
2300
- candidatesByLocation.delete(key);
2301
- return false;
2302
- }
2303
- });
2304
- for (const candidate of candidatesByLocation.values()) {
2305
- skipped.push(candidate);
2306
- }
2307
- if (componentsNeedingHook.size > 0) {
2308
- this.injectUseVocoderHooks(ast, componentsNeedingHook, b);
2309
- }
2310
- this.manageImports(ast, usedStrategies, existingImportDecl, existingSpecifiers, componentsNeedingHook.size > 0, b);
2311
- const output = recast.print(ast).code;
2312
- return {
2313
- file: filePath,
2314
- output,
2315
- wrappedCount: wrapped.length,
2316
- wrapped,
2317
- skipped
2318
- };
2319
- }
2320
- /**
2321
- * Inject `const { t } = useVocoder();` at the top of component functions.
2322
- */
2323
- injectUseVocoderHooks(ast, componentFuncs, b) {
2324
- const adapterFunctionName = this.adapter.functionName;
2325
- const adapterHookName = this.adapter.hookName;
2326
- const buildHookDecl = () => b.variableDeclaration("const", [
2327
- b.variableDeclarator(
2328
- b.objectPattern([
2329
- b.property.from({
2330
- kind: "init",
2331
- key: b.identifier(adapterFunctionName),
2332
- value: b.identifier(adapterFunctionName),
2333
- shorthand: true
2334
- })
2335
- ]),
2336
- b.callExpression(b.identifier(adapterHookName), [])
2337
- )
2338
- ]);
2339
- recast.visit(ast, {
2340
- visitFunction(path) {
2341
- if (componentFuncs.has(path.node)) {
2342
- const body = path.node.body;
2343
- if (body.type === "BlockStatement") {
2344
- const alreadyHasHook = body.body.some((stmt) => {
2345
- if (stmt.type !== "VariableDeclaration") return false;
2346
- return stmt.declarations.some(
2347
- (decl) => decl.init?.type === "CallExpression" && decl.init.callee?.type === "Identifier" && decl.init.callee.name === "useVocoder"
2348
- );
2349
- });
2350
- if (!alreadyHasHook) {
2351
- body.body.unshift(buildHookDecl());
2352
- }
2353
- }
2354
- }
2355
- this.traverse(path);
2356
- },
2357
- visitArrowFunctionExpression(path) {
2358
- if (componentFuncs.has(path.node)) {
2359
- const body = path.node.body;
2360
- if (body.type === "BlockStatement") {
2361
- const alreadyHasHook = body.body.some((stmt) => {
2362
- if (stmt.type !== "VariableDeclaration") return false;
2363
- return stmt.declarations.some(
2364
- (decl) => decl.init?.type === "CallExpression" && decl.init.callee?.type === "Identifier" && decl.init.callee.name === "useVocoder"
2365
- );
2366
- });
2367
- if (!alreadyHasHook) {
2368
- body.body.unshift(buildHookDecl());
2369
- }
2370
- }
2371
- }
2372
- this.traverse(path);
2373
- }
2374
- });
2375
- }
2376
- /**
2377
- * Add or update @vocoder/react imports.
2378
- */
2379
- manageImports(ast, usedStrategies, existingImportPath, existingSpecifiers, needsHook, b) {
2380
- if (usedStrategies.size === 0) return;
2381
- const neededSpecifiers = /* @__PURE__ */ new Set();
2382
- if (usedStrategies.has("T-component")) {
2383
- neededSpecifiers.add(this.adapter.componentName);
2384
- }
2385
- if (usedStrategies.has("t-function") && needsHook) {
2386
- neededSpecifiers.add(this.adapter.hookName);
2387
- }
2388
- const missingSpecifiers = [];
2389
- for (const spec of neededSpecifiers) {
2390
- if (!existingSpecifiers.has(spec)) {
2391
- missingSpecifiers.push(spec);
2392
- }
2393
- }
2394
- if (missingSpecifiers.length === 0) return;
2395
- if (existingImportPath) {
2396
- for (const name of missingSpecifiers) {
2397
- const specifier = b.importSpecifier(b.identifier(name), b.identifier(name));
2398
- existingImportPath.node.specifiers.push(specifier);
2399
- }
2400
- } else {
2401
- const specifiers = missingSpecifiers.map(
2402
- (name) => b.importSpecifier(b.identifier(name), b.identifier(name))
2403
- );
2404
- const importDecl = b.importDeclaration(
2405
- specifiers,
2406
- b.stringLiteral(this.adapter.importSource)
2407
- );
2408
- const body = ast.program.body;
2409
- let lastImportIndex = -1;
2410
- for (let i = 0; i < body.length; i++) {
2411
- if (body[i].type === "ImportDeclaration") {
2412
- lastImportIndex = i;
2413
- }
2414
- }
2415
- if (lastImportIndex >= 0) {
2416
- body.splice(lastImportIndex + 1, 0, importDecl);
2417
- } else {
2418
- body.unshift(importDecl);
2419
- }
2420
- }
2421
- }
2422
- };
2423
- function findEnclosingComponent(path) {
2424
- let current = path.parent;
2425
- while (current) {
2426
- const node = current.node;
2427
- if (node.type === "FunctionDeclaration" && node.id?.name) {
2428
- const name = node.id.name;
2429
- if (/^[A-Z]/.test(name)) return node;
2430
- }
2431
- if (node.type === "ArrowFunctionExpression") {
2432
- const parent = current.parent?.node;
2433
- if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
2434
- const name = parent.id.name;
2435
- if (/^[A-Z]/.test(name)) return node;
2436
- }
2437
- }
2438
- if (node.type === "FunctionExpression") {
2439
- const parent = current.parent?.node;
2440
- if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
2441
- const name = parent.id.name;
2442
- if (/^[A-Z]/.test(name)) return node;
2443
- }
2444
- }
2445
- current = current.parent;
2446
- }
2447
- return null;
2448
- }
2449
-
2450
- // src/utils/wrap/adapters/react.ts
2451
- var reactAdapter = {
2452
- name: "react",
2453
- extensions: [".tsx", ".jsx", ".ts", ".js"],
2454
- importSource: "@vocoder/react",
2455
- componentName: "T",
2456
- functionName: "t",
2457
- hookName: "useVocoder",
2458
- translatableAttributes: [
2459
- "title",
2460
- "placeholder",
2461
- "alt",
2462
- "aria-label",
2463
- "aria-description",
2464
- "aria-placeholder",
2465
- "aria-roledescription",
2466
- "aria-valuetext",
2467
- "label",
2468
- "description",
2469
- "message",
2470
- "heading",
2471
- "caption",
2472
- "helperText",
2473
- "errorMessage",
2474
- "successMessage",
2475
- "tooltip"
2476
- ],
2477
- nonTranslatableAttributes: [
2478
- "className",
2479
- "class",
2480
- "href",
2481
- "src",
2482
- "id",
2483
- "key",
2484
- "ref",
2485
- "style",
2486
- "data-testid",
2487
- "data-cy",
2488
- "data-test",
2489
- "type",
2490
- "name",
2491
- "value",
2492
- "action",
2493
- "method",
2494
- "encType",
2495
- "target",
2496
- "rel",
2497
- "role",
2498
- "tabIndex",
2499
- "htmlFor",
2500
- "for",
2501
- "width",
2502
- "height",
2503
- "viewBox",
2504
- "xmlns",
2505
- "fill",
2506
- "stroke"
2507
- ],
2508
- isAlreadyWrapped(ancestors, imports) {
2509
- for (const ancestor of ancestors) {
2510
- if (ancestor.type === "JSXElement") {
2511
- const opening = ancestor.openingElement;
2512
- if (opening && opening.name && opening.name.type === "JSXIdentifier") {
2513
- const tagName = opening.name.name;
2514
- if (imports.has(tagName) && imports.get(tagName) === "T") {
2515
- return true;
2516
- }
2517
- }
2518
- }
2519
- }
2520
- return false;
2521
- },
2522
- isAlreadyWrappedCall(node, tNames) {
2523
- if (node.type === "CallExpression") {
2524
- const callee = node.callee;
2525
- if (callee.type === "Identifier" && tNames.has(callee.name)) {
2526
- return true;
2527
- }
2528
- }
2529
- return false;
2530
- },
2531
- getRequiredImports(strategies) {
2532
- const specifiers = [];
2533
- if (strategies.has("T-component")) {
2534
- specifiers.push("T");
2535
- }
2536
- if (strategies.has("t-function")) {
2537
- specifiers.push("useVocoder");
2538
- }
2539
- return { specifiers, source: "@vocoder/react" };
2540
- }
2541
- };
2542
-
2543
- // src/commands/wrap.ts
2544
- var CONFIDENCE_ORDER = ["high", "medium", "low"];
2545
- function meetsConfidenceThreshold(candidate, threshold) {
2546
- return CONFIDENCE_ORDER.indexOf(candidate) <= CONFIDENCE_ORDER.indexOf(threshold);
2547
- }
2548
- async function wrap(options = {}) {
2549
- const startTime = Date.now();
2550
- const projectRoot = process.cwd();
2551
- const confidenceThreshold = options.confidence || "high";
2552
- p3.intro("Vocoder Wrap");
2553
- const spinner4 = p3.spinner();
2554
- try {
2555
- spinner4.start("Scanning files for unwrapped strings");
2556
- const analyzer = new StringAnalyzer(reactAdapter);
2557
- const allCandidates = await analyzer.analyzeProject(options, projectRoot);
2558
- if (allCandidates.length === 0) {
2559
- spinner4.stop("No unwrapped strings found");
2560
- p3.log.info("All user-facing strings appear to be wrapped already.");
2561
- p3.outro("");
2562
- return 0;
2563
- }
2564
- spinner4.stop(
2565
- `Found ${chalk4.cyan(allCandidates.length)} candidate strings`
2566
- );
2567
- const filtered = allCandidates.filter(
2568
- (c) => meetsConfidenceThreshold(c.confidence, confidenceThreshold)
2569
- );
2570
- if (filtered.length === 0) {
2571
- p3.log.warn(
2572
- `No strings meet the ${chalk4.bold(confidenceThreshold)} confidence threshold.`
2573
- );
2574
- p3.log.info("Try --confidence medium or --confidence low to see more candidates.");
2575
- p3.outro("");
2576
- return 0;
2577
- }
2578
- p3.log.info(
2579
- `${filtered.length} strings meet ${chalk4.bold(confidenceThreshold)} confidence threshold`
2580
- );
2581
- const byFile = /* @__PURE__ */ new Map();
2582
- for (const c of filtered) {
2583
- const existing = byFile.get(c.file) || [];
2584
- existing.push(c);
2585
- byFile.set(c.file, existing);
2586
- }
2587
- if (options.dryRun) {
2588
- const lines = [];
2589
- for (const [file, candidates] of byFile) {
2590
- const relPath = relative2(projectRoot, file);
2591
- lines.push(chalk4.bold(relPath));
2592
- for (const c of candidates) {
2593
- const confidenceColor = c.confidence === "high" ? chalk4.green : c.confidence === "medium" ? chalk4.yellow : chalk4.red;
2594
- const strategyLabel = c.strategy === "T-component" ? "<T>" : "t()";
2595
- lines.push(
2596
- ` ${chalk4.dim(`L${c.line}`)} ${confidenceColor(`[${c.confidence}]`)} ${chalk4.cyan(strategyLabel)} "${truncate(c.text, 50)}"`
2597
- );
2598
- if (options.verbose) {
2599
- lines.push(chalk4.dim(` ${c.reason}`));
2600
- }
2601
- }
2602
- lines.push("");
2603
- }
2604
- lines.push(summarizeCandidates(filtered));
2605
- p3.note(lines.join("\n"), "Dry run \u2014 would wrap");
2606
- p3.outro("Run without --dry-run to apply changes.");
2607
- return 0;
2608
- }
2609
- let accepted;
2610
- if (options.interactive) {
2611
- accepted = await interactiveConfirm(byFile, projectRoot);
2612
- if (accepted.length === 0) {
2613
- p3.log.warn("No strings selected for wrapping.");
2614
- p3.outro("");
2615
- return 0;
2616
- }
2617
- } else {
2618
- accepted = filtered;
2619
- }
2620
- spinner4.start("Wrapping strings");
2621
- const transformer = new StringTransformer(reactAdapter);
2622
- let totalWrapped = 0;
2623
- let filesModified = 0;
2624
- const acceptedByFile = /* @__PURE__ */ new Map();
2625
- for (const c of accepted) {
2626
- const existing = acceptedByFile.get(c.file) || [];
2627
- existing.push(c);
2628
- acceptedByFile.set(c.file, existing);
2629
- }
2630
- for (const [file, candidates] of acceptedByFile) {
2631
- const code = readFileSync4(file, "utf-8");
2632
- const result = transformer.transform(code, candidates, file);
2633
- if (result.wrappedCount > 0) {
2634
- writeFileSync2(file, result.output, "utf-8");
2635
- totalWrapped += result.wrappedCount;
2636
- filesModified++;
2637
- }
2638
- if (options.verbose && result.skipped.length > 0) {
2639
- const relPath = relative2(projectRoot, file);
2640
- p3.log.info(`Skipped ${result.skipped.length} strings in ${relPath}`);
2641
- }
2642
- }
2643
- spinner4.stop(
2644
- `Wrapped ${chalk4.cyan(totalWrapped)} strings across ${chalk4.cyan(filesModified)} files`
2645
- );
2646
- const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
2647
- p3.outro(`Done! (${duration}s)`);
2648
- p3.log.info("Next steps:");
2649
- p3.log.info(" 1. Review the changes (git diff)");
2650
- p3.log.info(" 2. Run your tests to verify nothing broke");
2651
- p3.log.info(' 3. Run "vocoder sync" to extract and translate');
2652
- return 0;
2653
- } catch (error) {
2654
- spinner4.stop();
2655
- if (error instanceof Error) {
2656
- p3.log.error(error.message);
2657
- if (options.verbose) {
2658
- p3.log.info(`Full error: ${error.stack ?? error}`);
2659
- }
2660
- }
2661
- return 1;
2662
- }
2663
- }
2664
- async function interactiveConfirm(byFile, projectRoot) {
2665
- const accepted = [];
2666
- p3.log.info("Interactive mode \u2014 confirm each string:");
2667
- for (const [file, candidates] of byFile) {
2668
- const relPath = relative2(projectRoot, file);
2669
- p3.log.step(chalk4.bold(relPath));
2670
- let skipFile = false;
2671
- for (const c of candidates) {
2672
- if (skipFile) break;
2673
- const strategyLabel = c.strategy === "T-component" ? "<T>" : "t()";
2674
- const label = `L${c.line} ${strategyLabel} "${truncate(c.text, 50)}"`;
2675
- const action = await p3.select({
2676
- message: label,
2677
- options: [
2678
- { value: "yes", label: "Yes, wrap this string" },
2679
- { value: "no", label: "No, skip" },
2680
- { value: "all", label: "Accept all remaining" },
2681
- { value: "skip", label: "Skip this file" },
2682
- { value: "quit", label: "Quit" }
2683
- ]
2684
- });
2685
- if (p3.isCancel(action) || action === "quit") {
2686
- return accepted;
2687
- }
2688
- if (action === "yes") {
2689
- accepted.push(c);
2690
- } else if (action === "all") {
2691
- accepted.push(c);
2692
- const remaining = candidates.slice(candidates.indexOf(c) + 1);
2693
- accepted.push(...remaining);
2694
- for (const [, moreCandidates] of byFile) {
2695
- if (moreCandidates !== candidates) {
2696
- accepted.push(...moreCandidates);
2697
- }
2698
- }
2699
- return accepted;
2700
- } else if (action === "skip") {
2701
- skipFile = true;
2702
- }
2703
- }
2704
- }
2705
- return accepted;
2706
- }
2707
- function truncate(text, maxLen) {
2708
- if (text.length <= maxLen) return text;
2709
- return text.slice(0, maxLen - 3) + "...";
2710
- }
2711
- function summarizeCandidates(candidates) {
2712
- let high = 0;
2713
- let medium = 0;
2714
- let low = 0;
2715
- let tComponent = 0;
2716
- let tFunction = 0;
2717
- for (const c of candidates) {
2718
- if (c.confidence === "high") high++;
2719
- else if (c.confidence === "medium") medium++;
2720
- else low++;
2721
- if (c.strategy === "T-component") tComponent++;
2722
- else tFunction++;
2723
- }
2724
- const parts = [];
2725
- if (high > 0) parts.push(chalk4.green(`${high} high`));
2726
- if (medium > 0) parts.push(chalk4.yellow(`${medium} medium`));
2727
- if (low > 0) parts.push(chalk4.red(`${low} low`));
2728
- return `${candidates.length} total (${parts.join(", ")}) | ${tComponent} <T>, ${tFunction} t()`;
2729
- }
2730
-
2731
1915
  // src/bin.ts
2732
1916
  function collect(value, previous = []) {
2733
1917
  return previous.concat([value]);
@@ -2737,12 +1921,13 @@ async function runCommand(command, options) {
2737
1921
  process.exitCode = exitCode;
2738
1922
  }
2739
1923
  var program = new Command();
2740
- program.name("vocoder").description("Vocoder CLI - Sync translations for your application").version("0.1.2");
2741
- 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, {
2742
- ...options,
2743
- noFallback: options.noFallback ? true : void 0
2744
- }));
2745
- 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");
2746
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
+ });
2747
1932
  program.parse(process.argv);
2748
1933
  //# sourceMappingURL=bin.mjs.map