@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/README.md +6 -57
- package/dist/bin.mjs +397 -1212
- package/dist/bin.mjs.map +1 -1
- package/package.json +1 -2
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
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
801
|
+
spinner3.stop("Found existing project!");
|
|
475
802
|
p.outro("Vocoder is already set up for this repository.");
|
|
476
|
-
|
|
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
|
-
|
|
811
|
+
spinner3.stop("No existing project found for this repo.");
|
|
480
812
|
}
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
867
|
+
spinner3.stop("Setup complete!");
|
|
536
868
|
const { credentials } = status;
|
|
537
869
|
p.outro("Vocoder initialized successfully!");
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
637
|
-
|
|
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 =
|
|
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
|
|
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 (!
|
|
1419
|
+
if (!existsSync2(cacheFilePath)) {
|
|
1083
1420
|
continue;
|
|
1084
1421
|
}
|
|
1085
1422
|
try {
|
|
1086
|
-
const raw =
|
|
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(
|
|
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
|
|
1619
|
+
const spinner3 = p2.spinner();
|
|
1283
1620
|
try {
|
|
1284
|
-
|
|
1621
|
+
spinner3.start("Detecting branch");
|
|
1285
1622
|
const branch = detectBranch(options.branch);
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1329
|
-
p2.log.warn("Make sure you are
|
|
1665
|
+
spinner3.stop("No translatable strings found");
|
|
1666
|
+
p2.log.warn("Make sure you are wrapping translatable strings with Vocoder");
|
|
1330
1667
|
p2.outro("");
|
|
1331
1668
|
return 0;
|
|
1332
1669
|
}
|
|
1333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1782
|
+
spinner3.stop("Translations complete");
|
|
1446
1783
|
} catch (error) {
|
|
1447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1815
|
+
spinner3.stop("Using latest completed API snapshot");
|
|
1479
1816
|
} else {
|
|
1480
|
-
|
|
1817
|
+
spinner3.stop("No completed API snapshot available");
|
|
1481
1818
|
}
|
|
1482
1819
|
} catch (error) {
|
|
1483
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|