@vocoder/cli 0.1.3 → 0.1.4
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 +84 -81
- package/dist/bin.mjs +110 -220
- package/dist/bin.mjs.map +1 -1
- package/package.json +1 -1
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,12 +255,29 @@ 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";
|
|
261
|
-
import { join } from "path";
|
|
262
|
-
|
|
263
281
|
// src/utils/git-identity.ts
|
|
264
282
|
import { execSync } from "child_process";
|
|
265
283
|
import { relative, resolve } from "path";
|
|
@@ -359,55 +377,11 @@ function resolveGitContext() {
|
|
|
359
377
|
// src/commands/init.ts
|
|
360
378
|
import { spawn } from "child_process";
|
|
361
379
|
var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
|
|
362
|
-
function escapeRegExp(value) {
|
|
363
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
364
|
-
}
|
|
365
380
|
function parseTargetLocales(value) {
|
|
366
381
|
if (!value) return void 0;
|
|
367
382
|
const locales = value.split(",").map((locale) => locale.trim()).filter(Boolean);
|
|
368
383
|
return locales.length > 0 ? locales : void 0;
|
|
369
384
|
}
|
|
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
385
|
async function sleep(ms) {
|
|
412
386
|
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
413
387
|
}
|
|
@@ -468,76 +442,44 @@ function printPlanLimitMessage(apiUrl, message) {
|
|
|
468
442
|
${message}`);
|
|
469
443
|
p.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
|
|
470
444
|
}
|
|
471
|
-
function
|
|
472
|
-
|
|
473
|
-
|
|
445
|
+
function printNextSteps(projectName, organizationName) {
|
|
446
|
+
p.log.info(`Project: ${chalk.bold(projectName)}`);
|
|
447
|
+
p.log.info(`Workspace: ${chalk.bold(organizationName)}`);
|
|
448
|
+
p.log.info("");
|
|
449
|
+
p.log.info("Next steps:");
|
|
450
|
+
p.log.info(` 1. Add ${chalk.cyan("@vocoder/unplugin")} to your build config`);
|
|
451
|
+
p.log.info(` 2. Wrap translatable strings with ${chalk.green("<T>")}...${chalk.green("</T>")}`);
|
|
452
|
+
p.log.info(" 3. Push to trigger extraction + translation");
|
|
474
453
|
}
|
|
475
454
|
async function init(options = {}) {
|
|
476
|
-
const projectRoot = process.cwd();
|
|
477
455
|
const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
|
|
478
|
-
const envPath = join(projectRoot, ".env");
|
|
479
456
|
p.intro("Vocoder Setup");
|
|
480
457
|
const spinner4 = p.spinner();
|
|
481
458
|
try {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
]
|
|
507
|
-
});
|
|
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
|
-
}
|
|
459
|
+
const gitContext = resolveGitContext();
|
|
460
|
+
const identity = gitContext.identity;
|
|
461
|
+
if (gitContext.warnings.length > 0) {
|
|
462
|
+
for (const warning of gitContext.warnings) {
|
|
463
|
+
p.log.warn(warning);
|
|
535
464
|
}
|
|
536
465
|
}
|
|
466
|
+
if (identity) {
|
|
467
|
+
spinner4.start("Checking for existing project...");
|
|
468
|
+
const api2 = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
469
|
+
const existing = await api2.lookupProjectByRepo({
|
|
470
|
+
repoCanonical: identity.repoCanonical,
|
|
471
|
+
scopePath: identity.repoScopePath
|
|
472
|
+
});
|
|
473
|
+
if (existing) {
|
|
474
|
+
spinner4.stop("Found existing project!");
|
|
475
|
+
p.outro("Vocoder is already set up for this repository.");
|
|
476
|
+
printNextSteps(existing.projectName, existing.organizationName);
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
spinner4.stop("No existing project found for this repo.");
|
|
480
|
+
}
|
|
537
481
|
spinner4.start("Creating setup session");
|
|
538
482
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
539
|
-
const gitContext = resolveGitContext();
|
|
540
|
-
const identity = gitContext.identity;
|
|
541
483
|
const start = await api.startInitSession({
|
|
542
484
|
projectName: options.projectName,
|
|
543
485
|
sourceLocale: options.sourceLocale,
|
|
@@ -547,12 +489,8 @@ async function init(options = {}) {
|
|
|
547
489
|
});
|
|
548
490
|
spinner4.stop("Setup session created");
|
|
549
491
|
const verificationUrlString = start.verificationUrl;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
p.log.warn(warning);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
p.note(verificationUrlString, "Authorize in your browser");
|
|
492
|
+
p.log.info("Create a project in your browser to continue.");
|
|
493
|
+
p.note(verificationUrlString, "Setup URL");
|
|
556
494
|
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
557
495
|
const shouldOpen = options.yes ? true : await p.confirm({ message: "Open this URL in your browser?" });
|
|
558
496
|
if (p.isCancel(shouldOpen)) {
|
|
@@ -562,14 +500,14 @@ async function init(options = {}) {
|
|
|
562
500
|
if (shouldOpen) {
|
|
563
501
|
const opened = await tryOpenBrowser(verificationUrlString);
|
|
564
502
|
if (opened) {
|
|
565
|
-
p.log.info("Opened your browser
|
|
503
|
+
p.log.info("Opened your browser.");
|
|
566
504
|
} else {
|
|
567
505
|
p.log.info("Could not open a browser automatically. Use the URL above.");
|
|
568
506
|
}
|
|
569
507
|
}
|
|
570
508
|
}
|
|
571
509
|
const expiresAt = new Date(start.expiresAt).getTime();
|
|
572
|
-
spinner4.start("Waiting for
|
|
510
|
+
spinner4.start("Waiting for setup to complete...");
|
|
573
511
|
while (Date.now() < expiresAt) {
|
|
574
512
|
const status = await api.getInitSessionStatus({
|
|
575
513
|
sessionId: start.sessionId,
|
|
@@ -578,7 +516,7 @@ async function init(options = {}) {
|
|
|
578
516
|
if (status.status === "pending") {
|
|
579
517
|
const pendingMessage = status.message?.trim();
|
|
580
518
|
if (pendingMessage) {
|
|
581
|
-
spinner4.message(`Waiting for
|
|
519
|
+
spinner4.message(`Waiting for setup to complete... (${pendingMessage})`);
|
|
582
520
|
}
|
|
583
521
|
await sleep((status.pollIntervalSeconds || start.poll.intervalSeconds) * 1e3);
|
|
584
522
|
continue;
|
|
@@ -594,70 +532,19 @@ async function init(options = {}) {
|
|
|
594
532
|
return 1;
|
|
595
533
|
}
|
|
596
534
|
if (status.status === "completed") {
|
|
597
|
-
spinner4.stop("
|
|
598
|
-
const
|
|
599
|
-
const desiredLine = `${key}=${status.credentials.apiKey}`;
|
|
600
|
-
const existingLine = getEnvLine(envPath, key);
|
|
601
|
-
const isAlreadyCurrent = existingLine === desiredLine;
|
|
602
|
-
let didOverwrite = false;
|
|
603
|
-
if (!isAlreadyCurrent) {
|
|
604
|
-
try {
|
|
605
|
-
upsertEnvValue({
|
|
606
|
-
filePath: envPath,
|
|
607
|
-
key,
|
|
608
|
-
value: status.credentials.apiKey,
|
|
609
|
-
allowOverwrite: Boolean(options.yes)
|
|
610
|
-
});
|
|
611
|
-
didOverwrite = Boolean(existingLine);
|
|
612
|
-
} catch (error) {
|
|
613
|
-
const overwriteConflict = error instanceof Error && error.message.includes(`${key} already exists in ${envPath}`);
|
|
614
|
-
if (!overwriteConflict) {
|
|
615
|
-
throw error;
|
|
616
|
-
}
|
|
617
|
-
const shouldOverwrite = await p.confirm({
|
|
618
|
-
message: `${key} already exists in ${envPath}. Overwrite it?`
|
|
619
|
-
});
|
|
620
|
-
if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
621
|
-
p.log.warn("Existing VOCODER_API_KEY was not changed.");
|
|
622
|
-
p.log.info("Re-run with --yes to overwrite it without prompting.");
|
|
623
|
-
p.cancel("Setup cancelled.");
|
|
624
|
-
return 1;
|
|
625
|
-
}
|
|
626
|
-
upsertEnvValue({
|
|
627
|
-
filePath: envPath,
|
|
628
|
-
key,
|
|
629
|
-
value: status.credentials.apiKey,
|
|
630
|
-
allowOverwrite: true
|
|
631
|
-
});
|
|
632
|
-
didOverwrite = true;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (isAlreadyCurrent) {
|
|
636
|
-
p.log.info(`VOCODER_API_KEY already matches your .env file`);
|
|
637
|
-
} else if (didOverwrite) {
|
|
638
|
-
p.log.success(`Updated VOCODER_API_KEY in .env`);
|
|
639
|
-
} else {
|
|
640
|
-
p.log.success(`Wrote VOCODER_API_KEY to .env`);
|
|
641
|
-
}
|
|
535
|
+
spinner4.stop("Setup complete!");
|
|
536
|
+
const { credentials } = status;
|
|
642
537
|
p.outro("Vocoder initialized successfully!");
|
|
643
|
-
|
|
644
|
-
p.log.info(`Workspace: ${status.credentials.organizationName}`);
|
|
538
|
+
printNextSteps(credentials.projectName, credentials.organizationName);
|
|
645
539
|
return 0;
|
|
646
540
|
}
|
|
647
541
|
}
|
|
648
|
-
spinner4.stop("
|
|
649
|
-
p.log.error("
|
|
542
|
+
spinner4.stop("Setup timed out");
|
|
543
|
+
p.log.error("Setup timed out. Run `vocoder init` again.");
|
|
650
544
|
p.cancel("Setup could not be completed.");
|
|
651
545
|
return 1;
|
|
652
546
|
} catch (error) {
|
|
653
547
|
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
|
-
}
|
|
661
548
|
if (error instanceof Error) {
|
|
662
549
|
if (isPlanLimitFailure(error.message)) {
|
|
663
550
|
printPlanLimitMessage(apiUrl, error.message);
|
|
@@ -742,10 +629,10 @@ function matchBranchPattern(branch, pattern) {
|
|
|
742
629
|
}
|
|
743
630
|
|
|
744
631
|
// src/commands/sync.ts
|
|
745
|
-
import { existsSync
|
|
632
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
746
633
|
|
|
747
634
|
// src/utils/config.ts
|
|
748
|
-
import
|
|
635
|
+
import chalk2 from "chalk";
|
|
749
636
|
import { config as loadEnv } from "dotenv";
|
|
750
637
|
loadEnv();
|
|
751
638
|
function validateLocalConfig(config) {
|
|
@@ -837,19 +724,19 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
837
724
|
configSources.noFallback = "environment";
|
|
838
725
|
}
|
|
839
726
|
if (verbose) {
|
|
840
|
-
console.log(
|
|
841
|
-
console.log(
|
|
727
|
+
console.log(chalk2.dim("\n Configuration sources:"));
|
|
728
|
+
console.log(chalk2.dim(` Include patterns: ${configSources.extractionPattern}`));
|
|
842
729
|
if (excludePattern.length > 0) {
|
|
843
|
-
console.log(
|
|
730
|
+
console.log(chalk2.dim(` Exclude patterns: ${configSources.excludePattern}`));
|
|
844
731
|
}
|
|
845
|
-
console.log(
|
|
846
|
-
console.log(
|
|
732
|
+
console.log(chalk2.dim(` API key: ${configSources.apiKey}`));
|
|
733
|
+
console.log(chalk2.dim(` API URL: ${configSources.apiUrl}
|
|
847
734
|
`));
|
|
848
|
-
console.log(
|
|
735
|
+
console.log(chalk2.dim(` Sync mode: ${configSources.mode}`));
|
|
849
736
|
if (maxWaitMs) {
|
|
850
|
-
console.log(
|
|
737
|
+
console.log(chalk2.dim(` Max wait: ${configSources.maxWaitMs}`));
|
|
851
738
|
}
|
|
852
|
-
console.log(
|
|
739
|
+
console.log(chalk2.dim(` No fallback: ${configSources.noFallback}
|
|
853
740
|
`));
|
|
854
741
|
}
|
|
855
742
|
return {
|
|
@@ -866,7 +753,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
866
753
|
|
|
867
754
|
// src/utils/extract.ts
|
|
868
755
|
import { createHash } from "crypto";
|
|
869
|
-
import { readFileSync
|
|
756
|
+
import { readFileSync } from "fs";
|
|
870
757
|
import { parse } from "@babel/parser";
|
|
871
758
|
import babelTraverse from "@babel/traverse";
|
|
872
759
|
import { glob } from "glob";
|
|
@@ -910,7 +797,7 @@ var StringExtractor = class {
|
|
|
910
797
|
* Extract strings from a single file
|
|
911
798
|
*/
|
|
912
799
|
async extractFromFile(filePath, projectRoot) {
|
|
913
|
-
const code =
|
|
800
|
+
const code = readFileSync(filePath, "utf-8");
|
|
914
801
|
const strings = [];
|
|
915
802
|
const relativeFilePath = pathRelative(projectRoot, filePath).split("\\").join("/");
|
|
916
803
|
try {
|
|
@@ -1137,8 +1024,8 @@ var StringExtractor = class {
|
|
|
1137
1024
|
};
|
|
1138
1025
|
|
|
1139
1026
|
// src/commands/sync.ts
|
|
1140
|
-
import
|
|
1141
|
-
import { join
|
|
1027
|
+
import chalk3 from "chalk";
|
|
1028
|
+
import { join } from "path";
|
|
1142
1029
|
function isRecord(value) {
|
|
1143
1030
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1144
1031
|
}
|
|
@@ -1186,17 +1073,17 @@ function getCacheFilePath(projectRoot, branch) {
|
|
|
1186
1073
|
const slug = branch.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
|
|
1187
1074
|
const branchHash = createHash2("sha1").update(branch).digest("hex").slice(0, 12);
|
|
1188
1075
|
const filename = `${slug || "branch"}-${branchHash}.json`;
|
|
1189
|
-
return
|
|
1076
|
+
return join(projectRoot, ".vocoder", "cache", "sync", filename);
|
|
1190
1077
|
}
|
|
1191
1078
|
function readLocalSnapshotCache(params) {
|
|
1192
1079
|
const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
|
|
1193
1080
|
for (const candidateBranch of candidateBranches) {
|
|
1194
1081
|
const cacheFilePath = getCacheFilePath(params.projectRoot, candidateBranch);
|
|
1195
|
-
if (!
|
|
1082
|
+
if (!existsSync(cacheFilePath)) {
|
|
1196
1083
|
continue;
|
|
1197
1084
|
}
|
|
1198
1085
|
try {
|
|
1199
|
-
const raw =
|
|
1086
|
+
const raw = readFileSync2(cacheFilePath, "utf-8");
|
|
1200
1087
|
const parsed = JSON.parse(raw);
|
|
1201
1088
|
if (!isRecord(parsed)) {
|
|
1202
1089
|
continue;
|
|
@@ -1222,7 +1109,7 @@ function readLocalSnapshotCache(params) {
|
|
|
1222
1109
|
}
|
|
1223
1110
|
function writeLocalSnapshotCache(params) {
|
|
1224
1111
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
|
|
1225
|
-
mkdirSync(
|
|
1112
|
+
mkdirSync(join(params.projectRoot, ".vocoder", "cache", "sync"), {
|
|
1226
1113
|
recursive: true
|
|
1227
1114
|
});
|
|
1228
1115
|
const payload = {
|
|
@@ -1236,7 +1123,7 @@ function writeLocalSnapshotCache(params) {
|
|
|
1236
1123
|
...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
|
|
1237
1124
|
translations: params.translations
|
|
1238
1125
|
};
|
|
1239
|
-
|
|
1126
|
+
writeFileSync(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
1240
1127
|
return cacheFilePath;
|
|
1241
1128
|
}
|
|
1242
1129
|
function resolveEffectiveModeFromPolicy(params) {
|
|
@@ -1396,7 +1283,7 @@ async function sync(options = {}) {
|
|
|
1396
1283
|
try {
|
|
1397
1284
|
spinner4.start("Detecting branch");
|
|
1398
1285
|
const branch = detectBranch(options.branch);
|
|
1399
|
-
spinner4.stop(`Branch: ${
|
|
1286
|
+
spinner4.stop(`Branch: ${chalk3.cyan(branch)}`);
|
|
1400
1287
|
spinner4.start("Loading project configuration");
|
|
1401
1288
|
const mergedConfig = await getMergedConfig(options, options.verbose);
|
|
1402
1289
|
const localConfig = {
|
|
@@ -1422,7 +1309,7 @@ async function sync(options = {}) {
|
|
|
1422
1309
|
spinner4.stop("Project configuration loaded");
|
|
1423
1310
|
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
1424
1311
|
p2.log.warn(
|
|
1425
|
-
`Skipping translations (${
|
|
1312
|
+
`Skipping translations (${chalk3.cyan(branch)} is not a target branch)`
|
|
1426
1313
|
);
|
|
1427
1314
|
p2.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
|
|
1428
1315
|
p2.log.info("Use --force to translate anyway");
|
|
@@ -1444,7 +1331,7 @@ async function sync(options = {}) {
|
|
|
1444
1331
|
return 0;
|
|
1445
1332
|
}
|
|
1446
1333
|
spinner4.stop(
|
|
1447
|
-
`Extracted ${
|
|
1334
|
+
`Extracted ${chalk3.cyan(extractedStrings.length)} strings from ${chalk3.cyan(patternsDisplay)}`
|
|
1448
1335
|
);
|
|
1449
1336
|
if (options.verbose) {
|
|
1450
1337
|
const sampleLines = extractedStrings.slice(0, 5).map((s) => ` "${s.text}" (${s.file}:${s.line})`);
|
|
@@ -1493,7 +1380,7 @@ async function sync(options = {}) {
|
|
|
1493
1380
|
},
|
|
1494
1381
|
repoIdentity ?? void 0
|
|
1495
1382
|
);
|
|
1496
|
-
spinner4.stop(`Submitted to API - Batch ${
|
|
1383
|
+
spinner4.stop(`Submitted to API - Batch ${chalk3.cyan(batchResponse.batchId)}`);
|
|
1497
1384
|
const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
|
|
1498
1385
|
branch,
|
|
1499
1386
|
requestedMode,
|
|
@@ -1510,13 +1397,13 @@ async function sync(options = {}) {
|
|
|
1510
1397
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
1511
1398
|
p2.log.success("No changes detected - strings are up to date");
|
|
1512
1399
|
}
|
|
1513
|
-
p2.log.info(`New strings: ${
|
|
1400
|
+
p2.log.info(`New strings: ${chalk3.cyan(batchResponse.newStrings)}`);
|
|
1514
1401
|
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
1515
1402
|
p2.log.info(
|
|
1516
|
-
`Deleted strings: ${
|
|
1403
|
+
`Deleted strings: ${chalk3.yellow(batchResponse.deletedStrings)} (archived)`
|
|
1517
1404
|
);
|
|
1518
1405
|
}
|
|
1519
|
-
p2.log.info(`Total strings: ${
|
|
1406
|
+
p2.log.info(`Total strings: ${chalk3.cyan(batchResponse.totalStrings)}`);
|
|
1520
1407
|
if (batchResponse.newStrings === 0) {
|
|
1521
1408
|
p2.log.success("No new strings - using existing translations");
|
|
1522
1409
|
} else {
|
|
@@ -1670,9 +1557,12 @@ async function sync(options = {}) {
|
|
|
1670
1557
|
if (error instanceof Error) {
|
|
1671
1558
|
p2.log.error(error.message);
|
|
1672
1559
|
if (error.message.includes("VOCODER_API_KEY")) {
|
|
1673
|
-
p2.log.warn("
|
|
1674
|
-
p2.log.info(
|
|
1675
|
-
p2.log.info("
|
|
1560
|
+
p2.log.warn("VOCODER_API_KEY is only needed for `vocoder sync` (CLI push).");
|
|
1561
|
+
p2.log.info(" Create one at: https://vocoder.app/dashboard");
|
|
1562
|
+
p2.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
|
|
1563
|
+
p2.log.info("");
|
|
1564
|
+
p2.log.info(" Note: If you use @vocoder/unplugin, `vocoder sync` is optional.");
|
|
1565
|
+
p2.log.info(" Translations are fetched automatically at build time.");
|
|
1676
1566
|
} else if (error.message.includes("git branch")) {
|
|
1677
1567
|
p2.log.warn("Run from a git repository, or use:");
|
|
1678
1568
|
p2.log.info(" vocoder sync --branch main");
|
|
@@ -1686,13 +1576,13 @@ async function sync(options = {}) {
|
|
|
1686
1576
|
}
|
|
1687
1577
|
|
|
1688
1578
|
// src/commands/wrap.ts
|
|
1689
|
-
import { readFileSync as
|
|
1579
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1690
1580
|
import { relative as relative2 } from "path";
|
|
1691
1581
|
import * as p3 from "@clack/prompts";
|
|
1692
|
-
import
|
|
1582
|
+
import chalk4 from "chalk";
|
|
1693
1583
|
|
|
1694
1584
|
// src/utils/wrap/analyzer.ts
|
|
1695
|
-
import { readFileSync as
|
|
1585
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1696
1586
|
import { parse as parse2 } from "@babel/parser";
|
|
1697
1587
|
import babelTraverse2 from "@babel/traverse";
|
|
1698
1588
|
import { glob as glob2 } from "glob";
|
|
@@ -2045,7 +1935,7 @@ var StringAnalyzer = class {
|
|
|
2045
1935
|
* Analyze a single file and return wrap candidates.
|
|
2046
1936
|
*/
|
|
2047
1937
|
analyzeFile(filePath) {
|
|
2048
|
-
const code =
|
|
1938
|
+
const code = readFileSync3(filePath, "utf-8");
|
|
2049
1939
|
return this.analyzeCode(code, filePath);
|
|
2050
1940
|
}
|
|
2051
1941
|
/**
|
|
@@ -2672,21 +2562,21 @@ async function wrap(options = {}) {
|
|
|
2672
2562
|
return 0;
|
|
2673
2563
|
}
|
|
2674
2564
|
spinner4.stop(
|
|
2675
|
-
`Found ${
|
|
2565
|
+
`Found ${chalk4.cyan(allCandidates.length)} candidate strings`
|
|
2676
2566
|
);
|
|
2677
2567
|
const filtered = allCandidates.filter(
|
|
2678
2568
|
(c) => meetsConfidenceThreshold(c.confidence, confidenceThreshold)
|
|
2679
2569
|
);
|
|
2680
2570
|
if (filtered.length === 0) {
|
|
2681
2571
|
p3.log.warn(
|
|
2682
|
-
`No strings meet the ${
|
|
2572
|
+
`No strings meet the ${chalk4.bold(confidenceThreshold)} confidence threshold.`
|
|
2683
2573
|
);
|
|
2684
2574
|
p3.log.info("Try --confidence medium or --confidence low to see more candidates.");
|
|
2685
2575
|
p3.outro("");
|
|
2686
2576
|
return 0;
|
|
2687
2577
|
}
|
|
2688
2578
|
p3.log.info(
|
|
2689
|
-
`${filtered.length} strings meet ${
|
|
2579
|
+
`${filtered.length} strings meet ${chalk4.bold(confidenceThreshold)} confidence threshold`
|
|
2690
2580
|
);
|
|
2691
2581
|
const byFile = /* @__PURE__ */ new Map();
|
|
2692
2582
|
for (const c of filtered) {
|
|
@@ -2698,15 +2588,15 @@ async function wrap(options = {}) {
|
|
|
2698
2588
|
const lines = [];
|
|
2699
2589
|
for (const [file, candidates] of byFile) {
|
|
2700
2590
|
const relPath = relative2(projectRoot, file);
|
|
2701
|
-
lines.push(
|
|
2591
|
+
lines.push(chalk4.bold(relPath));
|
|
2702
2592
|
for (const c of candidates) {
|
|
2703
|
-
const confidenceColor = c.confidence === "high" ?
|
|
2593
|
+
const confidenceColor = c.confidence === "high" ? chalk4.green : c.confidence === "medium" ? chalk4.yellow : chalk4.red;
|
|
2704
2594
|
const strategyLabel = c.strategy === "T-component" ? "<T>" : "t()";
|
|
2705
2595
|
lines.push(
|
|
2706
|
-
` ${
|
|
2596
|
+
` ${chalk4.dim(`L${c.line}`)} ${confidenceColor(`[${c.confidence}]`)} ${chalk4.cyan(strategyLabel)} "${truncate(c.text, 50)}"`
|
|
2707
2597
|
);
|
|
2708
2598
|
if (options.verbose) {
|
|
2709
|
-
lines.push(
|
|
2599
|
+
lines.push(chalk4.dim(` ${c.reason}`));
|
|
2710
2600
|
}
|
|
2711
2601
|
}
|
|
2712
2602
|
lines.push("");
|
|
@@ -2738,10 +2628,10 @@ async function wrap(options = {}) {
|
|
|
2738
2628
|
acceptedByFile.set(c.file, existing);
|
|
2739
2629
|
}
|
|
2740
2630
|
for (const [file, candidates] of acceptedByFile) {
|
|
2741
|
-
const code =
|
|
2631
|
+
const code = readFileSync4(file, "utf-8");
|
|
2742
2632
|
const result = transformer.transform(code, candidates, file);
|
|
2743
2633
|
if (result.wrappedCount > 0) {
|
|
2744
|
-
|
|
2634
|
+
writeFileSync2(file, result.output, "utf-8");
|
|
2745
2635
|
totalWrapped += result.wrappedCount;
|
|
2746
2636
|
filesModified++;
|
|
2747
2637
|
}
|
|
@@ -2751,7 +2641,7 @@ async function wrap(options = {}) {
|
|
|
2751
2641
|
}
|
|
2752
2642
|
}
|
|
2753
2643
|
spinner4.stop(
|
|
2754
|
-
`Wrapped ${
|
|
2644
|
+
`Wrapped ${chalk4.cyan(totalWrapped)} strings across ${chalk4.cyan(filesModified)} files`
|
|
2755
2645
|
);
|
|
2756
2646
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2757
2647
|
p3.outro(`Done! (${duration}s)`);
|
|
@@ -2776,7 +2666,7 @@ async function interactiveConfirm(byFile, projectRoot) {
|
|
|
2776
2666
|
p3.log.info("Interactive mode \u2014 confirm each string:");
|
|
2777
2667
|
for (const [file, candidates] of byFile) {
|
|
2778
2668
|
const relPath = relative2(projectRoot, file);
|
|
2779
|
-
p3.log.step(
|
|
2669
|
+
p3.log.step(chalk4.bold(relPath));
|
|
2780
2670
|
let skipFile = false;
|
|
2781
2671
|
for (const c of candidates) {
|
|
2782
2672
|
if (skipFile) break;
|
|
@@ -2832,9 +2722,9 @@ function summarizeCandidates(candidates) {
|
|
|
2832
2722
|
else tFunction++;
|
|
2833
2723
|
}
|
|
2834
2724
|
const parts = [];
|
|
2835
|
-
if (high > 0) parts.push(
|
|
2836
|
-
if (medium > 0) parts.push(
|
|
2837
|
-
if (low > 0) parts.push(
|
|
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`));
|
|
2838
2728
|
return `${candidates.length} total (${parts.join(", ")}) | ${tComponent} <T>, ${tFunction} t()`;
|
|
2839
2729
|
}
|
|
2840
2730
|
|