@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/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/commands/init.ts
260
- import { existsSync, readFileSync, writeFileSync } from "fs";
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 maskApiKey(key) {
472
- if (key.length <= 8) return key;
473
- return `${key.slice(0, 4)}...${key.slice(-4)}`;
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 spinner4 = p.spinner();
784
+ const spinner3 = p.spinner();
481
785
  try {
482
- const existingKey = getEnvValue(envPath, "VOCODER_API_KEY");
483
- if (existingKey && existingKey.startsWith("vc_")) {
484
- const existingApi = new VocoderAPI({ apiUrl, apiKey: existingKey });
485
- try {
486
- const config = await existingApi.getProjectConfig();
487
- p.log.info("Existing configuration found:");
488
- p.note(
489
- [
490
- `Project: ${config.projectName}`,
491
- `Workspace: ${config.organizationName}`,
492
- `Source: ${config.sourceLocale}`,
493
- `Targets: ${config.targetLocales.join(", ")}`,
494
- `Key: ${maskApiKey(existingKey)}`
495
- ].join("\n")
496
- );
497
- if (options.yes) {
498
- p.outro("Configuration unchanged. You're all set!");
499
- return 0;
500
- }
501
- const action = await p.select({
502
- message: "What would you like to do?",
503
- options: [
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
- if (p.isCancel(action)) {
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
- spinner4.start("Creating setup session");
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
- spinner4.stop("Setup session created");
822
+ spinner3.stop("Setup session created");
549
823
  const verificationUrlString = start.verificationUrl;
550
- if (gitContext.warnings.length > 0) {
551
- for (const warning of gitContext.warnings) {
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 for verification.");
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
- spinner4.start("Waiting for browser authorization...");
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
- spinner4.message(`Waiting for browser authorization... (${pendingMessage})`);
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
- spinner4.stop("Setup failed");
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
- spinner4.stop("Authorization complete!");
598
- const key = "VOCODER_API_KEY";
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
- p.log.info(`Project: ${status.credentials.projectName}`);
644
- p.log.info(`Workspace: ${status.credentials.organizationName}`);
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
- spinner4.stop("Authorization timed out");
649
- p.log.error("Authorization timed out. Run `vocoder init` again.");
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
- spinner4.stop();
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 execSync2 } from "child_process";
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 = execSync2("git rev-parse --abbrev-ref HEAD", {
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 as writeFileSync2 } from "fs";
969
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
746
970
 
747
971
  // src/utils/config.ts
748
- import chalk from "chalk";
749
- import { config as loadEnv } from "dotenv";
750
- loadEnv();
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(chalk.dim("\n Configuration sources:"));
841
- console.log(chalk.dim(` Include patterns: ${configSources.extractionPattern}`));
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(chalk.dim(` Exclude patterns: ${configSources.excludePattern}`));
1067
+ console.log(chalk2.dim(` Exclude patterns: ${configSources.excludePattern}`));
844
1068
  }
845
- console.log(chalk.dim(` API key: ${configSources.apiKey}`));
846
- console.log(chalk.dim(` API URL: ${configSources.apiUrl}
1069
+ console.log(chalk2.dim(` API key: ${configSources.apiKey}`));
1070
+ console.log(chalk2.dim(` API URL: ${configSources.apiUrl}
847
1071
  `));
848
- console.log(chalk.dim(` Sync mode: ${configSources.mode}`));
1072
+ console.log(chalk2.dim(` Sync mode: ${configSources.mode}`));
849
1073
  if (maxWaitMs) {
850
- console.log(chalk.dim(` Max wait: ${configSources.maxWaitMs}`));
1074
+ console.log(chalk2.dim(` Max wait: ${configSources.maxWaitMs}`));
851
1075
  }
852
- console.log(chalk.dim(` No fallback: ${configSources.noFallback}
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 chalk2 from "chalk";
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
- writeFileSync2(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
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 spinner4 = p2.spinner();
1619
+ const spinner3 = p2.spinner();
1396
1620
  try {
1397
- spinner4.start("Detecting branch");
1621
+ spinner3.start("Detecting branch");
1398
1622
  const branch = detectBranch(options.branch);
1399
- spinner4.stop(`Branch: ${chalk2.cyan(branch)}`);
1400
- spinner4.start("Loading project configuration");
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
- spinner4.stop("Project configuration loaded");
1646
+ spinner3.stop("Project configuration loaded");
1423
1647
  if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
1424
1648
  p2.log.warn(
1425
- `Skipping translations (${chalk2.cyan(branch)} is not a target branch)`
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
- spinner4.start(`Extracting strings from ${patternsDisplay}`);
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
- spinner4.stop("No translatable strings found");
1442
- 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");
1443
1667
  p2.outro("");
1444
1668
  return 0;
1445
1669
  }
1446
- spinner4.stop(
1447
- `Extracted ${chalk2.cyan(extractedStrings.length)} strings from ${chalk2.cyan(patternsDisplay)}`
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
- spinner4.start("Submitting strings to Vocoder API");
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
- spinner4.stop(`Submitted to API - Batch ${chalk2.cyan(batchResponse.batchId)}`);
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: ${chalk2.cyan(batchResponse.newStrings)}`);
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: ${chalk2.yellow(batchResponse.deletedStrings)} (archived)`
1740
+ `Deleted strings: ${chalk3.yellow(batchResponse.deletedStrings)} (archived)`
1517
1741
  );
1518
1742
  }
1519
- p2.log.info(`Total strings: ${chalk2.cyan(batchResponse.totalStrings)}`);
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
- spinner4.start(`Waiting for translations (max ${waitTimeoutMs}ms)`);
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
- spinner4.message(`Translating... ${percent}%`);
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
- spinner4.stop("Translations complete");
1782
+ spinner3.stop("Translations complete");
1559
1783
  } catch (error) {
1560
- spinner4.stop("Translation wait incomplete");
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
- spinner4.start("Loading fallback translations");
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
- spinner4.stop(`Using local cached snapshot (${cacheBranchLabel})`);
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
- spinner4.stop("Using latest completed API snapshot");
1815
+ spinner3.stop("Using latest completed API snapshot");
1592
1816
  } else {
1593
- spinner4.stop("No completed API snapshot available");
1817
+ spinner3.stop("No completed API snapshot available");
1594
1818
  }
1595
1819
  } catch (error) {
1596
- spinner4.stop("Failed to fetch API snapshot");
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
- spinner4.stop();
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("Set your API key:");
1674
- p2.log.info(' export VOCODER_API_KEY="your-api-key"');
1675
- p2.log.info(" or add it to your .env file");
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 - Sync translations for your application").version("0.1.2");
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