@vocoder/cli 0.1.1 → 0.1.3
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 +69 -247
- package/dist/bin.mjs +2851 -5
- package/dist/bin.mjs.map +1 -1
- package/package.json +6 -9
- package/dist/chunk-N45Q4R6O.mjs +0 -635
- package/dist/chunk-N45Q4R6O.mjs.map +0 -1
- package/dist/index.d.mts +0 -97
- package/dist/index.mjs +0 -13
- package/dist/index.mjs.map +0 -1
package/dist/chunk-N45Q4R6O.mjs
DELETED
|
@@ -1,635 +0,0 @@
|
|
|
1
|
-
// src/utils/branch.ts
|
|
2
|
-
import { execSync } from "child_process";
|
|
3
|
-
function detectBranch(override) {
|
|
4
|
-
if (override) {
|
|
5
|
-
return override;
|
|
6
|
-
}
|
|
7
|
-
const envBranch = process.env.GITHUB_REF_NAME || // GitHub Actions
|
|
8
|
-
process.env.VERCEL_GIT_COMMIT_REF || // Vercel
|
|
9
|
-
process.env.BRANCH || // Netlify, generic
|
|
10
|
-
process.env.CI_COMMIT_REF_NAME || // GitLab
|
|
11
|
-
process.env.BITBUCKET_BRANCH || // Bitbucket
|
|
12
|
-
process.env.CIRCLE_BRANCH;
|
|
13
|
-
if (envBranch) {
|
|
14
|
-
return envBranch;
|
|
15
|
-
}
|
|
16
|
-
try {
|
|
17
|
-
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
18
|
-
encoding: "utf-8",
|
|
19
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
20
|
-
}).trim();
|
|
21
|
-
return branch;
|
|
22
|
-
} catch (error) {
|
|
23
|
-
throw new Error(
|
|
24
|
-
"Failed to detect git branch. Make sure you are in a git repository or set the --branch flag."
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function isTargetBranch(currentBranch, targetBranches) {
|
|
29
|
-
return targetBranches.includes(currentBranch);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// src/utils/config.ts
|
|
33
|
-
import { config as loadEnv } from "dotenv";
|
|
34
|
-
loadEnv();
|
|
35
|
-
function getLocalConfig() {
|
|
36
|
-
const apiKey = process.env.VOCODER_API_KEY;
|
|
37
|
-
if (!apiKey) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
'VOCODER_API_KEY is required. Set it in your .env file or environment:\n export VOCODER_API_KEY="your-api-key"\n\nGet your API key from: https://vocoder.app/settings/api-keys'
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
apiKey,
|
|
44
|
-
apiUrl: process.env.VOCODER_API_URL || "https://vocoder.app"
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function validateLocalConfig(config) {
|
|
48
|
-
if (!config.apiKey || config.apiKey.length === 0) {
|
|
49
|
-
throw new Error("Invalid API key");
|
|
50
|
-
}
|
|
51
|
-
if (!config.apiKey.startsWith("vc_")) {
|
|
52
|
-
throw new Error("Invalid API key format. Expected format: vc_...");
|
|
53
|
-
}
|
|
54
|
-
if (!config.apiUrl || !config.apiUrl.startsWith("http")) {
|
|
55
|
-
throw new Error("Invalid API URL");
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/commands/sync.ts
|
|
60
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
61
|
-
import { join, dirname } from "path";
|
|
62
|
-
import chalk from "chalk";
|
|
63
|
-
import ora from "ora";
|
|
64
|
-
|
|
65
|
-
// src/utils/api.ts
|
|
66
|
-
var VocoderAPI = class {
|
|
67
|
-
constructor(config) {
|
|
68
|
-
this.apiUrl = config.apiUrl;
|
|
69
|
-
this.apiKey = config.apiKey;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Fetch project configuration from API
|
|
73
|
-
* Project is determined from the API key
|
|
74
|
-
*/
|
|
75
|
-
async getProjectConfig() {
|
|
76
|
-
const response = await fetch(
|
|
77
|
-
`${this.apiUrl}/api/cli/config`,
|
|
78
|
-
{
|
|
79
|
-
headers: {
|
|
80
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
);
|
|
84
|
-
if (!response.ok) {
|
|
85
|
-
const error = await response.text();
|
|
86
|
-
throw new Error(`Failed to fetch project config: ${error}`);
|
|
87
|
-
}
|
|
88
|
-
const data = await response.json();
|
|
89
|
-
return {
|
|
90
|
-
sourceLocale: data.sourceLocale,
|
|
91
|
-
targetLocales: data.targetLocales,
|
|
92
|
-
targetBranches: data.targetBranches
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Submit strings for translation
|
|
97
|
-
* Project is determined from the API key
|
|
98
|
-
*/
|
|
99
|
-
async submitTranslation(branch, strings, targetLocales) {
|
|
100
|
-
const crypto = await import("crypto");
|
|
101
|
-
const sortedStrings = [...strings].sort();
|
|
102
|
-
const stringsHash = crypto.createHash("sha256").update(JSON.stringify(sortedStrings)).digest("hex");
|
|
103
|
-
const response = await fetch(`${this.apiUrl}/api/cli/sync`, {
|
|
104
|
-
method: "POST",
|
|
105
|
-
headers: {
|
|
106
|
-
"Content-Type": "application/json",
|
|
107
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
108
|
-
},
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
branch,
|
|
111
|
-
strings,
|
|
112
|
-
targetLocales,
|
|
113
|
-
stringsHash
|
|
114
|
-
})
|
|
115
|
-
});
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
const error = await response.text();
|
|
118
|
-
throw new Error(`Translation submission failed: ${error}`);
|
|
119
|
-
}
|
|
120
|
-
return response.json();
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Check translation status
|
|
124
|
-
*/
|
|
125
|
-
async getTranslationStatus(batchId) {
|
|
126
|
-
const response = await fetch(
|
|
127
|
-
`${this.apiUrl}/api/cli/sync/status/${batchId}`,
|
|
128
|
-
{
|
|
129
|
-
headers: {
|
|
130
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
);
|
|
134
|
-
if (!response.ok) {
|
|
135
|
-
const error = await response.text();
|
|
136
|
-
throw new Error(`Failed to check translation status: ${error}`);
|
|
137
|
-
}
|
|
138
|
-
return response.json();
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Wait for translation to complete with polling
|
|
142
|
-
*/
|
|
143
|
-
async waitForCompletion(batchId, timeout = 6e4, onProgress) {
|
|
144
|
-
const startTime = Date.now();
|
|
145
|
-
const pollInterval = 1e3;
|
|
146
|
-
while (Date.now() - startTime < timeout) {
|
|
147
|
-
const status = await this.getTranslationStatus(batchId);
|
|
148
|
-
if (onProgress) {
|
|
149
|
-
onProgress(status.progress);
|
|
150
|
-
}
|
|
151
|
-
if (status.status === "COMPLETED") {
|
|
152
|
-
if (!status.translations) {
|
|
153
|
-
throw new Error("Translation completed but no translations returned");
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
translations: status.translations,
|
|
157
|
-
localeMetadata: status.localeMetadata
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
if (status.status === "FAILED") {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`Translation failed: ${status.errorMessage || "Unknown error"}`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
166
|
-
}
|
|
167
|
-
throw new Error(`Translation timeout after ${timeout}ms`);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
// src/utils/extract.ts
|
|
172
|
-
import { readFileSync } from "fs";
|
|
173
|
-
import { parse } from "@babel/parser";
|
|
174
|
-
import babelTraverse from "@babel/traverse";
|
|
175
|
-
import { glob } from "glob";
|
|
176
|
-
var traverse = babelTraverse.default || babelTraverse;
|
|
177
|
-
var StringExtractor = class {
|
|
178
|
-
/**
|
|
179
|
-
* Extract strings from all files matching the pattern
|
|
180
|
-
*/
|
|
181
|
-
async extractFromProject(pattern, projectRoot = process.cwd()) {
|
|
182
|
-
const files = await glob(pattern, {
|
|
183
|
-
cwd: projectRoot,
|
|
184
|
-
absolute: true,
|
|
185
|
-
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"]
|
|
186
|
-
});
|
|
187
|
-
const allStrings = [];
|
|
188
|
-
for (const file of files) {
|
|
189
|
-
try {
|
|
190
|
-
const strings = await this.extractFromFile(file);
|
|
191
|
-
allStrings.push(...strings);
|
|
192
|
-
} catch (error) {
|
|
193
|
-
console.warn(`Warning: Failed to extract from ${file}:`, error);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const unique = this.deduplicateStrings(allStrings);
|
|
197
|
-
return unique;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Extract strings from a single file
|
|
201
|
-
*/
|
|
202
|
-
async extractFromFile(filePath) {
|
|
203
|
-
const code = readFileSync(filePath, "utf-8");
|
|
204
|
-
const strings = [];
|
|
205
|
-
try {
|
|
206
|
-
const ast = parse(code, {
|
|
207
|
-
sourceType: "module",
|
|
208
|
-
plugins: ["jsx", "typescript"]
|
|
209
|
-
});
|
|
210
|
-
const vocoderImports = /* @__PURE__ */ new Map();
|
|
211
|
-
const tFunctionNames = /* @__PURE__ */ new Set();
|
|
212
|
-
traverse(ast, {
|
|
213
|
-
// Track imports of <T> component and t function
|
|
214
|
-
ImportDeclaration: (path) => {
|
|
215
|
-
const source = path.node.source.value;
|
|
216
|
-
if (source === "@vocoder/react") {
|
|
217
|
-
path.node.specifiers.forEach((spec) => {
|
|
218
|
-
if (spec.type === "ImportSpecifier") {
|
|
219
|
-
const imported = spec.imported.type === "Identifier" ? spec.imported.name : null;
|
|
220
|
-
const local = spec.local.name;
|
|
221
|
-
if (imported === "T") {
|
|
222
|
-
vocoderImports.set(local, "T");
|
|
223
|
-
}
|
|
224
|
-
if (imported === "t") {
|
|
225
|
-
tFunctionNames.add(local);
|
|
226
|
-
}
|
|
227
|
-
if (imported === "useVocoder") {
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
// Track destructured 't' from useVocoder hook
|
|
234
|
-
VariableDeclarator: (path) => {
|
|
235
|
-
const init = path.node.init;
|
|
236
|
-
if (init && init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "useVocoder" && path.node.id.type === "ObjectPattern") {
|
|
237
|
-
path.node.id.properties.forEach((prop) => {
|
|
238
|
-
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === "t") {
|
|
239
|
-
const localName = prop.value.type === "Identifier" ? prop.value.name : "t";
|
|
240
|
-
tFunctionNames.add(localName);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
// Extract from t() function calls
|
|
246
|
-
CallExpression: (path) => {
|
|
247
|
-
const callee = path.node.callee;
|
|
248
|
-
const isTFunction = callee.type === "Identifier" && tFunctionNames.has(callee.name);
|
|
249
|
-
if (!isTFunction) return;
|
|
250
|
-
const firstArg = path.node.arguments[0];
|
|
251
|
-
if (!firstArg) return;
|
|
252
|
-
let text = null;
|
|
253
|
-
if (firstArg.type === "StringLiteral") {
|
|
254
|
-
text = firstArg.value;
|
|
255
|
-
} else if (firstArg.type === "TemplateLiteral") {
|
|
256
|
-
text = this.extractTemplateText(firstArg);
|
|
257
|
-
}
|
|
258
|
-
if (!text || text.trim().length === 0) return;
|
|
259
|
-
const secondArg = path.node.arguments[1];
|
|
260
|
-
let context;
|
|
261
|
-
let formality;
|
|
262
|
-
if (secondArg && secondArg.type === "ObjectExpression") {
|
|
263
|
-
secondArg.properties.forEach((prop) => {
|
|
264
|
-
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier") {
|
|
265
|
-
if (prop.key.name === "context" && prop.value.type === "StringLiteral") {
|
|
266
|
-
context = prop.value.value;
|
|
267
|
-
}
|
|
268
|
-
if (prop.key.name === "formality" && prop.value.type === "StringLiteral") {
|
|
269
|
-
formality = prop.value.value;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
strings.push({
|
|
275
|
-
text: text.trim(),
|
|
276
|
-
file: filePath,
|
|
277
|
-
line: path.node.loc?.start.line || 0,
|
|
278
|
-
context,
|
|
279
|
-
formality
|
|
280
|
-
});
|
|
281
|
-
},
|
|
282
|
-
// Extract from JSX elements
|
|
283
|
-
JSXElement: (path) => {
|
|
284
|
-
const opening = path.node.openingElement;
|
|
285
|
-
const tagName = opening.name.type === "JSXIdentifier" ? opening.name.name : null;
|
|
286
|
-
if (!tagName) return;
|
|
287
|
-
const isTranslationComponent = vocoderImports.has(tagName);
|
|
288
|
-
if (!isTranslationComponent) return;
|
|
289
|
-
const text = this.extractTextContent(path.node.children);
|
|
290
|
-
if (!text || text.trim().length === 0) return;
|
|
291
|
-
const context = this.getStringAttribute(opening.attributes, "context");
|
|
292
|
-
const formality = this.getStringAttribute(
|
|
293
|
-
opening.attributes,
|
|
294
|
-
"formality"
|
|
295
|
-
);
|
|
296
|
-
strings.push({
|
|
297
|
-
text: text.trim(),
|
|
298
|
-
file: filePath,
|
|
299
|
-
line: path.node.loc?.start.line || 0,
|
|
300
|
-
context,
|
|
301
|
-
formality
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
} catch (error) {
|
|
306
|
-
throw new Error(
|
|
307
|
-
`Failed to parse ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
return strings;
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Extract text from template literal
|
|
314
|
-
* Converts template literals like `Hello ${name}` to `Hello {name}`
|
|
315
|
-
*/
|
|
316
|
-
extractTemplateText(node) {
|
|
317
|
-
let text = "";
|
|
318
|
-
for (let i = 0; i < node.quasis.length; i++) {
|
|
319
|
-
const quasi = node.quasis[i];
|
|
320
|
-
text += quasi.value.raw;
|
|
321
|
-
if (i < node.expressions.length) {
|
|
322
|
-
const expr = node.expressions[i];
|
|
323
|
-
if (expr.type === "Identifier") {
|
|
324
|
-
text += `{${expr.name}}`;
|
|
325
|
-
} else {
|
|
326
|
-
text += "{value}";
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return text;
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Extract text content from JSX children
|
|
334
|
-
*/
|
|
335
|
-
extractTextContent(children) {
|
|
336
|
-
let text = "";
|
|
337
|
-
for (const child of children) {
|
|
338
|
-
if (child.type === "JSXText") {
|
|
339
|
-
text += child.value;
|
|
340
|
-
} else if (child.type === "JSXExpressionContainer") {
|
|
341
|
-
const expr = child.expression;
|
|
342
|
-
if (expr.type === "Identifier") {
|
|
343
|
-
text += `{${expr.name}}`;
|
|
344
|
-
} else if (expr.type === "StringLiteral") {
|
|
345
|
-
text += expr.value;
|
|
346
|
-
} else if (expr.type === "TemplateLiteral") {
|
|
347
|
-
text += this.extractTemplateText(expr);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return text;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Get string value from JSX attribute
|
|
355
|
-
*/
|
|
356
|
-
getStringAttribute(attributes, name) {
|
|
357
|
-
const attr = attributes.find(
|
|
358
|
-
(a) => a.type === "JSXAttribute" && a.name.name === name
|
|
359
|
-
);
|
|
360
|
-
if (!attr || !attr.value) return void 0;
|
|
361
|
-
if (attr.value.type === "StringLiteral") {
|
|
362
|
-
return attr.value.value;
|
|
363
|
-
}
|
|
364
|
-
return void 0;
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Deduplicate strings (keep first occurrence)
|
|
368
|
-
*/
|
|
369
|
-
deduplicateStrings(strings) {
|
|
370
|
-
const seen = /* @__PURE__ */ new Set();
|
|
371
|
-
const unique = [];
|
|
372
|
-
for (const str of strings) {
|
|
373
|
-
const key = `${str.text}|${str.context || ""}|${str.formality || ""}`;
|
|
374
|
-
if (!seen.has(key)) {
|
|
375
|
-
seen.add(key);
|
|
376
|
-
unique.push(str);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return unique;
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
// src/commands/sync.ts
|
|
384
|
-
function generateIndexFile(locales, translations, localeMetadata) {
|
|
385
|
-
const toIdentifier = (locale) => locale.replace(/-/g, "_");
|
|
386
|
-
const imports = locales.map(
|
|
387
|
-
(locale) => `import ${toIdentifier(locale)} from './${locale}.json';`
|
|
388
|
-
).join("\n");
|
|
389
|
-
const translationsObj = locales.map(
|
|
390
|
-
(locale) => ` '${locale}': ${toIdentifier(locale)},`
|
|
391
|
-
).join("\n");
|
|
392
|
-
const localesObjEntries = locales.map((locale) => {
|
|
393
|
-
const metadata = localeMetadata?.[locale];
|
|
394
|
-
if (metadata) {
|
|
395
|
-
const escapedNativeName = metadata.nativeName.replace(/'/g, "\\'");
|
|
396
|
-
const dirProp = metadata.dir ? `, dir: '${metadata.dir}' as const` : "";
|
|
397
|
-
return ` '${locale}': { nativeName: '${escapedNativeName}'${dirProp} }`;
|
|
398
|
-
} else {
|
|
399
|
-
return ` '${locale}': { nativeName: '${locale}' }`;
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
const localesObjString = localesObjEntries.join(",\n");
|
|
403
|
-
return `// Auto-generated by Vocoder CLI
|
|
404
|
-
// This file imports all locale JSON files and exports them as a single object
|
|
405
|
-
// Usage: import { translations, locales } from './.vocoder/locales';
|
|
406
|
-
|
|
407
|
-
${imports}
|
|
408
|
-
|
|
409
|
-
export const translations = {
|
|
410
|
-
${translationsObj}
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Flat locale metadata map (O(N))
|
|
415
|
-
* Structure: locales[localeCode] = { nativeName, dir? }
|
|
416
|
-
* - nativeName: Name in the locale's own language (e.g., "Espa\xF1ol", "\u7B80\u4F53\u4E2D\u6587")
|
|
417
|
-
* - dir: Optional 'rtl' for right-to-left locales
|
|
418
|
-
*
|
|
419
|
-
* Translated names are generated at runtime using Intl.DisplayNames:
|
|
420
|
-
* Example: new Intl.DisplayNames(['es'], { type: 'language' }).of('en') \u2192 "ingl\xE9s"
|
|
421
|
-
* Display format: \`\${getDisplayName(code)} (\${locales[code].nativeName})\` \u2192 "ingl\xE9s (English)"
|
|
422
|
-
*/
|
|
423
|
-
export const locales = {
|
|
424
|
-
${localesObjString}
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
export type SupportedLocale = ${locales.map((l) => `'${l}'`).join(" | ")};
|
|
428
|
-
`;
|
|
429
|
-
}
|
|
430
|
-
async function sync(options = {}) {
|
|
431
|
-
const startTime = Date.now();
|
|
432
|
-
const projectRoot = process.cwd();
|
|
433
|
-
try {
|
|
434
|
-
const spinner = ora("Detecting branch...").start();
|
|
435
|
-
const branch = detectBranch(options.branch);
|
|
436
|
-
spinner.succeed(`Detected branch: ${chalk.cyan(branch)}`);
|
|
437
|
-
spinner.start("Loading project configuration...");
|
|
438
|
-
const localConfig = getLocalConfig();
|
|
439
|
-
validateLocalConfig(localConfig);
|
|
440
|
-
const api = new VocoderAPI(localConfig);
|
|
441
|
-
const apiConfig = await api.getProjectConfig();
|
|
442
|
-
const config = {
|
|
443
|
-
...localConfig,
|
|
444
|
-
...apiConfig,
|
|
445
|
-
extractionPattern: process.env.VOCODER_EXTRACTION_PATTERN || "src/**/*.{tsx,jsx,ts,js}",
|
|
446
|
-
outputDir: ".vocoder/locales",
|
|
447
|
-
timeout: 6e4
|
|
448
|
-
};
|
|
449
|
-
spinner.succeed("Project configuration loaded");
|
|
450
|
-
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
451
|
-
console.log(
|
|
452
|
-
chalk.yellow(
|
|
453
|
-
`\u2139\uFE0F Skipping translations (${branch} is not a target branch)`
|
|
454
|
-
)
|
|
455
|
-
);
|
|
456
|
-
console.log(
|
|
457
|
-
chalk.dim(
|
|
458
|
-
` Target branches: ${config.targetBranches.join(", ")}`
|
|
459
|
-
)
|
|
460
|
-
);
|
|
461
|
-
console.log(chalk.dim(` Use --force to translate anyway`));
|
|
462
|
-
process.exit(0);
|
|
463
|
-
}
|
|
464
|
-
spinner.start(`Extracting strings from ${config.extractionPattern}...`);
|
|
465
|
-
const extractor = new StringExtractor();
|
|
466
|
-
const extractedStrings = await extractor.extractFromProject(
|
|
467
|
-
config.extractionPattern,
|
|
468
|
-
projectRoot
|
|
469
|
-
);
|
|
470
|
-
if (extractedStrings.length === 0) {
|
|
471
|
-
spinner.warn("No translatable strings found");
|
|
472
|
-
console.log(chalk.yellow("Make sure you are using <T> components from @vocoder/react"));
|
|
473
|
-
process.exit(0);
|
|
474
|
-
}
|
|
475
|
-
spinner.succeed(
|
|
476
|
-
`Extracted ${chalk.cyan(extractedStrings.length)} strings from ${chalk.cyan(config.extractionPattern)}`
|
|
477
|
-
);
|
|
478
|
-
if (options.verbose) {
|
|
479
|
-
console.log(chalk.dim("\nSample strings:"));
|
|
480
|
-
extractedStrings.slice(0, 5).forEach((s) => {
|
|
481
|
-
console.log(chalk.dim(` - "${s.text}" (${s.file}:${s.line})`));
|
|
482
|
-
});
|
|
483
|
-
if (extractedStrings.length > 5) {
|
|
484
|
-
console.log(chalk.dim(` ... and ${extractedStrings.length - 5} more`));
|
|
485
|
-
}
|
|
486
|
-
console.log();
|
|
487
|
-
}
|
|
488
|
-
if (options.dryRun) {
|
|
489
|
-
console.log(chalk.cyan("\n\u{1F4CB} Dry run mode - would translate:"));
|
|
490
|
-
console.log(chalk.dim(` Strings: ${extractedStrings.length}`));
|
|
491
|
-
console.log(chalk.dim(` Branch: ${branch}`));
|
|
492
|
-
console.log(chalk.dim(` Target locales: ${config.targetLocales.join(", ")}`));
|
|
493
|
-
console.log(chalk.dim(`
|
|
494
|
-
No API calls made.`));
|
|
495
|
-
process.exit(0);
|
|
496
|
-
}
|
|
497
|
-
spinner.start("Submitting strings to Vocoder API...");
|
|
498
|
-
const strings = extractedStrings.map((s) => s.text);
|
|
499
|
-
const batchResponse = await api.submitTranslation(
|
|
500
|
-
branch,
|
|
501
|
-
strings,
|
|
502
|
-
config.targetLocales
|
|
503
|
-
);
|
|
504
|
-
spinner.succeed(
|
|
505
|
-
`Submitted to API - Batch ID: ${chalk.cyan(batchResponse.batchId)}`
|
|
506
|
-
);
|
|
507
|
-
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
508
|
-
console.log(chalk.green("\n\u2714 No changes detected - strings are up to date"));
|
|
509
|
-
console.log(chalk.dim(" (Files will be written for build environment)\n"));
|
|
510
|
-
}
|
|
511
|
-
console.log(
|
|
512
|
-
chalk.dim(
|
|
513
|
-
` New strings: ${chalk.cyan(batchResponse.newStrings)}`
|
|
514
|
-
)
|
|
515
|
-
);
|
|
516
|
-
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
517
|
-
console.log(
|
|
518
|
-
chalk.dim(
|
|
519
|
-
` Deleted strings: ${chalk.yellow(batchResponse.deletedStrings)} (archived)`
|
|
520
|
-
)
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
console.log(
|
|
524
|
-
chalk.dim(
|
|
525
|
-
` Total strings: ${chalk.cyan(batchResponse.totalStrings)}`
|
|
526
|
-
)
|
|
527
|
-
);
|
|
528
|
-
if (batchResponse.newStrings === 0) {
|
|
529
|
-
console.log(
|
|
530
|
-
chalk.green("\n\u2705 No new strings - using existing translations")
|
|
531
|
-
);
|
|
532
|
-
} else {
|
|
533
|
-
console.log(
|
|
534
|
-
chalk.cyan(
|
|
535
|
-
`
|
|
536
|
-
\u23F3 Syncing to ${config.targetLocales.length} locales (${config.targetLocales.join(", ")})`
|
|
537
|
-
)
|
|
538
|
-
);
|
|
539
|
-
if (batchResponse.estimatedTime) {
|
|
540
|
-
console.log(
|
|
541
|
-
chalk.dim(
|
|
542
|
-
` Estimated time: ~${batchResponse.estimatedTime} seconds`
|
|
543
|
-
)
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
spinner.start("Waiting for translations to complete...");
|
|
548
|
-
let lastProgress = 0;
|
|
549
|
-
const result = await api.waitForCompletion(
|
|
550
|
-
batchResponse.batchId,
|
|
551
|
-
config.timeout,
|
|
552
|
-
(progress) => {
|
|
553
|
-
const percent = Math.round(progress * 100);
|
|
554
|
-
if (percent > lastProgress) {
|
|
555
|
-
spinner.text = `Syncing... ${percent}% complete`;
|
|
556
|
-
lastProgress = percent;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
);
|
|
560
|
-
const { translations, localeMetadata: apiLocaleMetadata } = result;
|
|
561
|
-
spinner.succeed("Translations complete!");
|
|
562
|
-
spinner.start(`Writing locale files to ${config.outputDir}...`);
|
|
563
|
-
const outputPath = join(projectRoot, config.outputDir);
|
|
564
|
-
mkdirSync(outputPath, { recursive: true });
|
|
565
|
-
let filesWritten = 0;
|
|
566
|
-
const localeNames = [];
|
|
567
|
-
for (const [locale, strings2] of Object.entries(translations)) {
|
|
568
|
-
const filePath = join(outputPath, `${locale}.json`);
|
|
569
|
-
const content = JSON.stringify(strings2, null, 2);
|
|
570
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
571
|
-
writeFileSync(filePath, content, "utf-8");
|
|
572
|
-
filesWritten++;
|
|
573
|
-
localeNames.push(locale);
|
|
574
|
-
const sizeKB = (content.length / 1024).toFixed(1);
|
|
575
|
-
console.log(
|
|
576
|
-
chalk.dim(` \u2713 Wrote ${locale}.json (${sizeKB}KB)`)
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
const indexContent = generateIndexFile(localeNames, translations, apiLocaleMetadata);
|
|
580
|
-
const indexPath = join(outputPath, "index.ts");
|
|
581
|
-
writeFileSync(indexPath, indexContent, "utf-8");
|
|
582
|
-
console.log(chalk.dim(` \u2713 Generated index.ts (with flat locales map)`));
|
|
583
|
-
spinner.succeed(`Wrote ${chalk.cyan(filesWritten)} locale files`);
|
|
584
|
-
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
585
|
-
console.log(
|
|
586
|
-
chalk.green(`
|
|
587
|
-
\u2705 Sync complete! (${duration}s)
|
|
588
|
-
`)
|
|
589
|
-
);
|
|
590
|
-
console.log(chalk.dim("Next steps:"));
|
|
591
|
-
console.log(
|
|
592
|
-
chalk.dim(
|
|
593
|
-
` 1. Import translations: import { translations } from '${config.outputDir}'`
|
|
594
|
-
)
|
|
595
|
-
);
|
|
596
|
-
console.log(
|
|
597
|
-
chalk.dim(
|
|
598
|
-
` 2. Use VocoderProvider: <VocoderProvider translations={translations} defaultLocale="en">`
|
|
599
|
-
)
|
|
600
|
-
);
|
|
601
|
-
console.log(
|
|
602
|
-
chalk.dim(
|
|
603
|
-
` 3. Commit ${config.outputDir}/ to your repository`
|
|
604
|
-
)
|
|
605
|
-
);
|
|
606
|
-
} catch (error) {
|
|
607
|
-
if (error instanceof Error) {
|
|
608
|
-
console.error(chalk.red(`
|
|
609
|
-
\u274C Error: ${error.message}
|
|
610
|
-
`));
|
|
611
|
-
if (error.message.includes("VOCODER_API_KEY")) {
|
|
612
|
-
console.log(chalk.yellow("\u{1F4A1} Solution:"));
|
|
613
|
-
console.log(chalk.dim(" Set your API key:"));
|
|
614
|
-
console.log(chalk.dim(' export VOCODER_API_KEY="your-api-key"'));
|
|
615
|
-
console.log(chalk.dim(" or add it to your .env file"));
|
|
616
|
-
} else if (error.message.includes("git branch")) {
|
|
617
|
-
console.log(chalk.yellow("\u{1F4A1} Solution:"));
|
|
618
|
-
console.log(chalk.dim(" Run from a git repository, or use:"));
|
|
619
|
-
console.log(chalk.dim(" vocoder translate --branch main"));
|
|
620
|
-
}
|
|
621
|
-
if (options.verbose) {
|
|
622
|
-
console.error(chalk.dim("\nFull error:"), error);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
process.exit(1);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
export {
|
|
630
|
-
detectBranch,
|
|
631
|
-
getLocalConfig,
|
|
632
|
-
validateLocalConfig,
|
|
633
|
-
sync
|
|
634
|
-
};
|
|
635
|
-
//# sourceMappingURL=chunk-N45Q4R6O.mjs.map
|