translate-kit 0.1.0 → 0.3.0
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 +115 -82
- package/dist/cli.js +1905 -730
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -48,12 +48,12 @@ var init_config = __esm({
|
|
|
48
48
|
glossary: z.record(z.string()).optional(),
|
|
49
49
|
tone: z.string().optional(),
|
|
50
50
|
retries: z.number().int().min(0).default(2),
|
|
51
|
-
concurrency: z.number().int().positive().default(3)
|
|
51
|
+
concurrency: z.number().int().positive().default(3),
|
|
52
|
+
validatePlaceholders: z.boolean().default(true).optional()
|
|
52
53
|
}).optional(),
|
|
53
54
|
scan: z.object({
|
|
54
55
|
include: z.array(z.string()),
|
|
55
56
|
exclude: z.array(z.string()).optional(),
|
|
56
|
-
keyStrategy: z.enum(["hash", "path"]).default("hash"),
|
|
57
57
|
translatableProps: z.array(z.string()).default(["placeholder", "title", "alt", "aria-label"]),
|
|
58
58
|
i18nImport: z.string().default("next-intl")
|
|
59
59
|
}).optional(),
|
|
@@ -166,151 +166,6 @@ function computeDiff(sourceFlat, targetFlat, lockData) {
|
|
|
166
166
|
var init_diff = __esm({
|
|
167
167
|
"src/diff.ts"() {
|
|
168
168
|
"use strict";
|
|
169
|
-
init_flatten();
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// src/translate.ts
|
|
174
|
-
import { generateObject } from "ai";
|
|
175
|
-
import { z as z2 } from "zod";
|
|
176
|
-
import pLimit from "p-limit";
|
|
177
|
-
function buildPrompt(entries, sourceLocale, targetLocale, options) {
|
|
178
|
-
const lines = [
|
|
179
|
-
`Translate the following strings from "${sourceLocale}" to "${targetLocale}".`,
|
|
180
|
-
"",
|
|
181
|
-
"Rules:",
|
|
182
|
-
"- Preserve all placeholders like {name}, {{count}}, %s, %d exactly as-is",
|
|
183
|
-
"- Preserve HTML tags and markdown formatting",
|
|
184
|
-
"- Do NOT translate proper nouns, brand names, or technical identifiers",
|
|
185
|
-
"- Maintain the same level of formality and register",
|
|
186
|
-
"- Return natural, fluent translations (not word-for-word)"
|
|
187
|
-
];
|
|
188
|
-
if (options?.tone) {
|
|
189
|
-
lines.push(`- Use a ${options.tone} tone`);
|
|
190
|
-
}
|
|
191
|
-
if (options?.context) {
|
|
192
|
-
lines.push(`
|
|
193
|
-
Context: ${options.context}`);
|
|
194
|
-
}
|
|
195
|
-
if (options?.glossary && Object.keys(options.glossary).length > 0) {
|
|
196
|
-
lines.push("\nGlossary (use these exact translations):");
|
|
197
|
-
for (const [term, translation] of Object.entries(options.glossary)) {
|
|
198
|
-
lines.push(` "${term}" \u2192 "${translation}"`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
lines.push("\nStrings to translate:");
|
|
202
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
203
|
-
lines.push(` "${key}": "${value}"`);
|
|
204
|
-
}
|
|
205
|
-
return lines.join("\n");
|
|
206
|
-
}
|
|
207
|
-
function buildSchema(keys) {
|
|
208
|
-
const shape = {};
|
|
209
|
-
for (const key of keys) {
|
|
210
|
-
shape[key] = z2.string();
|
|
211
|
-
}
|
|
212
|
-
return z2.object(shape);
|
|
213
|
-
}
|
|
214
|
-
async function translateBatchWithRetry(input, retries) {
|
|
215
|
-
const { model, entries, sourceLocale, targetLocale, options } = input;
|
|
216
|
-
const keys = Object.keys(entries);
|
|
217
|
-
const prompt = buildPrompt(entries, sourceLocale, targetLocale, options);
|
|
218
|
-
const schema = buildSchema(keys);
|
|
219
|
-
let lastError;
|
|
220
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
221
|
-
try {
|
|
222
|
-
const { object } = await generateObject({
|
|
223
|
-
model,
|
|
224
|
-
prompt,
|
|
225
|
-
schema
|
|
226
|
-
});
|
|
227
|
-
return object;
|
|
228
|
-
} catch (error) {
|
|
229
|
-
lastError = error;
|
|
230
|
-
if (attempt < retries) {
|
|
231
|
-
const delay = Math.min(Math.pow(2, attempt) * 1e3, 3e4);
|
|
232
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
throw lastError;
|
|
237
|
-
}
|
|
238
|
-
async function translateAll(input) {
|
|
239
|
-
const {
|
|
240
|
-
model,
|
|
241
|
-
entries,
|
|
242
|
-
sourceLocale,
|
|
243
|
-
targetLocale,
|
|
244
|
-
options,
|
|
245
|
-
onBatchComplete
|
|
246
|
-
} = input;
|
|
247
|
-
const keys = Object.keys(entries);
|
|
248
|
-
if (keys.length === 0) return {};
|
|
249
|
-
const batchSize = options?.batchSize ?? 50;
|
|
250
|
-
const concurrency = options?.concurrency ?? 3;
|
|
251
|
-
const retries = options?.retries ?? 2;
|
|
252
|
-
const limit = pLimit(concurrency);
|
|
253
|
-
const batches = [];
|
|
254
|
-
for (let i = 0; i < keys.length; i += batchSize) {
|
|
255
|
-
const batchKeys = keys.slice(i, i + batchSize);
|
|
256
|
-
const batch = {};
|
|
257
|
-
for (const key of batchKeys) {
|
|
258
|
-
batch[key] = entries[key];
|
|
259
|
-
}
|
|
260
|
-
batches.push(batch);
|
|
261
|
-
}
|
|
262
|
-
const results = {};
|
|
263
|
-
await Promise.all(
|
|
264
|
-
batches.map(
|
|
265
|
-
(batch) => limit(async () => {
|
|
266
|
-
const translated = await translateBatchWithRetry(
|
|
267
|
-
{ model, entries: batch, sourceLocale, targetLocale, options },
|
|
268
|
-
retries
|
|
269
|
-
);
|
|
270
|
-
Object.assign(results, translated);
|
|
271
|
-
onBatchComplete?.(translated);
|
|
272
|
-
})
|
|
273
|
-
)
|
|
274
|
-
);
|
|
275
|
-
return results;
|
|
276
|
-
}
|
|
277
|
-
var init_translate = __esm({
|
|
278
|
-
"src/translate.ts"() {
|
|
279
|
-
"use strict";
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// src/writer.ts
|
|
284
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
285
|
-
import { join as join2, dirname } from "path";
|
|
286
|
-
async function writeTranslation(filePath, flatEntries, options) {
|
|
287
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
288
|
-
const data = options?.flat ? flatEntries : unflatten(flatEntries);
|
|
289
|
-
const content = JSON.stringify(data, null, 2) + "\n";
|
|
290
|
-
await writeFile(filePath, content, "utf-8");
|
|
291
|
-
}
|
|
292
|
-
async function writeLockFile(messagesDir, sourceFlat, existingLock, translatedKeys) {
|
|
293
|
-
const lock = { ...existingLock };
|
|
294
|
-
for (const key of translatedKeys) {
|
|
295
|
-
if (key in sourceFlat) {
|
|
296
|
-
lock[key] = hashValue(sourceFlat[key]);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
for (const key of Object.keys(lock)) {
|
|
300
|
-
if (!(key in sourceFlat)) {
|
|
301
|
-
delete lock[key];
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const lockPath = join2(messagesDir, ".translate-lock.json");
|
|
305
|
-
await mkdir(dirname(lockPath), { recursive: true });
|
|
306
|
-
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
307
|
-
await writeFile(lockPath, content, "utf-8");
|
|
308
|
-
}
|
|
309
|
-
var init_writer = __esm({
|
|
310
|
-
"src/writer.ts"() {
|
|
311
|
-
"use strict";
|
|
312
|
-
init_flatten();
|
|
313
|
-
init_diff();
|
|
314
169
|
}
|
|
315
170
|
});
|
|
316
171
|
|
|
@@ -408,8 +263,8 @@ var init_filters = __esm({
|
|
|
408
263
|
// CONSTANT_CASE
|
|
409
264
|
/^[\d.,%$€£¥]+$/,
|
|
410
265
|
// Numbers, currency
|
|
411
|
-
/^[
|
|
412
|
-
// No letters at all
|
|
266
|
+
/^[^\p{L}]*$/u
|
|
267
|
+
// No letters at all (Unicode-aware)
|
|
413
268
|
];
|
|
414
269
|
CONTENT_PROPERTY_NAMES = [
|
|
415
270
|
"title",
|
|
@@ -487,8 +342,92 @@ var init_ast_helpers = __esm({
|
|
|
487
342
|
}
|
|
488
343
|
});
|
|
489
344
|
|
|
345
|
+
// src/utils/template-literal.ts
|
|
346
|
+
import * as t from "@babel/types";
|
|
347
|
+
function memberExpressionToName(node) {
|
|
348
|
+
if (node.type === "Identifier") return node.name;
|
|
349
|
+
if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
|
|
350
|
+
const objectName = memberExpressionToName(node.object);
|
|
351
|
+
if (!objectName) return null;
|
|
352
|
+
const prop = node.property.name;
|
|
353
|
+
return objectName + prop.charAt(0).toUpperCase() + prop.slice(1);
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
function buildTemplateLiteralText(quasis, expressions) {
|
|
358
|
+
const placeholders = [];
|
|
359
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
360
|
+
let text2 = "";
|
|
361
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
362
|
+
text2 += quasis[i].value.cooked ?? quasis[i].value.raw;
|
|
363
|
+
if (i < expressions.length) {
|
|
364
|
+
const expr = expressions[i];
|
|
365
|
+
let name = null;
|
|
366
|
+
if (expr.type === "Identifier") {
|
|
367
|
+
name = expr.name;
|
|
368
|
+
} else if (expr.type === "MemberExpression") {
|
|
369
|
+
name = memberExpressionToName(expr);
|
|
370
|
+
}
|
|
371
|
+
if (name === null) return null;
|
|
372
|
+
let finalName = name;
|
|
373
|
+
if (usedNames.has(finalName)) {
|
|
374
|
+
let suffix = 2;
|
|
375
|
+
while (usedNames.has(`${name}${suffix}`)) suffix++;
|
|
376
|
+
finalName = `${name}${suffix}`;
|
|
377
|
+
}
|
|
378
|
+
usedNames.add(finalName);
|
|
379
|
+
placeholders.push(finalName);
|
|
380
|
+
text2 += `{${finalName}}`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return { text: text2, placeholders };
|
|
384
|
+
}
|
|
385
|
+
function buildValuesObject(expressions, placeholders) {
|
|
386
|
+
const properties = placeholders.map((name, i) => {
|
|
387
|
+
const expr = expressions[i];
|
|
388
|
+
const isShorthand = expr.type === "Identifier" && expr.name === name;
|
|
389
|
+
return t.objectProperty(
|
|
390
|
+
t.identifier(name),
|
|
391
|
+
t.cloneNode(expr),
|
|
392
|
+
false,
|
|
393
|
+
isShorthand
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
return t.objectExpression(properties);
|
|
397
|
+
}
|
|
398
|
+
var init_template_literal = __esm({
|
|
399
|
+
"src/utils/template-literal.ts"() {
|
|
400
|
+
"use strict";
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
490
404
|
// src/scanner/extractor.ts
|
|
491
405
|
import _traverse from "@babel/traverse";
|
|
406
|
+
function extractTextFromNode(node) {
|
|
407
|
+
if (node.type === "StringLiteral") {
|
|
408
|
+
const trimmed = node.value.trim();
|
|
409
|
+
return trimmed || null;
|
|
410
|
+
}
|
|
411
|
+
if (node.type === "TemplateLiteral") {
|
|
412
|
+
const info = buildTemplateLiteralText(node.quasis, node.expressions);
|
|
413
|
+
return info ? info.text : null;
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
function collectConditionalTexts(node) {
|
|
418
|
+
const texts = [];
|
|
419
|
+
for (const branch of [node.consequent, node.alternate]) {
|
|
420
|
+
if (branch.type === "ConditionalExpression") {
|
|
421
|
+
texts.push(...collectConditionalTexts(branch));
|
|
422
|
+
} else {
|
|
423
|
+
const text2 = extractTextFromNode(branch);
|
|
424
|
+
if (text2 && !shouldIgnore(text2)) {
|
|
425
|
+
texts.push(text2);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return texts;
|
|
430
|
+
}
|
|
492
431
|
function extractStrings(ast, filePath, translatableProps) {
|
|
493
432
|
const results = [];
|
|
494
433
|
traverse(ast, {
|
|
@@ -517,8 +456,33 @@ function extractStrings(ast, filePath, translatableProps) {
|
|
|
517
456
|
let text2;
|
|
518
457
|
if (value.type === "StringLiteral") {
|
|
519
458
|
text2 = value.value;
|
|
520
|
-
} else if (value.type === "JSXExpressionContainer"
|
|
521
|
-
|
|
459
|
+
} else if (value.type === "JSXExpressionContainer") {
|
|
460
|
+
if (value.expression.type === "StringLiteral") {
|
|
461
|
+
text2 = value.expression.value;
|
|
462
|
+
} else if (value.expression.type === "TemplateLiteral") {
|
|
463
|
+
const info = buildTemplateLiteralText(
|
|
464
|
+
value.expression.quasis,
|
|
465
|
+
value.expression.expressions
|
|
466
|
+
);
|
|
467
|
+
if (info) text2 = info.text;
|
|
468
|
+
} else if (value.expression.type === "ConditionalExpression") {
|
|
469
|
+
const parentTag2 = getParentTagName(path);
|
|
470
|
+
if (parentTag2 && isIgnoredTag(parentTag2)) return;
|
|
471
|
+
const texts = collectConditionalTexts(value.expression);
|
|
472
|
+
for (const t3 of texts) {
|
|
473
|
+
results.push({
|
|
474
|
+
text: t3,
|
|
475
|
+
type: "jsx-attribute",
|
|
476
|
+
file: filePath,
|
|
477
|
+
line: path.node.loc?.start.line ?? 0,
|
|
478
|
+
column: path.node.loc?.start.column ?? 0,
|
|
479
|
+
componentName: getComponentName(path),
|
|
480
|
+
propName,
|
|
481
|
+
parentTag: parentTag2
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
522
486
|
}
|
|
523
487
|
if (!text2 || shouldIgnore(text2)) return;
|
|
524
488
|
const parentTag = getParentTagName(path);
|
|
@@ -535,11 +499,33 @@ function extractStrings(ast, filePath, translatableProps) {
|
|
|
535
499
|
});
|
|
536
500
|
},
|
|
537
501
|
JSXExpressionContainer(path) {
|
|
538
|
-
const expr = path.node.expression;
|
|
539
|
-
if (expr.type !== "StringLiteral") return;
|
|
540
|
-
const text2 = expr.value.trim();
|
|
541
|
-
if (shouldIgnore(text2)) return;
|
|
542
502
|
if (path.parent.type === "JSXAttribute") return;
|
|
503
|
+
const expr = path.node.expression;
|
|
504
|
+
if (expr.type === "ConditionalExpression") {
|
|
505
|
+
const parentTag2 = getParentTagName(path);
|
|
506
|
+
if (parentTag2 && isIgnoredTag(parentTag2)) return;
|
|
507
|
+
const texts = collectConditionalTexts(expr);
|
|
508
|
+
for (const t3 of texts) {
|
|
509
|
+
results.push({
|
|
510
|
+
text: t3,
|
|
511
|
+
type: "jsx-expression",
|
|
512
|
+
file: filePath,
|
|
513
|
+
line: path.node.loc?.start.line ?? 0,
|
|
514
|
+
column: path.node.loc?.start.column ?? 0,
|
|
515
|
+
componentName: getComponentName(path),
|
|
516
|
+
parentTag: parentTag2
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
let text2;
|
|
522
|
+
if (expr.type === "StringLiteral") {
|
|
523
|
+
text2 = expr.value.trim();
|
|
524
|
+
} else if (expr.type === "TemplateLiteral") {
|
|
525
|
+
const info = buildTemplateLiteralText(expr.quasis, expr.expressions);
|
|
526
|
+
if (info) text2 = info.text;
|
|
527
|
+
}
|
|
528
|
+
if (!text2 || shouldIgnore(text2)) return;
|
|
543
529
|
const parentTag = getParentTagName(path);
|
|
544
530
|
if (parentTag && isIgnoredTag(parentTag)) return;
|
|
545
531
|
results.push({
|
|
@@ -560,9 +546,32 @@ function extractStrings(ast, filePath, translatableProps) {
|
|
|
560
546
|
const propName = keyNode.type === "Identifier" ? keyNode.name : keyNode.value;
|
|
561
547
|
if (!isContentProperty(propName)) return;
|
|
562
548
|
const valueNode = path.node.value;
|
|
563
|
-
if (valueNode.type
|
|
564
|
-
|
|
565
|
-
|
|
549
|
+
if (valueNode.type === "ConditionalExpression") {
|
|
550
|
+
const texts = collectConditionalTexts(valueNode);
|
|
551
|
+
for (const t3 of texts) {
|
|
552
|
+
results.push({
|
|
553
|
+
text: t3,
|
|
554
|
+
type: "object-property",
|
|
555
|
+
file: filePath,
|
|
556
|
+
line: valueNode.loc?.start.line ?? 0,
|
|
557
|
+
column: valueNode.loc?.start.column ?? 0,
|
|
558
|
+
componentName: getComponentName(path),
|
|
559
|
+
propName
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
let text2;
|
|
565
|
+
if (valueNode.type === "StringLiteral") {
|
|
566
|
+
text2 = valueNode.value.trim();
|
|
567
|
+
} else if (valueNode.type === "TemplateLiteral") {
|
|
568
|
+
const info = buildTemplateLiteralText(
|
|
569
|
+
valueNode.quasis,
|
|
570
|
+
valueNode.expressions
|
|
571
|
+
);
|
|
572
|
+
if (info) text2 = info.text;
|
|
573
|
+
}
|
|
574
|
+
if (!text2 || shouldIgnore(text2)) return;
|
|
566
575
|
results.push({
|
|
567
576
|
text: text2,
|
|
568
577
|
type: "object-property",
|
|
@@ -641,33 +650,48 @@ var init_extractor = __esm({
|
|
|
641
650
|
"use strict";
|
|
642
651
|
init_filters();
|
|
643
652
|
init_ast_helpers();
|
|
653
|
+
init_template_literal();
|
|
644
654
|
traverse = resolveDefault(_traverse);
|
|
645
655
|
}
|
|
646
656
|
});
|
|
647
657
|
|
|
648
|
-
// src/scanner/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
658
|
+
// src/scanner/context-enricher.ts
|
|
659
|
+
function deriveRoutePath(filePath) {
|
|
660
|
+
const appMatch = filePath.match(/src\/app\/(.+?)\/page\.[jt]sx?$/);
|
|
661
|
+
if (appMatch) return appMatch[1].replace(/\//g, ".");
|
|
662
|
+
const pagesMatch = filePath.match(/(?:src\/)?pages\/(.+?)\.[jt]sx?$/);
|
|
663
|
+
if (pagesMatch) {
|
|
664
|
+
const route = pagesMatch[1].replace(/\/index$/, "").replace(/\//g, ".");
|
|
665
|
+
return route || void 0;
|
|
666
|
+
}
|
|
667
|
+
const compMatch = filePath.match(/src\/components\/(.+?)\//);
|
|
668
|
+
if (compMatch) return compMatch[1];
|
|
669
|
+
return void 0;
|
|
652
670
|
}
|
|
653
|
-
function
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
671
|
+
function enrichStrings(strings, filePath) {
|
|
672
|
+
const routePath = deriveRoutePath(filePath);
|
|
673
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
674
|
+
for (const str of strings) {
|
|
675
|
+
const key = str.componentName ?? "__root__";
|
|
676
|
+
if (!byComponent.has(key)) byComponent.set(key, []);
|
|
677
|
+
byComponent.get(key).push(str);
|
|
678
|
+
}
|
|
679
|
+
const headings = strings.filter((s) => s.parentTag && /^h[1-3]$/.test(s.parentTag));
|
|
680
|
+
const defaultHeading = headings.length > 0 ? headings[0].text : void 0;
|
|
681
|
+
return strings.map((str) => {
|
|
682
|
+
const enriched = { ...str };
|
|
683
|
+
if (routePath) enriched.routePath = routePath;
|
|
684
|
+
const siblings = byComponent.get(str.componentName ?? "__root__") ?? [];
|
|
685
|
+
const siblingTexts = siblings.filter((s) => s !== str).slice(0, 5).map((s) => s.text);
|
|
686
|
+
if (siblingTexts.length > 0) enriched.siblingTexts = siblingTexts;
|
|
687
|
+
if (defaultHeading && str.text !== defaultHeading) {
|
|
688
|
+
enriched.sectionHeading = defaultHeading;
|
|
689
|
+
}
|
|
690
|
+
return enriched;
|
|
691
|
+
});
|
|
668
692
|
}
|
|
669
|
-
var
|
|
670
|
-
"src/scanner/
|
|
693
|
+
var init_context_enricher = __esm({
|
|
694
|
+
"src/scanner/context-enricher.ts"() {
|
|
671
695
|
"use strict";
|
|
672
696
|
}
|
|
673
697
|
});
|
|
@@ -741,6 +765,22 @@ function logVerbose(message, verbose) {
|
|
|
741
765
|
console.log(pc.dim(` ${message}`));
|
|
742
766
|
}
|
|
743
767
|
}
|
|
768
|
+
function logUsage(tokens, cost) {
|
|
769
|
+
console.log(`
|
|
770
|
+
${pc.dim("Tokens:")} ${tokens}`);
|
|
771
|
+
if (cost) console.log(` ${pc.dim("Est. cost:")} ${cost}`);
|
|
772
|
+
}
|
|
773
|
+
function logProgress(current, total, label) {
|
|
774
|
+
if (!process.stdout.isTTY) return;
|
|
775
|
+
const pct = total > 0 ? Math.round(current / total * 100) : 0;
|
|
776
|
+
process.stdout.write(
|
|
777
|
+
`\r ${pc.dim(label)} ${pc.bold(`${current}`)}${pc.dim(`/${total}`)} ${pc.dim(`(${pct}%)`)}`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
function logProgressClear() {
|
|
781
|
+
if (!process.stdout.isTTY) return;
|
|
782
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
783
|
+
}
|
|
744
784
|
var init_logger = __esm({
|
|
745
785
|
"src/logger.ts"() {
|
|
746
786
|
"use strict";
|
|
@@ -750,39 +790,46 @@ var init_logger = __esm({
|
|
|
750
790
|
// src/scanner/index.ts
|
|
751
791
|
import { readFile as readFile2 } from "fs/promises";
|
|
752
792
|
import { glob } from "tinyglobby";
|
|
753
|
-
|
|
793
|
+
import pLimit from "p-limit";
|
|
794
|
+
async function scan(options, cwd = process.cwd(), callbacks) {
|
|
754
795
|
const files = await glob(options.include, {
|
|
755
796
|
ignore: options.exclude ?? [],
|
|
756
797
|
cwd,
|
|
757
798
|
absolute: true
|
|
758
799
|
});
|
|
759
800
|
const allStrings = [];
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
801
|
+
const limit = pLimit(10);
|
|
802
|
+
let completed = 0;
|
|
803
|
+
const fileResults = await Promise.all(
|
|
804
|
+
files.map(
|
|
805
|
+
(filePath) => limit(async () => {
|
|
806
|
+
const code = await readFile2(filePath, "utf-8");
|
|
807
|
+
let ast;
|
|
808
|
+
try {
|
|
809
|
+
ast = parseFile(code, filePath);
|
|
810
|
+
} catch (err) {
|
|
811
|
+
logVerbose(
|
|
812
|
+
`Skipping unparseable file ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
813
|
+
false
|
|
814
|
+
);
|
|
815
|
+
completed++;
|
|
816
|
+
callbacks?.onProgress?.(completed, files.length);
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
const raw = extractStrings(ast, filePath, options.translatableProps);
|
|
820
|
+
const strings = enrichStrings(raw, filePath);
|
|
821
|
+
completed++;
|
|
822
|
+
callbacks?.onProgress?.(completed, files.length);
|
|
823
|
+
return { strings, filePath };
|
|
824
|
+
})
|
|
825
|
+
)
|
|
826
|
+
);
|
|
827
|
+
for (const result of fileResults) {
|
|
828
|
+
if (!result) continue;
|
|
829
|
+
allStrings.push(...result.strings);
|
|
782
830
|
}
|
|
783
831
|
return {
|
|
784
832
|
strings: allStrings,
|
|
785
|
-
messages,
|
|
786
833
|
fileCount: files.length
|
|
787
834
|
};
|
|
788
835
|
}
|
|
@@ -791,16 +838,16 @@ var init_scanner = __esm({
|
|
|
791
838
|
"use strict";
|
|
792
839
|
init_parser();
|
|
793
840
|
init_extractor();
|
|
794
|
-
|
|
841
|
+
init_context_enricher();
|
|
795
842
|
init_logger();
|
|
796
843
|
}
|
|
797
844
|
});
|
|
798
845
|
|
|
799
846
|
// src/scanner/key-ai.ts
|
|
800
|
-
import { generateObject
|
|
801
|
-
import { z as
|
|
847
|
+
import { generateObject } from "ai";
|
|
848
|
+
import { z as z2 } from "zod";
|
|
802
849
|
import pLimit2 from "p-limit";
|
|
803
|
-
function
|
|
850
|
+
function buildPrompt(strings) {
|
|
804
851
|
const lines = [
|
|
805
852
|
"Generate semantic i18n keys for these UI strings.",
|
|
806
853
|
"",
|
|
@@ -825,41 +872,49 @@ function buildPrompt2(strings) {
|
|
|
825
872
|
if (str.parentTag) parts.push(`tag: ${str.parentTag}`);
|
|
826
873
|
if (str.propName) parts.push(`prop: ${str.propName}`);
|
|
827
874
|
if (str.file) parts.push(`file: ${str.file}`);
|
|
875
|
+
if (str.routePath) parts.push(`route: ${str.routePath}`);
|
|
876
|
+
if (str.sectionHeading) parts.push(`section: "${str.sectionHeading}"`);
|
|
877
|
+
if (str.siblingTexts?.length) {
|
|
878
|
+
parts.push(`siblings: [${str.siblingTexts.slice(0, 3).map((t3) => `"${t3}"`).join(", ")}]`);
|
|
879
|
+
}
|
|
828
880
|
lines.push(` ${parts.join(", ")}`);
|
|
829
881
|
}
|
|
830
882
|
return lines.join("\n");
|
|
831
883
|
}
|
|
832
884
|
async function generateKeysBatchWithRetry(model, strings, retries) {
|
|
833
|
-
const prompt =
|
|
885
|
+
const prompt = buildPrompt(strings);
|
|
834
886
|
const texts = strings.map((s) => s.text);
|
|
835
|
-
const schema =
|
|
836
|
-
mappings:
|
|
837
|
-
|
|
838
|
-
index:
|
|
839
|
-
key:
|
|
887
|
+
const schema = z2.object({
|
|
888
|
+
mappings: z2.array(
|
|
889
|
+
z2.object({
|
|
890
|
+
index: z2.number().describe("Zero-based index of the string"),
|
|
891
|
+
key: z2.string().describe("Semantic i18n key")
|
|
840
892
|
})
|
|
841
893
|
)
|
|
842
894
|
});
|
|
843
895
|
let lastError;
|
|
896
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
844
897
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
845
898
|
try {
|
|
846
|
-
const { object } = await
|
|
899
|
+
const { object, usage } = await generateObject({
|
|
847
900
|
model,
|
|
848
901
|
prompt,
|
|
849
902
|
schema
|
|
850
903
|
});
|
|
904
|
+
totalUsage.inputTokens += usage.inputTokens ?? 0;
|
|
905
|
+
totalUsage.outputTokens += usage.outputTokens ?? 0;
|
|
851
906
|
const result = {};
|
|
852
907
|
for (const mapping of object.mappings) {
|
|
853
908
|
if (mapping.index >= 0 && mapping.index < texts.length) {
|
|
854
909
|
result[texts[mapping.index]] = mapping.key;
|
|
855
910
|
}
|
|
856
911
|
}
|
|
857
|
-
return result;
|
|
912
|
+
return { keys: result, usage: totalUsage };
|
|
858
913
|
} catch (error) {
|
|
859
914
|
lastError = error;
|
|
860
915
|
if (attempt < retries) {
|
|
861
916
|
const delay = Math.min(Math.pow(2, attempt) * 1e3, 3e4);
|
|
862
|
-
await new Promise((
|
|
917
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
863
918
|
}
|
|
864
919
|
}
|
|
865
920
|
}
|
|
@@ -885,12 +940,22 @@ async function generateSemanticKeys(input) {
|
|
|
885
940
|
model,
|
|
886
941
|
strings,
|
|
887
942
|
existingMap = {},
|
|
943
|
+
allTexts,
|
|
888
944
|
batchSize = 50,
|
|
889
945
|
concurrency = 3,
|
|
890
|
-
retries = 2
|
|
946
|
+
retries = 2,
|
|
947
|
+
onProgress,
|
|
948
|
+
onUsage
|
|
891
949
|
} = input;
|
|
892
|
-
const
|
|
893
|
-
|
|
950
|
+
const activeTexts = allTexts ?? new Set(strings.map((s) => s.text));
|
|
951
|
+
const activeExisting = {};
|
|
952
|
+
for (const [text2, key] of Object.entries(existingMap)) {
|
|
953
|
+
if (activeTexts.has(text2)) {
|
|
954
|
+
activeExisting[text2] = key;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
const newStrings = strings.filter((s) => !(s.text in activeExisting));
|
|
958
|
+
if (newStrings.length === 0) return activeExisting;
|
|
894
959
|
const uniqueMap = /* @__PURE__ */ new Map();
|
|
895
960
|
for (const str of newStrings) {
|
|
896
961
|
if (!uniqueMap.has(str.text)) {
|
|
@@ -904,16 +969,26 @@ async function generateSemanticKeys(input) {
|
|
|
904
969
|
batches.push(uniqueStrings.slice(i, i + batchSize));
|
|
905
970
|
}
|
|
906
971
|
const allNewKeys = {};
|
|
972
|
+
let completedStrings = 0;
|
|
973
|
+
let totalInputTokens = 0;
|
|
974
|
+
let totalOutputTokens = 0;
|
|
907
975
|
await Promise.all(
|
|
908
976
|
batches.map(
|
|
909
977
|
(batch) => limit(async () => {
|
|
910
|
-
const keys = await generateKeysBatchWithRetry(model, batch, retries);
|
|
978
|
+
const { keys, usage } = await generateKeysBatchWithRetry(model, batch, retries);
|
|
911
979
|
Object.assign(allNewKeys, keys);
|
|
980
|
+
totalInputTokens += usage.inputTokens;
|
|
981
|
+
totalOutputTokens += usage.outputTokens;
|
|
982
|
+
completedStrings += batch.length;
|
|
983
|
+
onProgress?.(completedStrings, uniqueStrings.length);
|
|
912
984
|
})
|
|
913
985
|
)
|
|
914
986
|
);
|
|
915
|
-
|
|
916
|
-
|
|
987
|
+
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
|
988
|
+
onUsage?.({ inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
|
|
989
|
+
}
|
|
990
|
+
const resolved = resolveCollisions(allNewKeys, activeExisting);
|
|
991
|
+
return { ...activeExisting, ...resolved };
|
|
917
992
|
}
|
|
918
993
|
var init_key_ai = __esm({
|
|
919
994
|
"src/scanner/key-ai.ts"() {
|
|
@@ -924,7 +999,14 @@ var init_key_ai = __esm({
|
|
|
924
999
|
// src/codegen/transform.ts
|
|
925
1000
|
import _traverse2 from "@babel/traverse";
|
|
926
1001
|
import _generate from "@babel/generator";
|
|
927
|
-
import * as
|
|
1002
|
+
import * as t2 from "@babel/types";
|
|
1003
|
+
function hasSubstantialSiblings(parent) {
|
|
1004
|
+
const count = parent.node.children.filter((child) => {
|
|
1005
|
+
if (child.type === "JSXText") return child.value.trim().length > 0;
|
|
1006
|
+
return true;
|
|
1007
|
+
}).length;
|
|
1008
|
+
return count > 1;
|
|
1009
|
+
}
|
|
928
1010
|
function findLastImportIndex(ast) {
|
|
929
1011
|
let idx = -1;
|
|
930
1012
|
for (let i = 0; i < ast.program.body.length; i++) {
|
|
@@ -946,25 +1028,123 @@ function hasUseTranslationsImport(ast, importSource) {
|
|
|
946
1028
|
}
|
|
947
1029
|
return false;
|
|
948
1030
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
path.stop();
|
|
1031
|
+
function hasGetTranslationsImport(ast, importSource) {
|
|
1032
|
+
for (const node of ast.program.body) {
|
|
1033
|
+
if (node.type === "ImportDeclaration" && node.source.value === importSource) {
|
|
1034
|
+
for (const spec of node.specifiers) {
|
|
1035
|
+
if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "getTranslations") {
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
957
1038
|
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
function transformConditionalBranch(node, textToKey) {
|
|
1044
|
+
if (node.type === "ConditionalExpression") {
|
|
1045
|
+
const cons = transformConditionalBranch(
|
|
1046
|
+
node.consequent,
|
|
1047
|
+
textToKey
|
|
1048
|
+
);
|
|
1049
|
+
const alt = transformConditionalBranch(
|
|
1050
|
+
node.alternate,
|
|
1051
|
+
textToKey
|
|
1052
|
+
);
|
|
1053
|
+
if (cons.count > 0 || alt.count > 0) {
|
|
1054
|
+
return {
|
|
1055
|
+
node: t2.conditionalExpression(node.test, cons.node, alt.node),
|
|
1056
|
+
count: cons.count + alt.count
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
return { node, count: 0 };
|
|
1060
|
+
}
|
|
1061
|
+
if (node.type === "StringLiteral") {
|
|
1062
|
+
const text2 = node.value.trim();
|
|
1063
|
+
if (text2 && text2 in textToKey) {
|
|
1064
|
+
const key = textToKey[text2];
|
|
1065
|
+
return {
|
|
1066
|
+
node: t2.callExpression(t2.identifier("t"), [t2.stringLiteral(key)]),
|
|
1067
|
+
count: 1
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return { node, count: 0 };
|
|
1071
|
+
}
|
|
1072
|
+
if (node.type === "TemplateLiteral") {
|
|
1073
|
+
const info = buildTemplateLiteralText(node.quasis, node.expressions);
|
|
1074
|
+
if (info && info.text in textToKey) {
|
|
1075
|
+
const key = textToKey[info.text];
|
|
1076
|
+
const args = [t2.stringLiteral(key)];
|
|
1077
|
+
if (info.placeholders.length > 0) {
|
|
1078
|
+
args.push(buildValuesObject(node.expressions, info.placeholders));
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
node: t2.callExpression(t2.identifier("t"), args),
|
|
1082
|
+
count: 1
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
return { node, count: 0 };
|
|
1086
|
+
}
|
|
1087
|
+
return { node, count: 0 };
|
|
1088
|
+
}
|
|
1089
|
+
function transformConditionalBranchInline(node, textToKey) {
|
|
1090
|
+
if (node.type === "ConditionalExpression") {
|
|
1091
|
+
const cons = transformConditionalBranchInline(
|
|
1092
|
+
node.consequent,
|
|
1093
|
+
textToKey
|
|
1094
|
+
);
|
|
1095
|
+
const alt = transformConditionalBranchInline(
|
|
1096
|
+
node.alternate,
|
|
1097
|
+
textToKey
|
|
1098
|
+
);
|
|
1099
|
+
if (cons.count > 0 || alt.count > 0) {
|
|
1100
|
+
return {
|
|
1101
|
+
node: t2.conditionalExpression(node.test, cons.node, alt.node),
|
|
1102
|
+
count: cons.count + alt.count
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
return { node, count: 0 };
|
|
1106
|
+
}
|
|
1107
|
+
if (node.type === "StringLiteral") {
|
|
1108
|
+
const text2 = node.value.trim();
|
|
1109
|
+
if (text2 && text2 in textToKey) {
|
|
1110
|
+
const key = textToKey[text2];
|
|
1111
|
+
return {
|
|
1112
|
+
node: t2.callExpression(t2.identifier("t"), [
|
|
1113
|
+
t2.stringLiteral(text2),
|
|
1114
|
+
t2.stringLiteral(key)
|
|
1115
|
+
]),
|
|
1116
|
+
count: 1
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
return { node, count: 0 };
|
|
1120
|
+
}
|
|
1121
|
+
if (node.type === "TemplateLiteral") {
|
|
1122
|
+
const info = buildTemplateLiteralText(node.quasis, node.expressions);
|
|
1123
|
+
if (info && info.text in textToKey) {
|
|
1124
|
+
const key = textToKey[info.text];
|
|
1125
|
+
const args = [
|
|
1126
|
+
t2.stringLiteral(info.text),
|
|
1127
|
+
t2.stringLiteral(key)
|
|
1128
|
+
];
|
|
1129
|
+
if (info.placeholders.length > 0) {
|
|
1130
|
+
args.push(buildValuesObject(node.expressions, info.placeholders));
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
node: t2.callExpression(t2.identifier("t"), args),
|
|
1134
|
+
count: 1
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
return { node, count: 0 };
|
|
1138
|
+
}
|
|
1139
|
+
return { node, count: 0 };
|
|
962
1140
|
}
|
|
963
1141
|
function transform(ast, textToKey, options = {}) {
|
|
964
1142
|
if (options.mode === "inline") {
|
|
965
1143
|
return transformInline(ast, textToKey, options);
|
|
966
1144
|
}
|
|
967
1145
|
const importSource = options.i18nImport ?? "next-intl";
|
|
1146
|
+
const supportsServerSplit = importSource === "next-intl";
|
|
1147
|
+
const isClient = !supportsServerSplit || options.forceClient || detectClientFile(ast);
|
|
968
1148
|
let stringsWrapped = 0;
|
|
969
1149
|
const componentsNeedingT = /* @__PURE__ */ new Set();
|
|
970
1150
|
traverse2(ast, {
|
|
@@ -974,14 +1154,10 @@ function transform(ast, textToKey, options = {}) {
|
|
|
974
1154
|
const parent = path.parentPath;
|
|
975
1155
|
if (!parent?.isJSXElement()) return;
|
|
976
1156
|
const key = textToKey[text2];
|
|
977
|
-
const tCall =
|
|
978
|
-
|
|
1157
|
+
const tCall = t2.jsxExpressionContainer(
|
|
1158
|
+
t2.callExpression(t2.identifier("t"), [t2.stringLiteral(key)])
|
|
979
1159
|
);
|
|
980
|
-
|
|
981
|
-
if (child.type === "JSXText") return child.value.trim().length > 0;
|
|
982
|
-
return true;
|
|
983
|
-
});
|
|
984
|
-
if (siblings.length === 1) {
|
|
1160
|
+
if (!hasSubstantialSiblings(parent)) {
|
|
985
1161
|
path.replaceWith(tCall);
|
|
986
1162
|
} else {
|
|
987
1163
|
const raw = path.node.value;
|
|
@@ -989,11 +1165,11 @@ function transform(ast, textToKey, options = {}) {
|
|
|
989
1165
|
const hasTrailing = raw !== raw.trimEnd();
|
|
990
1166
|
const nodes = [];
|
|
991
1167
|
if (hasLeading) {
|
|
992
|
-
nodes.push(
|
|
1168
|
+
nodes.push(t2.jsxExpressionContainer(t2.stringLiteral(" ")));
|
|
993
1169
|
}
|
|
994
1170
|
nodes.push(tCall);
|
|
995
1171
|
if (hasTrailing) {
|
|
996
|
-
nodes.push(
|
|
1172
|
+
nodes.push(t2.jsxExpressionContainer(t2.stringLiteral(" ")));
|
|
997
1173
|
}
|
|
998
1174
|
path.replaceWithMultiple(nodes);
|
|
999
1175
|
}
|
|
@@ -1001,68 +1177,162 @@ function transform(ast, textToKey, options = {}) {
|
|
|
1001
1177
|
const compName = getComponentName(path);
|
|
1002
1178
|
if (compName) componentsNeedingT.add(compName);
|
|
1003
1179
|
},
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1180
|
+
JSXExpressionContainer(path) {
|
|
1181
|
+
const expr = path.node.expression;
|
|
1182
|
+
if (path.parent.type === "JSXAttribute") return;
|
|
1183
|
+
if (expr.type === "ConditionalExpression") {
|
|
1184
|
+
const result = transformConditionalBranch(expr, textToKey);
|
|
1185
|
+
if (result.count > 0) {
|
|
1186
|
+
path.node.expression = result.node;
|
|
1187
|
+
stringsWrapped += result.count;
|
|
1188
|
+
const compName2 = getComponentName(path);
|
|
1189
|
+
if (compName2) componentsNeedingT.add(compName2);
|
|
1190
|
+
}
|
|
1015
1191
|
return;
|
|
1016
1192
|
}
|
|
1193
|
+
if (expr.type !== "TemplateLiteral") return;
|
|
1194
|
+
const info = buildTemplateLiteralText(expr.quasis, expr.expressions);
|
|
1195
|
+
if (!info) return;
|
|
1196
|
+
const { text: text2, placeholders } = info;
|
|
1197
|
+
if (!(text2 in textToKey)) return;
|
|
1017
1198
|
const key = textToKey[text2];
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1199
|
+
const args = [t2.stringLiteral(key)];
|
|
1200
|
+
if (placeholders.length > 0) {
|
|
1201
|
+
args.push(buildValuesObject(expr.expressions, placeholders));
|
|
1202
|
+
}
|
|
1203
|
+
path.node.expression = t2.callExpression(t2.identifier("t"), args);
|
|
1204
|
+
stringsWrapped++;
|
|
1205
|
+
const compName = getComponentName(path);
|
|
1206
|
+
if (compName) componentsNeedingT.add(compName);
|
|
1207
|
+
},
|
|
1208
|
+
JSXAttribute(path) {
|
|
1209
|
+
const value = path.node.value;
|
|
1210
|
+
if (!value) return;
|
|
1211
|
+
if (value.type === "JSXExpressionContainer" && value.expression.type === "ConditionalExpression") {
|
|
1212
|
+
const result = transformConditionalBranch(value.expression, textToKey);
|
|
1213
|
+
if (result.count > 0) {
|
|
1214
|
+
path.node.value = t2.jsxExpressionContainer(result.node);
|
|
1215
|
+
stringsWrapped += result.count;
|
|
1216
|
+
const compName2 = getComponentName(path);
|
|
1217
|
+
if (compName2) componentsNeedingT.add(compName2);
|
|
1218
|
+
}
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
let text2;
|
|
1222
|
+
let templateInfo;
|
|
1223
|
+
if (value.type === "StringLiteral") {
|
|
1224
|
+
text2 = value.value;
|
|
1225
|
+
} else if (value.type === "JSXExpressionContainer") {
|
|
1226
|
+
if (value.expression.type === "StringLiteral") {
|
|
1227
|
+
text2 = value.expression.value;
|
|
1228
|
+
} else if (value.expression.type === "TemplateLiteral") {
|
|
1229
|
+
const info = buildTemplateLiteralText(
|
|
1230
|
+
value.expression.quasis,
|
|
1231
|
+
value.expression.expressions
|
|
1232
|
+
);
|
|
1233
|
+
if (info) {
|
|
1234
|
+
text2 = info.text;
|
|
1235
|
+
templateInfo = {
|
|
1236
|
+
placeholders: info.placeholders,
|
|
1237
|
+
expressions: value.expression.expressions
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (!text2 || !(text2 in textToKey)) return;
|
|
1243
|
+
if (value.type === "JSXExpressionContainer" && value.expression.type === "CallExpression" && value.expression.callee.type === "Identifier" && value.expression.callee.name === "t") {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const key = textToKey[text2];
|
|
1247
|
+
const args = [t2.stringLiteral(key)];
|
|
1248
|
+
if (templateInfo && templateInfo.placeholders.length > 0) {
|
|
1249
|
+
args.push(
|
|
1250
|
+
buildValuesObject(
|
|
1251
|
+
templateInfo.expressions,
|
|
1252
|
+
templateInfo.placeholders
|
|
1253
|
+
)
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
path.node.value = t2.jsxExpressionContainer(
|
|
1257
|
+
t2.callExpression(t2.identifier("t"), args)
|
|
1258
|
+
);
|
|
1021
1259
|
stringsWrapped++;
|
|
1022
1260
|
const compName = getComponentName(path);
|
|
1023
1261
|
if (compName) componentsNeedingT.add(compName);
|
|
1024
1262
|
},
|
|
1025
1263
|
ObjectProperty(path) {
|
|
1026
1264
|
if (!isInsideFunction(path)) return;
|
|
1265
|
+
const compName = getComponentName(path);
|
|
1266
|
+
if (!compName) return;
|
|
1027
1267
|
const keyNode = path.node.key;
|
|
1028
1268
|
if (keyNode.type !== "Identifier" && keyNode.type !== "StringLiteral")
|
|
1029
1269
|
return;
|
|
1030
1270
|
const propName = keyNode.type === "Identifier" ? keyNode.name : keyNode.value;
|
|
1031
1271
|
if (!isContentProperty(propName)) return;
|
|
1032
1272
|
const valueNode = path.node.value;
|
|
1033
|
-
if (valueNode.type
|
|
1034
|
-
|
|
1273
|
+
if (valueNode.type === "ConditionalExpression") {
|
|
1274
|
+
const result = transformConditionalBranch(valueNode, textToKey);
|
|
1275
|
+
if (result.count > 0) {
|
|
1276
|
+
path.node.value = result.node;
|
|
1277
|
+
stringsWrapped += result.count;
|
|
1278
|
+
componentsNeedingT.add(compName);
|
|
1279
|
+
}
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
let text2;
|
|
1283
|
+
let templateInfo;
|
|
1284
|
+
if (valueNode.type === "StringLiteral") {
|
|
1285
|
+
text2 = valueNode.value;
|
|
1286
|
+
} else if (valueNode.type === "TemplateLiteral") {
|
|
1287
|
+
const info = buildTemplateLiteralText(
|
|
1288
|
+
valueNode.quasis,
|
|
1289
|
+
valueNode.expressions
|
|
1290
|
+
);
|
|
1291
|
+
if (info) {
|
|
1292
|
+
text2 = info.text;
|
|
1293
|
+
templateInfo = {
|
|
1294
|
+
placeholders: info.placeholders,
|
|
1295
|
+
expressions: valueNode.expressions
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1035
1299
|
if (!text2 || !(text2 in textToKey)) return;
|
|
1036
1300
|
const key = textToKey[text2];
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1301
|
+
const args = [t2.stringLiteral(key)];
|
|
1302
|
+
if (templateInfo && templateInfo.placeholders.length > 0) {
|
|
1303
|
+
args.push(
|
|
1304
|
+
buildValuesObject(
|
|
1305
|
+
templateInfo.expressions,
|
|
1306
|
+
templateInfo.placeholders
|
|
1307
|
+
)
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
path.node.value = t2.callExpression(t2.identifier("t"), args);
|
|
1040
1311
|
stringsWrapped++;
|
|
1041
|
-
|
|
1042
|
-
if (compName) componentsNeedingT.add(compName);
|
|
1312
|
+
componentsNeedingT.add(compName);
|
|
1043
1313
|
}
|
|
1044
1314
|
});
|
|
1045
1315
|
if (stringsWrapped === 0) {
|
|
1046
1316
|
return { code: generate(ast).code, stringsWrapped: 0, modified: false };
|
|
1047
1317
|
}
|
|
1048
|
-
if (
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1318
|
+
if (isClient) {
|
|
1319
|
+
if (!hasUseTranslationsImport(ast, importSource)) {
|
|
1320
|
+
const importDecl = t2.importDeclaration(
|
|
1321
|
+
[
|
|
1322
|
+
t2.importSpecifier(
|
|
1323
|
+
t2.identifier("useTranslations"),
|
|
1324
|
+
t2.identifier("useTranslations")
|
|
1325
|
+
)
|
|
1326
|
+
],
|
|
1327
|
+
t2.stringLiteral(importSource)
|
|
1328
|
+
);
|
|
1329
|
+
const lastImportIndex = findLastImportIndex(ast);
|
|
1330
|
+
if (lastImportIndex >= 0) {
|
|
1331
|
+
ast.program.body.splice(lastImportIndex + 1, 0, importDecl);
|
|
1332
|
+
} else {
|
|
1333
|
+
ast.program.body.unshift(importDecl);
|
|
1334
|
+
}
|
|
1063
1335
|
}
|
|
1064
|
-
}
|
|
1065
|
-
if (!hasUseTranslationsCall(ast)) {
|
|
1066
1336
|
traverse2(ast, {
|
|
1067
1337
|
FunctionDeclaration(path) {
|
|
1068
1338
|
const name = path.node.id?.name;
|
|
@@ -1083,6 +1353,47 @@ function transform(ast, textToKey, options = {}) {
|
|
|
1083
1353
|
},
|
|
1084
1354
|
noScope: true
|
|
1085
1355
|
});
|
|
1356
|
+
} else {
|
|
1357
|
+
const serverSource = `${importSource}/server`;
|
|
1358
|
+
if (!hasGetTranslationsImport(ast, serverSource)) {
|
|
1359
|
+
const importDecl = t2.importDeclaration(
|
|
1360
|
+
[
|
|
1361
|
+
t2.importSpecifier(
|
|
1362
|
+
t2.identifier("getTranslations"),
|
|
1363
|
+
t2.identifier("getTranslations")
|
|
1364
|
+
)
|
|
1365
|
+
],
|
|
1366
|
+
t2.stringLiteral(serverSource)
|
|
1367
|
+
);
|
|
1368
|
+
const lastImportIndex = findLastImportIndex(ast);
|
|
1369
|
+
if (lastImportIndex >= 0) {
|
|
1370
|
+
ast.program.body.splice(lastImportIndex + 1, 0, importDecl);
|
|
1371
|
+
} else {
|
|
1372
|
+
ast.program.body.unshift(importDecl);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
traverse2(ast, {
|
|
1376
|
+
FunctionDeclaration(path) {
|
|
1377
|
+
const name = path.node.id?.name;
|
|
1378
|
+
if (!name || !componentsNeedingT.has(name)) return;
|
|
1379
|
+
path.node.async = true;
|
|
1380
|
+
injectAsyncTIntoBlock(path.node.body);
|
|
1381
|
+
},
|
|
1382
|
+
VariableDeclarator(path) {
|
|
1383
|
+
if (path.node.id.type !== "Identifier") return;
|
|
1384
|
+
const name = path.node.id.name;
|
|
1385
|
+
if (!componentsNeedingT.has(name)) return;
|
|
1386
|
+
const init = path.node.init;
|
|
1387
|
+
if (!init) return;
|
|
1388
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
|
|
1389
|
+
init.async = true;
|
|
1390
|
+
if (init.body.type === "BlockStatement") {
|
|
1391
|
+
injectAsyncTIntoBlock(init.body);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
noScope: true
|
|
1396
|
+
});
|
|
1086
1397
|
}
|
|
1087
1398
|
const output = generate(ast, { retainLines: false });
|
|
1088
1399
|
return { code: output.code, stringsWrapped, modified: true };
|
|
@@ -1095,20 +1406,38 @@ function injectTDeclaration(path) {
|
|
|
1095
1406
|
function injectTIntoBlock(block) {
|
|
1096
1407
|
for (const stmt of block.body) {
|
|
1097
1408
|
if (stmt.type === "VariableDeclaration" && stmt.declarations.some(
|
|
1098
|
-
(d) => d.id.type === "Identifier" && d.id.name === "t" && d.init?.type === "CallExpression" && d.init.callee.type === "Identifier" && d.init.callee.name === "useTranslations"
|
|
1409
|
+
(d) => d.id.type === "Identifier" && d.id.name === "t" && (d.init?.type === "CallExpression" && d.init.callee.type === "Identifier" && (d.init.callee.name === "useTranslations" || d.init.callee.name === "getTranslations") || d.init?.type === "AwaitExpression" && d.init.argument.type === "CallExpression" && d.init.argument.callee.type === "Identifier" && d.init.argument.callee.name === "getTranslations")
|
|
1410
|
+
)) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
const tDecl = t2.variableDeclaration("const", [
|
|
1415
|
+
t2.variableDeclarator(
|
|
1416
|
+
t2.identifier("t"),
|
|
1417
|
+
t2.callExpression(t2.identifier("useTranslations"), [])
|
|
1418
|
+
)
|
|
1419
|
+
]);
|
|
1420
|
+
block.body.unshift(tDecl);
|
|
1421
|
+
}
|
|
1422
|
+
function injectAsyncTIntoBlock(block) {
|
|
1423
|
+
for (const stmt of block.body) {
|
|
1424
|
+
if (stmt.type === "VariableDeclaration" && stmt.declarations.some(
|
|
1425
|
+
(d) => d.id.type === "Identifier" && d.id.name === "t" && (d.init?.type === "AwaitExpression" && d.init.argument.type === "CallExpression" && d.init.argument.callee.type === "Identifier" && d.init.argument.callee.name === "getTranslations" || d.init?.type === "CallExpression" && d.init.callee.type === "Identifier" && d.init.callee.name === "getTranslations")
|
|
1099
1426
|
)) {
|
|
1100
1427
|
return;
|
|
1101
1428
|
}
|
|
1102
1429
|
}
|
|
1103
|
-
const tDecl =
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1430
|
+
const tDecl = t2.variableDeclaration("const", [
|
|
1431
|
+
t2.variableDeclarator(
|
|
1432
|
+
t2.identifier("t"),
|
|
1433
|
+
t2.awaitExpression(
|
|
1434
|
+
t2.callExpression(t2.identifier("getTranslations"), [])
|
|
1435
|
+
)
|
|
1107
1436
|
)
|
|
1108
1437
|
]);
|
|
1109
1438
|
block.body.unshift(tDecl);
|
|
1110
1439
|
}
|
|
1111
|
-
function
|
|
1440
|
+
function detectClientFile(ast) {
|
|
1112
1441
|
if (ast.program.directives) {
|
|
1113
1442
|
for (const directive of ast.program.directives) {
|
|
1114
1443
|
if (directive.value?.value === "use client") {
|
|
@@ -1121,7 +1450,18 @@ function isClientFile(ast) {
|
|
|
1121
1450
|
return true;
|
|
1122
1451
|
}
|
|
1123
1452
|
}
|
|
1124
|
-
|
|
1453
|
+
let usesHooks = false;
|
|
1454
|
+
traverse2(ast, {
|
|
1455
|
+
CallExpression(path) {
|
|
1456
|
+
if (usesHooks) return;
|
|
1457
|
+
const callee = path.node.callee;
|
|
1458
|
+
if (callee.type === "Identifier" && /^use[A-Z]/.test(callee.name)) {
|
|
1459
|
+
usesHooks = true;
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
noScope: true
|
|
1463
|
+
});
|
|
1464
|
+
return usesHooks;
|
|
1125
1465
|
}
|
|
1126
1466
|
function hasInlineImport(ast, componentPath) {
|
|
1127
1467
|
let hasT = false;
|
|
@@ -1141,27 +1481,46 @@ function hasInlineImport(ast, componentPath) {
|
|
|
1141
1481
|
}
|
|
1142
1482
|
return { hasT, hasHook };
|
|
1143
1483
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1484
|
+
function normalizeInlineImports(ast, componentPath, isClient) {
|
|
1485
|
+
const validSources = /* @__PURE__ */ new Set([
|
|
1486
|
+
componentPath,
|
|
1487
|
+
`${componentPath}-server`,
|
|
1488
|
+
`${componentPath}/t-server`
|
|
1489
|
+
]);
|
|
1490
|
+
const desiredSource = isClient ? componentPath : `${componentPath}-server`;
|
|
1491
|
+
let changed = false;
|
|
1492
|
+
for (const node of ast.program.body) {
|
|
1493
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
1494
|
+
if (!validSources.has(node.source.value)) continue;
|
|
1495
|
+
if (node.source.value !== desiredSource) {
|
|
1496
|
+
node.source.value = desiredSource;
|
|
1497
|
+
changed = true;
|
|
1498
|
+
}
|
|
1499
|
+
for (const spec of node.specifiers) {
|
|
1500
|
+
if (spec.type !== "ImportSpecifier" || spec.imported.type !== "Identifier") {
|
|
1501
|
+
continue;
|
|
1152
1502
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1503
|
+
if (isClient && spec.imported.name === "createT") {
|
|
1504
|
+
spec.imported = t2.identifier("useT");
|
|
1505
|
+
changed = true;
|
|
1506
|
+
}
|
|
1507
|
+
if (!isClient && spec.imported.name === "useT") {
|
|
1508
|
+
spec.imported = t2.identifier("createT");
|
|
1509
|
+
changed = true;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return changed;
|
|
1157
1514
|
}
|
|
1158
1515
|
function transformInline(ast, textToKey, options) {
|
|
1159
1516
|
const componentPath = options.componentPath ?? "@/components/t";
|
|
1160
|
-
const isClient =
|
|
1517
|
+
const isClient = options.forceClient || detectClientFile(ast);
|
|
1161
1518
|
let stringsWrapped = 0;
|
|
1162
1519
|
const componentsNeedingT = /* @__PURE__ */ new Set();
|
|
1163
1520
|
let needsTComponent = false;
|
|
1164
1521
|
let repaired = false;
|
|
1522
|
+
let boundaryRepaired = false;
|
|
1523
|
+
boundaryRepaired = normalizeInlineImports(ast, componentPath, isClient);
|
|
1165
1524
|
if (!isClient) {
|
|
1166
1525
|
traverse2(ast, {
|
|
1167
1526
|
CallExpression(path) {
|
|
@@ -1190,19 +1549,15 @@ function transformInline(ast, textToKey, options) {
|
|
|
1190
1549
|
}
|
|
1191
1550
|
const key = textToKey[text2];
|
|
1192
1551
|
needsTComponent = true;
|
|
1193
|
-
const tElement =
|
|
1194
|
-
|
|
1195
|
-
|
|
1552
|
+
const tElement = t2.jsxElement(
|
|
1553
|
+
t2.jsxOpeningElement(t2.jsxIdentifier("T"), [
|
|
1554
|
+
t2.jsxAttribute(t2.jsxIdentifier("id"), t2.stringLiteral(key))
|
|
1196
1555
|
]),
|
|
1197
|
-
|
|
1198
|
-
[
|
|
1556
|
+
t2.jsxClosingElement(t2.jsxIdentifier("T")),
|
|
1557
|
+
[t2.jsxText(text2)],
|
|
1199
1558
|
false
|
|
1200
1559
|
);
|
|
1201
|
-
|
|
1202
|
-
if (child.type === "JSXText") return child.value.trim().length > 0;
|
|
1203
|
-
return true;
|
|
1204
|
-
});
|
|
1205
|
-
if (siblings.length === 1) {
|
|
1560
|
+
if (!hasSubstantialSiblings(parent)) {
|
|
1206
1561
|
path.replaceWith(tElement);
|
|
1207
1562
|
} else {
|
|
1208
1563
|
const raw = path.node.value;
|
|
@@ -1210,35 +1565,101 @@ function transformInline(ast, textToKey, options) {
|
|
|
1210
1565
|
const hasTrailing = raw !== raw.trimEnd();
|
|
1211
1566
|
const nodes = [];
|
|
1212
1567
|
if (hasLeading) {
|
|
1213
|
-
nodes.push(
|
|
1568
|
+
nodes.push(t2.jsxExpressionContainer(t2.stringLiteral(" ")));
|
|
1214
1569
|
}
|
|
1215
1570
|
nodes.push(tElement);
|
|
1216
1571
|
if (hasTrailing) {
|
|
1217
|
-
nodes.push(
|
|
1572
|
+
nodes.push(t2.jsxExpressionContainer(t2.stringLiteral(" ")));
|
|
1218
1573
|
}
|
|
1219
1574
|
path.replaceWithMultiple(nodes);
|
|
1220
1575
|
}
|
|
1221
1576
|
stringsWrapped++;
|
|
1222
1577
|
},
|
|
1578
|
+
JSXExpressionContainer(path) {
|
|
1579
|
+
const expr = path.node.expression;
|
|
1580
|
+
if (path.parent.type === "JSXAttribute") return;
|
|
1581
|
+
if (expr.type === "ConditionalExpression") {
|
|
1582
|
+
const result = transformConditionalBranchInline(expr, textToKey);
|
|
1583
|
+
if (result.count > 0) {
|
|
1584
|
+
path.node.expression = result.node;
|
|
1585
|
+
stringsWrapped += result.count;
|
|
1586
|
+
const compName2 = getComponentName(path);
|
|
1587
|
+
if (compName2) componentsNeedingT.add(compName2);
|
|
1588
|
+
}
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (expr.type !== "TemplateLiteral") return;
|
|
1592
|
+
const info = buildTemplateLiteralText(expr.quasis, expr.expressions);
|
|
1593
|
+
if (!info || !(info.text in textToKey)) return;
|
|
1594
|
+
const key = textToKey[info.text];
|
|
1595
|
+
const args = [
|
|
1596
|
+
t2.stringLiteral(info.text),
|
|
1597
|
+
t2.stringLiteral(key)
|
|
1598
|
+
];
|
|
1599
|
+
if (info.placeholders.length > 0) {
|
|
1600
|
+
args.push(buildValuesObject(expr.expressions, info.placeholders));
|
|
1601
|
+
}
|
|
1602
|
+
path.node.expression = t2.callExpression(t2.identifier("t"), args);
|
|
1603
|
+
stringsWrapped++;
|
|
1604
|
+
const compName = getComponentName(path);
|
|
1605
|
+
if (compName) componentsNeedingT.add(compName);
|
|
1606
|
+
},
|
|
1223
1607
|
JSXAttribute(path) {
|
|
1224
1608
|
const value = path.node.value;
|
|
1225
1609
|
if (!value) return;
|
|
1610
|
+
if (value.type === "JSXExpressionContainer" && value.expression.type === "ConditionalExpression") {
|
|
1611
|
+
const result = transformConditionalBranchInline(
|
|
1612
|
+
value.expression,
|
|
1613
|
+
textToKey
|
|
1614
|
+
);
|
|
1615
|
+
if (result.count > 0) {
|
|
1616
|
+
path.node.value = t2.jsxExpressionContainer(result.node);
|
|
1617
|
+
stringsWrapped += result.count;
|
|
1618
|
+
const compName2 = getComponentName(path);
|
|
1619
|
+
if (compName2) componentsNeedingT.add(compName2);
|
|
1620
|
+
}
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1226
1623
|
let text2;
|
|
1624
|
+
let templateInfo;
|
|
1227
1625
|
if (value.type === "StringLiteral") {
|
|
1228
1626
|
text2 = value.value;
|
|
1229
|
-
} else if (value.type === "JSXExpressionContainer"
|
|
1230
|
-
|
|
1627
|
+
} else if (value.type === "JSXExpressionContainer") {
|
|
1628
|
+
if (value.expression.type === "StringLiteral") {
|
|
1629
|
+
text2 = value.expression.value;
|
|
1630
|
+
} else if (value.expression.type === "TemplateLiteral") {
|
|
1631
|
+
const info = buildTemplateLiteralText(
|
|
1632
|
+
value.expression.quasis,
|
|
1633
|
+
value.expression.expressions
|
|
1634
|
+
);
|
|
1635
|
+
if (info) {
|
|
1636
|
+
text2 = info.text;
|
|
1637
|
+
templateInfo = {
|
|
1638
|
+
placeholders: info.placeholders,
|
|
1639
|
+
expressions: value.expression.expressions
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1231
1643
|
}
|
|
1232
1644
|
if (!text2 || !(text2 in textToKey)) return;
|
|
1233
1645
|
if (value.type === "JSXExpressionContainer" && value.expression.type === "CallExpression" && value.expression.callee.type === "Identifier" && value.expression.callee.name === "t") {
|
|
1234
1646
|
return;
|
|
1235
1647
|
}
|
|
1236
1648
|
const key = textToKey[text2];
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1649
|
+
const args = [
|
|
1650
|
+
t2.stringLiteral(text2),
|
|
1651
|
+
t2.stringLiteral(key)
|
|
1652
|
+
];
|
|
1653
|
+
if (templateInfo && templateInfo.placeholders.length > 0) {
|
|
1654
|
+
args.push(
|
|
1655
|
+
buildValuesObject(
|
|
1656
|
+
templateInfo.expressions,
|
|
1657
|
+
templateInfo.placeholders
|
|
1658
|
+
)
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
path.node.value = t2.jsxExpressionContainer(
|
|
1662
|
+
t2.callExpression(t2.identifier("t"), args)
|
|
1242
1663
|
);
|
|
1243
1664
|
stringsWrapped++;
|
|
1244
1665
|
const compName = getComponentName(path);
|
|
@@ -1246,29 +1667,63 @@ function transformInline(ast, textToKey, options) {
|
|
|
1246
1667
|
},
|
|
1247
1668
|
ObjectProperty(path) {
|
|
1248
1669
|
if (!isInsideFunction(path)) return;
|
|
1670
|
+
const compName = getComponentName(path);
|
|
1671
|
+
if (!compName) return;
|
|
1249
1672
|
const keyNode = path.node.key;
|
|
1250
1673
|
if (keyNode.type !== "Identifier" && keyNode.type !== "StringLiteral")
|
|
1251
1674
|
return;
|
|
1252
1675
|
const propName = keyNode.type === "Identifier" ? keyNode.name : keyNode.value;
|
|
1253
1676
|
if (!isContentProperty(propName)) return;
|
|
1254
1677
|
const valueNode = path.node.value;
|
|
1255
|
-
if (valueNode.type
|
|
1256
|
-
|
|
1678
|
+
if (valueNode.type === "ConditionalExpression") {
|
|
1679
|
+
const result = transformConditionalBranchInline(valueNode, textToKey);
|
|
1680
|
+
if (result.count > 0) {
|
|
1681
|
+
path.node.value = result.node;
|
|
1682
|
+
stringsWrapped += result.count;
|
|
1683
|
+
componentsNeedingT.add(compName);
|
|
1684
|
+
}
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
let text2;
|
|
1688
|
+
let templateInfo;
|
|
1689
|
+
if (valueNode.type === "StringLiteral") {
|
|
1690
|
+
text2 = valueNode.value;
|
|
1691
|
+
} else if (valueNode.type === "TemplateLiteral") {
|
|
1692
|
+
const info = buildTemplateLiteralText(
|
|
1693
|
+
valueNode.quasis,
|
|
1694
|
+
valueNode.expressions
|
|
1695
|
+
);
|
|
1696
|
+
if (info) {
|
|
1697
|
+
text2 = info.text;
|
|
1698
|
+
templateInfo = {
|
|
1699
|
+
placeholders: info.placeholders,
|
|
1700
|
+
expressions: valueNode.expressions
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1257
1704
|
if (!text2 || !(text2 in textToKey)) return;
|
|
1258
1705
|
const key = textToKey[text2];
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
]
|
|
1706
|
+
const args = [
|
|
1707
|
+
t2.stringLiteral(text2),
|
|
1708
|
+
t2.stringLiteral(key)
|
|
1709
|
+
];
|
|
1710
|
+
if (templateInfo && templateInfo.placeholders.length > 0) {
|
|
1711
|
+
args.push(
|
|
1712
|
+
buildValuesObject(
|
|
1713
|
+
templateInfo.expressions,
|
|
1714
|
+
templateInfo.placeholders
|
|
1715
|
+
)
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
path.node.value = t2.callExpression(t2.identifier("t"), args);
|
|
1263
1719
|
stringsWrapped++;
|
|
1264
|
-
|
|
1265
|
-
if (compName) componentsNeedingT.add(compName);
|
|
1720
|
+
componentsNeedingT.add(compName);
|
|
1266
1721
|
}
|
|
1267
1722
|
});
|
|
1268
|
-
if (stringsWrapped === 0 && !repaired) {
|
|
1723
|
+
if (stringsWrapped === 0 && !repaired && !boundaryRepaired) {
|
|
1269
1724
|
return { code: generate(ast).code, stringsWrapped: 0, modified: false };
|
|
1270
1725
|
}
|
|
1271
|
-
if (stringsWrapped === 0 && repaired) {
|
|
1726
|
+
if (stringsWrapped === 0 && (repaired || boundaryRepaired)) {
|
|
1272
1727
|
const output2 = generate(ast, { retainLines: false });
|
|
1273
1728
|
return { code: output2.code, stringsWrapped: 0, modified: true };
|
|
1274
1729
|
}
|
|
@@ -1278,11 +1733,11 @@ function transformInline(ast, textToKey, options) {
|
|
|
1278
1733
|
const existing = hasInlineImport(ast, componentPath);
|
|
1279
1734
|
const specifiers = [];
|
|
1280
1735
|
if (needsTComponent && !existing.hasT) {
|
|
1281
|
-
specifiers.push(
|
|
1736
|
+
specifiers.push(t2.importSpecifier(t2.identifier("T"), t2.identifier("T")));
|
|
1282
1737
|
}
|
|
1283
1738
|
if (needsHook && !existing.hasHook) {
|
|
1284
1739
|
specifiers.push(
|
|
1285
|
-
|
|
1740
|
+
t2.importSpecifier(t2.identifier(hookName), t2.identifier(hookName))
|
|
1286
1741
|
);
|
|
1287
1742
|
}
|
|
1288
1743
|
if (specifiers.length > 0) {
|
|
@@ -1296,9 +1751,9 @@ function transformInline(ast, textToKey, options) {
|
|
|
1296
1751
|
}
|
|
1297
1752
|
}
|
|
1298
1753
|
if (!appended) {
|
|
1299
|
-
const importDecl =
|
|
1754
|
+
const importDecl = t2.importDeclaration(
|
|
1300
1755
|
specifiers,
|
|
1301
|
-
|
|
1756
|
+
t2.stringLiteral(importPath)
|
|
1302
1757
|
);
|
|
1303
1758
|
const lastImportIndex = findLastImportIndex(ast);
|
|
1304
1759
|
if (lastImportIndex >= 0) {
|
|
@@ -1312,8 +1767,8 @@ function transformInline(ast, textToKey, options) {
|
|
|
1312
1767
|
}
|
|
1313
1768
|
}
|
|
1314
1769
|
}
|
|
1315
|
-
if (needsHook
|
|
1316
|
-
const hookCall = isClient ?
|
|
1770
|
+
if (needsHook) {
|
|
1771
|
+
const hookCall = isClient ? t2.callExpression(t2.identifier("useT"), []) : t2.callExpression(t2.identifier("createT"), []);
|
|
1317
1772
|
traverse2(ast, {
|
|
1318
1773
|
FunctionDeclaration(path) {
|
|
1319
1774
|
const name = path.node.id?.name;
|
|
@@ -1348,8 +1803,8 @@ function injectInlineHookIntoBlock(block, hookCall) {
|
|
|
1348
1803
|
return;
|
|
1349
1804
|
}
|
|
1350
1805
|
}
|
|
1351
|
-
const tDecl =
|
|
1352
|
-
|
|
1806
|
+
const tDecl = t2.variableDeclaration("const", [
|
|
1807
|
+
t2.variableDeclarator(t2.identifier("t"), hookCall)
|
|
1353
1808
|
]);
|
|
1354
1809
|
block.body.unshift(tDecl);
|
|
1355
1810
|
}
|
|
@@ -1359,63 +1814,914 @@ var init_transform = __esm({
|
|
|
1359
1814
|
"use strict";
|
|
1360
1815
|
init_filters();
|
|
1361
1816
|
init_ast_helpers();
|
|
1817
|
+
init_template_literal();
|
|
1362
1818
|
init_logger();
|
|
1363
1819
|
traverse2 = resolveDefault(_traverse2);
|
|
1364
1820
|
generate = resolveDefault(_generate);
|
|
1365
1821
|
}
|
|
1366
1822
|
});
|
|
1367
1823
|
|
|
1368
|
-
// src/codegen/index.ts
|
|
1369
|
-
import {
|
|
1370
|
-
import {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1824
|
+
// src/codegen/index.ts
|
|
1825
|
+
import { dirname, extname, join as join2, resolve } from "path";
|
|
1826
|
+
import { readFile as readFile3, writeFile } from "fs/promises";
|
|
1827
|
+
import { glob as glob2 } from "tinyglobby";
|
|
1828
|
+
import pLimit3 from "p-limit";
|
|
1829
|
+
function collectRuntimeImportSources(ast) {
|
|
1830
|
+
const sources = [];
|
|
1831
|
+
for (const node of ast.program.body) {
|
|
1832
|
+
if (node.type === "ImportDeclaration") {
|
|
1833
|
+
if (node.importKind === "type") continue;
|
|
1834
|
+
const allTypeSpecifiers = node.specifiers.length > 0 && node.specifiers.every(
|
|
1835
|
+
(spec) => spec.type === "ImportSpecifier" && spec.importKind === "type"
|
|
1836
|
+
);
|
|
1837
|
+
if (allTypeSpecifiers) continue;
|
|
1838
|
+
sources.push(node.source.value);
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (node.type === "ExportNamedDeclaration" && node.source) {
|
|
1842
|
+
if (node.exportKind !== "type") {
|
|
1843
|
+
sources.push(node.source.value);
|
|
1844
|
+
}
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
if (node.type === "ExportAllDeclaration") {
|
|
1848
|
+
if (node.exportKind !== "type") {
|
|
1849
|
+
sources.push(node.source.value);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return sources;
|
|
1854
|
+
}
|
|
1855
|
+
function resolveFileCandidate(basePath, knownFiles) {
|
|
1856
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1857
|
+
const resolvedBase = resolve(basePath);
|
|
1858
|
+
const baseExt = extname(resolvedBase);
|
|
1859
|
+
candidates.add(resolvedBase);
|
|
1860
|
+
if (!baseExt) {
|
|
1861
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
1862
|
+
candidates.add(resolve(`${resolvedBase}${ext}`));
|
|
1863
|
+
candidates.add(resolve(join2(resolvedBase, `index${ext}`)));
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
if ([".js", ".jsx", ".mjs", ".cjs"].includes(baseExt)) {
|
|
1867
|
+
const noExt = resolvedBase.slice(0, -baseExt.length);
|
|
1868
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
1869
|
+
candidates.add(resolve(`${noExt}${ext}`));
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
for (const candidate of candidates) {
|
|
1873
|
+
if (knownFiles.has(candidate)) return candidate;
|
|
1874
|
+
}
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
function resolveLocalImport(importerPath, source, cwd, knownFiles) {
|
|
1878
|
+
const baseCandidates = [];
|
|
1879
|
+
if (source.startsWith(".")) {
|
|
1880
|
+
baseCandidates.push(resolve(dirname(importerPath), source));
|
|
1881
|
+
} else if (source.startsWith("@/")) {
|
|
1882
|
+
baseCandidates.push(resolve(join2(cwd, "src", source.slice(2))));
|
|
1883
|
+
baseCandidates.push(resolve(join2(cwd, source.slice(2))));
|
|
1884
|
+
} else if (source.startsWith("~/")) {
|
|
1885
|
+
baseCandidates.push(resolve(join2(cwd, source.slice(2))));
|
|
1886
|
+
} else if (source.startsWith("/")) {
|
|
1887
|
+
baseCandidates.push(resolve(join2(cwd, source.slice(1))));
|
|
1888
|
+
} else {
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
for (const base of baseCandidates) {
|
|
1892
|
+
const resolved = resolveFileCandidate(base, knownFiles);
|
|
1893
|
+
if (resolved) return resolved;
|
|
1894
|
+
}
|
|
1895
|
+
return null;
|
|
1896
|
+
}
|
|
1897
|
+
function buildClientGraph(entries, cwd) {
|
|
1898
|
+
const parsedEntries = entries.filter((e) => e.ast != null);
|
|
1899
|
+
const knownFiles = new Set(parsedEntries.map((e) => e.filePath));
|
|
1900
|
+
const depsByImporter = /* @__PURE__ */ new Map();
|
|
1901
|
+
for (const entry of parsedEntries) {
|
|
1902
|
+
const deps = [];
|
|
1903
|
+
const imports = collectRuntimeImportSources(entry.ast);
|
|
1904
|
+
for (const source of imports) {
|
|
1905
|
+
const dep = resolveLocalImport(entry.filePath, source, cwd, knownFiles);
|
|
1906
|
+
if (dep) deps.push(dep);
|
|
1907
|
+
}
|
|
1908
|
+
depsByImporter.set(entry.filePath, deps);
|
|
1909
|
+
}
|
|
1910
|
+
const clientReachable = /* @__PURE__ */ new Set();
|
|
1911
|
+
const queue = [];
|
|
1912
|
+
for (const entry of parsedEntries) {
|
|
1913
|
+
if (!entry.isClientRoot) continue;
|
|
1914
|
+
clientReachable.add(entry.filePath);
|
|
1915
|
+
queue.push(entry.filePath);
|
|
1916
|
+
}
|
|
1917
|
+
while (queue.length > 0) {
|
|
1918
|
+
const filePath = queue.shift();
|
|
1919
|
+
const deps = depsByImporter.get(filePath) ?? [];
|
|
1920
|
+
for (const dep of deps) {
|
|
1921
|
+
if (clientReachable.has(dep)) continue;
|
|
1922
|
+
clientReachable.add(dep);
|
|
1923
|
+
queue.push(dep);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return clientReachable;
|
|
1927
|
+
}
|
|
1928
|
+
async function codegen(options, cwd = process.cwd()) {
|
|
1929
|
+
const files = await glob2(options.include, {
|
|
1930
|
+
ignore: options.exclude ?? [],
|
|
1931
|
+
cwd,
|
|
1932
|
+
absolute: true
|
|
1933
|
+
});
|
|
1934
|
+
const transformOpts = {
|
|
1935
|
+
i18nImport: options.i18nImport,
|
|
1936
|
+
mode: options.mode,
|
|
1937
|
+
componentPath: options.componentPath
|
|
1938
|
+
};
|
|
1939
|
+
const parseLimit = pLimit3(10);
|
|
1940
|
+
const parsedEntries = await Promise.all(
|
|
1941
|
+
files.map(
|
|
1942
|
+
(filePath) => parseLimit(async () => {
|
|
1943
|
+
const code = await readFile3(filePath, "utf-8");
|
|
1944
|
+
try {
|
|
1945
|
+
const ast = parseFile(code, filePath);
|
|
1946
|
+
return {
|
|
1947
|
+
filePath,
|
|
1948
|
+
code,
|
|
1949
|
+
ast,
|
|
1950
|
+
isClientRoot: detectClientFile(ast)
|
|
1951
|
+
};
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
return {
|
|
1954
|
+
filePath,
|
|
1955
|
+
code,
|
|
1956
|
+
parseError: err instanceof Error ? err.message : String(err),
|
|
1957
|
+
isClientRoot: false
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
})
|
|
1961
|
+
)
|
|
1962
|
+
);
|
|
1963
|
+
const forceClientSet = buildClientGraph(parsedEntries, cwd);
|
|
1964
|
+
const limit = pLimit3(10);
|
|
1965
|
+
let completed = 0;
|
|
1966
|
+
const fileResults = await Promise.all(
|
|
1967
|
+
parsedEntries.map(
|
|
1968
|
+
(entry) => limit(async () => {
|
|
1969
|
+
if (!entry.ast) {
|
|
1970
|
+
logWarning(
|
|
1971
|
+
`Skipping unparseable file ${entry.filePath}: ${entry.parseError ?? "unknown parse error"}`
|
|
1972
|
+
);
|
|
1973
|
+
completed++;
|
|
1974
|
+
options.onProgress?.(completed, files.length);
|
|
1975
|
+
return { modified: false, wrapped: 0, skipped: false };
|
|
1976
|
+
}
|
|
1977
|
+
const fileTransformOpts = {
|
|
1978
|
+
...transformOpts,
|
|
1979
|
+
forceClient: forceClientSet.has(entry.filePath)
|
|
1980
|
+
};
|
|
1981
|
+
const result = transform(
|
|
1982
|
+
entry.ast,
|
|
1983
|
+
options.textToKey,
|
|
1984
|
+
fileTransformOpts
|
|
1985
|
+
);
|
|
1986
|
+
if (result.modified) {
|
|
1987
|
+
try {
|
|
1988
|
+
parseFile(result.code, entry.filePath);
|
|
1989
|
+
} catch {
|
|
1990
|
+
logWarning(
|
|
1991
|
+
`Codegen produced invalid syntax for ${entry.filePath}, file was NOT modified.`
|
|
1992
|
+
);
|
|
1993
|
+
completed++;
|
|
1994
|
+
options.onProgress?.(completed, files.length);
|
|
1995
|
+
return { modified: false, wrapped: 0, skipped: true };
|
|
1996
|
+
}
|
|
1997
|
+
await writeFile(entry.filePath, result.code, "utf-8");
|
|
1998
|
+
completed++;
|
|
1999
|
+
options.onProgress?.(completed, files.length);
|
|
2000
|
+
return {
|
|
2001
|
+
modified: true,
|
|
2002
|
+
wrapped: result.stringsWrapped,
|
|
2003
|
+
skipped: false
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
completed++;
|
|
2007
|
+
options.onProgress?.(completed, files.length);
|
|
2008
|
+
return { modified: false, wrapped: 0, skipped: false };
|
|
2009
|
+
})
|
|
2010
|
+
)
|
|
2011
|
+
);
|
|
2012
|
+
let filesModified = 0;
|
|
2013
|
+
let stringsWrapped = 0;
|
|
2014
|
+
let filesSkipped = 0;
|
|
2015
|
+
for (const r of fileResults) {
|
|
2016
|
+
if (r.modified) {
|
|
2017
|
+
filesModified++;
|
|
2018
|
+
stringsWrapped += r.wrapped;
|
|
2019
|
+
}
|
|
2020
|
+
if (r.skipped) filesSkipped++;
|
|
2021
|
+
}
|
|
2022
|
+
return {
|
|
2023
|
+
filesModified,
|
|
2024
|
+
stringsWrapped,
|
|
2025
|
+
filesProcessed: files.length,
|
|
2026
|
+
filesSkipped
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
var SOURCE_EXTENSIONS;
|
|
2030
|
+
var init_codegen = __esm({
|
|
2031
|
+
"src/codegen/index.ts"() {
|
|
2032
|
+
"use strict";
|
|
2033
|
+
init_parser();
|
|
2034
|
+
init_transform();
|
|
2035
|
+
init_logger();
|
|
2036
|
+
SOURCE_EXTENSIONS = [
|
|
2037
|
+
".ts",
|
|
2038
|
+
".tsx",
|
|
2039
|
+
".js",
|
|
2040
|
+
".jsx",
|
|
2041
|
+
".mts",
|
|
2042
|
+
".cts",
|
|
2043
|
+
".mjs",
|
|
2044
|
+
".cjs"
|
|
2045
|
+
];
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
// src/validate.ts
|
|
2050
|
+
function extractPlaceholders(text2) {
|
|
2051
|
+
const matches = text2.match(PLACEHOLDER_REGEX);
|
|
2052
|
+
if (!matches) return [];
|
|
2053
|
+
return matches.slice().sort();
|
|
2054
|
+
}
|
|
2055
|
+
function validatePlaceholders(source, translated) {
|
|
2056
|
+
const sourcePlaceholders = extractPlaceholders(source);
|
|
2057
|
+
const translatedPlaceholders = extractPlaceholders(translated);
|
|
2058
|
+
const sourceCount = /* @__PURE__ */ new Map();
|
|
2059
|
+
for (const p2 of sourcePlaceholders) {
|
|
2060
|
+
sourceCount.set(p2, (sourceCount.get(p2) ?? 0) + 1);
|
|
2061
|
+
}
|
|
2062
|
+
const translatedCount = /* @__PURE__ */ new Map();
|
|
2063
|
+
for (const p2 of translatedPlaceholders) {
|
|
2064
|
+
translatedCount.set(p2, (translatedCount.get(p2) ?? 0) + 1);
|
|
2065
|
+
}
|
|
2066
|
+
const missing = [];
|
|
2067
|
+
const extra = [];
|
|
2068
|
+
for (const [placeholder, count] of sourceCount) {
|
|
2069
|
+
const tCount = translatedCount.get(placeholder) ?? 0;
|
|
2070
|
+
for (let i = 0; i < count - tCount; i++) {
|
|
2071
|
+
missing.push(placeholder);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
for (const [placeholder, count] of translatedCount) {
|
|
2075
|
+
const sCount = sourceCount.get(placeholder) ?? 0;
|
|
2076
|
+
for (let i = 0; i < count - sCount; i++) {
|
|
2077
|
+
extra.push(placeholder);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return {
|
|
2081
|
+
valid: missing.length === 0 && extra.length === 0,
|
|
2082
|
+
missing,
|
|
2083
|
+
extra
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
function validateBatch(sourceEntries, translatedEntries) {
|
|
2087
|
+
const failures = [];
|
|
2088
|
+
for (const key of Object.keys(sourceEntries)) {
|
|
2089
|
+
const source = sourceEntries[key];
|
|
2090
|
+
const translated = translatedEntries[key];
|
|
2091
|
+
if (translated == null) continue;
|
|
2092
|
+
const result = validatePlaceholders(source, translated);
|
|
2093
|
+
if (!result.valid) {
|
|
2094
|
+
failures.push({ key, missing: result.missing, extra: result.extra });
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return {
|
|
2098
|
+
valid: failures.length === 0,
|
|
2099
|
+
failures
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
var PLACEHOLDER_REGEX;
|
|
2103
|
+
var init_validate = __esm({
|
|
2104
|
+
"src/validate.ts"() {
|
|
2105
|
+
"use strict";
|
|
2106
|
+
PLACEHOLDER_REGEX = /\{\{[\w.]+\}\}|\{[\w.]+\}|\{\d+\}|%[sd@]|%\.\d+f|<\/?[\w-]+(?:\s[^>]*)?\s*\/?>|<\/[\w-]+>/g;
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
// src/translate.ts
|
|
2111
|
+
import { generateObject as generateObject2 } from "ai";
|
|
2112
|
+
import { z as z3 } from "zod";
|
|
2113
|
+
import pLimit4 from "p-limit";
|
|
2114
|
+
function buildPrompt2(entries, sourceLocale, targetLocale, options) {
|
|
2115
|
+
const lines = [
|
|
2116
|
+
`Translate the following strings from "${sourceLocale}" to "${targetLocale}".`,
|
|
2117
|
+
"",
|
|
2118
|
+
"Rules:",
|
|
2119
|
+
"- Preserve all placeholders like {name}, {{count}}, %s, %d exactly as-is",
|
|
2120
|
+
"- Preserve HTML tags and markdown formatting",
|
|
2121
|
+
"- Do NOT translate proper nouns, brand names, or technical identifiers",
|
|
2122
|
+
"- Maintain the same level of formality and register",
|
|
2123
|
+
"- Return natural, fluent translations (not word-for-word)"
|
|
2124
|
+
];
|
|
2125
|
+
if (options?.tone) {
|
|
2126
|
+
lines.push(`- Use a ${options.tone} tone`);
|
|
2127
|
+
}
|
|
2128
|
+
if (options?.context) {
|
|
2129
|
+
lines.push(`
|
|
2130
|
+
Context: ${options.context}`);
|
|
2131
|
+
}
|
|
2132
|
+
if (options?.glossary && Object.keys(options.glossary).length > 0) {
|
|
2133
|
+
lines.push("\nGlossary (use these exact translations):");
|
|
2134
|
+
for (const [term, translation] of Object.entries(options.glossary)) {
|
|
2135
|
+
lines.push(` "${term}" \u2192 "${translation}"`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
lines.push("\nStrings to translate:");
|
|
2139
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
2140
|
+
lines.push(` "${key}": "${value}"`);
|
|
2141
|
+
}
|
|
2142
|
+
return lines.join("\n");
|
|
2143
|
+
}
|
|
2144
|
+
function buildSchema(keys) {
|
|
2145
|
+
const shape = {};
|
|
2146
|
+
for (const key of keys) {
|
|
2147
|
+
shape[key] = z3.string();
|
|
2148
|
+
}
|
|
2149
|
+
return z3.object(shape);
|
|
2150
|
+
}
|
|
2151
|
+
async function translateBatchWithRetry(input, retries) {
|
|
2152
|
+
const { model, entries, sourceLocale, targetLocale, options } = input;
|
|
2153
|
+
const keys = Object.keys(entries);
|
|
2154
|
+
const prompt = buildPrompt2(entries, sourceLocale, targetLocale, options);
|
|
2155
|
+
const schema = buildSchema(keys);
|
|
2156
|
+
const shouldValidate = options?.validatePlaceholders !== false;
|
|
2157
|
+
let lastError;
|
|
2158
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
2159
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
2160
|
+
try {
|
|
2161
|
+
const { object, usage } = await generateObject2({
|
|
2162
|
+
model,
|
|
2163
|
+
prompt,
|
|
2164
|
+
schema
|
|
2165
|
+
});
|
|
2166
|
+
totalUsage.inputTokens += usage.inputTokens ?? 0;
|
|
2167
|
+
totalUsage.outputTokens += usage.outputTokens ?? 0;
|
|
2168
|
+
if (shouldValidate) {
|
|
2169
|
+
const validation = validateBatch(entries, object);
|
|
2170
|
+
if (!validation.valid) {
|
|
2171
|
+
if (attempt < retries) {
|
|
2172
|
+
logWarning(
|
|
2173
|
+
`Placeholder mismatch in batch (attempt ${attempt + 1}/${retries + 1}), retrying...`
|
|
2174
|
+
);
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
for (const failure of validation.failures) {
|
|
2178
|
+
logWarning(
|
|
2179
|
+
`Placeholder mismatch for key "${failure.key}": missing=[${failure.missing.join(", ")}] extra=[${failure.extra.join(", ")}]`
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return { translations: object, usage: totalUsage };
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
lastError = error;
|
|
2187
|
+
if (attempt < retries) {
|
|
2188
|
+
const delay = Math.min(Math.pow(2, attempt) * 1e3, 3e4);
|
|
2189
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
throw lastError;
|
|
2194
|
+
}
|
|
2195
|
+
async function translateAll(input) {
|
|
2196
|
+
const {
|
|
2197
|
+
model,
|
|
2198
|
+
entries,
|
|
2199
|
+
sourceLocale,
|
|
2200
|
+
targetLocale,
|
|
2201
|
+
options,
|
|
2202
|
+
onBatchComplete,
|
|
2203
|
+
onProgress,
|
|
2204
|
+
onUsage
|
|
2205
|
+
} = input;
|
|
2206
|
+
const keys = Object.keys(entries);
|
|
2207
|
+
if (keys.length === 0) return {};
|
|
2208
|
+
const batchSize = options?.batchSize ?? 50;
|
|
2209
|
+
const concurrency = options?.concurrency ?? 3;
|
|
2210
|
+
const retries = options?.retries ?? 2;
|
|
2211
|
+
const limit = pLimit4(concurrency);
|
|
2212
|
+
const batches = [];
|
|
2213
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
|
2214
|
+
const batchKeys = keys.slice(i, i + batchSize);
|
|
2215
|
+
const batch = {};
|
|
2216
|
+
for (const key of batchKeys) {
|
|
2217
|
+
batch[key] = entries[key];
|
|
2218
|
+
}
|
|
2219
|
+
batches.push(batch);
|
|
2220
|
+
}
|
|
2221
|
+
const results = {};
|
|
2222
|
+
let completedKeys = 0;
|
|
2223
|
+
let totalInputTokens = 0;
|
|
2224
|
+
let totalOutputTokens = 0;
|
|
2225
|
+
await Promise.all(
|
|
2226
|
+
batches.map(
|
|
2227
|
+
(batch) => limit(async () => {
|
|
2228
|
+
const { translations, usage } = await translateBatchWithRetry(
|
|
2229
|
+
{ model, entries: batch, sourceLocale, targetLocale, options },
|
|
2230
|
+
retries
|
|
2231
|
+
);
|
|
2232
|
+
Object.assign(results, translations);
|
|
2233
|
+
totalInputTokens += usage.inputTokens;
|
|
2234
|
+
totalOutputTokens += usage.outputTokens;
|
|
2235
|
+
completedKeys += Object.keys(batch).length;
|
|
2236
|
+
onProgress?.(completedKeys, keys.length);
|
|
2237
|
+
onBatchComplete?.(translations);
|
|
2238
|
+
})
|
|
2239
|
+
)
|
|
2240
|
+
);
|
|
2241
|
+
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
|
2242
|
+
onUsage?.({ inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
|
|
2243
|
+
}
|
|
2244
|
+
return results;
|
|
2245
|
+
}
|
|
2246
|
+
var init_translate = __esm({
|
|
2247
|
+
"src/translate.ts"() {
|
|
2248
|
+
"use strict";
|
|
2249
|
+
init_validate();
|
|
2250
|
+
init_logger();
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
// src/writer.ts
|
|
2255
|
+
import { writeFile as writeFile2, mkdir } from "fs/promises";
|
|
2256
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
2257
|
+
async function writeTranslation(filePath, flatEntries, options) {
|
|
2258
|
+
await mkdir(dirname2(filePath), { recursive: true });
|
|
2259
|
+
const data = options?.flat ? flatEntries : unflatten(flatEntries);
|
|
2260
|
+
const content = JSON.stringify(data, null, 2) + "\n";
|
|
2261
|
+
await writeFile2(filePath, content, "utf-8");
|
|
2262
|
+
}
|
|
2263
|
+
async function writeLockFile(messagesDir, sourceFlat, existingLock, translatedKeys) {
|
|
2264
|
+
const lock = { ...existingLock };
|
|
2265
|
+
for (const key of translatedKeys) {
|
|
2266
|
+
if (key in sourceFlat) {
|
|
2267
|
+
lock[key] = hashValue(sourceFlat[key]);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
for (const key of Object.keys(lock)) {
|
|
2271
|
+
if (!(key in sourceFlat)) {
|
|
2272
|
+
delete lock[key];
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const lockPath = join3(messagesDir, ".translate-lock.json");
|
|
2276
|
+
await mkdir(dirname2(lockPath), { recursive: true });
|
|
2277
|
+
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
2278
|
+
await writeFile2(lockPath, content, "utf-8");
|
|
2279
|
+
}
|
|
2280
|
+
var init_writer = __esm({
|
|
2281
|
+
"src/writer.ts"() {
|
|
2282
|
+
"use strict";
|
|
2283
|
+
init_flatten();
|
|
2284
|
+
init_diff();
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
// src/pipeline.ts
|
|
2289
|
+
import { join as join4 } from "path";
|
|
2290
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
2291
|
+
async function loadMapFile(messagesDir) {
|
|
2292
|
+
const mapPath = join4(messagesDir, ".translate-map.json");
|
|
2293
|
+
let content;
|
|
2294
|
+
try {
|
|
2295
|
+
content = await readFile4(mapPath, "utf-8");
|
|
2296
|
+
} catch {
|
|
2297
|
+
return {};
|
|
2298
|
+
}
|
|
2299
|
+
try {
|
|
2300
|
+
return JSON.parse(content);
|
|
2301
|
+
} catch {
|
|
2302
|
+
logWarning(
|
|
2303
|
+
`.translate-map.json is corrupted (invalid JSON). Starting fresh.`
|
|
2304
|
+
);
|
|
2305
|
+
return {};
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async function writeMapFile(messagesDir, map) {
|
|
2309
|
+
const mapPath = join4(messagesDir, ".translate-map.json");
|
|
2310
|
+
await mkdir2(messagesDir, { recursive: true });
|
|
2311
|
+
const content = JSON.stringify(map, null, 2) + "\n";
|
|
2312
|
+
await writeFile3(mapPath, content, "utf-8");
|
|
2313
|
+
}
|
|
2314
|
+
async function runScanStep(input) {
|
|
2315
|
+
const { config, cwd, callbacks } = input;
|
|
2316
|
+
const mode = config.mode ?? "keys";
|
|
2317
|
+
const result = await scan(config.scan, cwd, {
|
|
2318
|
+
onProgress: callbacks?.onScanProgress
|
|
2319
|
+
});
|
|
2320
|
+
const bareStrings = result.strings.filter((s) => {
|
|
2321
|
+
if (s.type === "t-call") return false;
|
|
2322
|
+
if (s.type === "T-component" && s.id) return false;
|
|
2323
|
+
return true;
|
|
2324
|
+
});
|
|
2325
|
+
const existingMap = await loadMapFile(config.messagesDir);
|
|
2326
|
+
if (mode === "inline") {
|
|
2327
|
+
const existingTComponents = result.strings.filter(
|
|
2328
|
+
(s) => s.type === "T-component" && s.id
|
|
2329
|
+
);
|
|
2330
|
+
for (const tc of existingTComponents) {
|
|
2331
|
+
if (tc.id && !(tc.text in existingMap)) {
|
|
2332
|
+
existingMap[tc.text] = tc.id;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
const existingInlineTCalls = result.strings.filter(
|
|
2336
|
+
(s) => s.type === "t-call" && s.id
|
|
2337
|
+
);
|
|
2338
|
+
for (const tc of existingInlineTCalls) {
|
|
2339
|
+
if (tc.id && !(tc.text in existingMap)) {
|
|
2340
|
+
existingMap[tc.text] = tc.id;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
const allTexts = new Set(result.strings.map((s) => s.text));
|
|
2345
|
+
const textToKey = await generateSemanticKeys({
|
|
2346
|
+
model: config.model,
|
|
2347
|
+
strings: bareStrings,
|
|
2348
|
+
existingMap,
|
|
2349
|
+
allTexts,
|
|
2350
|
+
batchSize: config.translation?.batchSize ?? 50,
|
|
2351
|
+
concurrency: config.translation?.concurrency ?? 3,
|
|
2352
|
+
retries: config.translation?.retries ?? 2,
|
|
2353
|
+
onProgress: callbacks?.onKeygenProgress,
|
|
2354
|
+
onUsage: callbacks?.onUsage
|
|
2355
|
+
});
|
|
2356
|
+
await writeMapFile(config.messagesDir, textToKey);
|
|
2357
|
+
const sourceFlat = {};
|
|
2358
|
+
for (const [text2, key] of Object.entries(textToKey)) {
|
|
2359
|
+
sourceFlat[key] = text2;
|
|
2360
|
+
}
|
|
2361
|
+
if (mode !== "inline") {
|
|
2362
|
+
const sourceFile = join4(
|
|
2363
|
+
config.messagesDir,
|
|
2364
|
+
`${config.sourceLocale}.json`
|
|
2365
|
+
);
|
|
2366
|
+
await mkdir2(config.messagesDir, { recursive: true });
|
|
2367
|
+
const nested = unflatten(sourceFlat);
|
|
2368
|
+
const content = JSON.stringify(nested, null, 2) + "\n";
|
|
2369
|
+
await writeFile3(sourceFile, content, "utf-8");
|
|
2370
|
+
}
|
|
2371
|
+
return {
|
|
2372
|
+
textToKey,
|
|
2373
|
+
sourceFlat,
|
|
2374
|
+
bareStringCount: bareStrings.length,
|
|
2375
|
+
fileCount: result.fileCount
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
async function runCodegenStep(input) {
|
|
2379
|
+
const { config, cwd, callbacks } = input;
|
|
2380
|
+
const mode = config.mode ?? "keys";
|
|
2381
|
+
let textToKey = input.textToKey;
|
|
2382
|
+
if (!textToKey) {
|
|
2383
|
+
textToKey = await loadMapFile(config.messagesDir);
|
|
2384
|
+
if (Object.keys(textToKey).length === 0) {
|
|
2385
|
+
throw new Error(
|
|
2386
|
+
"No .translate-map.json found. Run 'translate-kit scan' first."
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return codegen(
|
|
2391
|
+
{
|
|
2392
|
+
include: config.scan.include,
|
|
2393
|
+
exclude: config.scan.exclude,
|
|
2394
|
+
textToKey,
|
|
2395
|
+
i18nImport: config.scan.i18nImport,
|
|
2396
|
+
mode,
|
|
2397
|
+
componentPath: config.inline?.componentPath,
|
|
2398
|
+
onProgress: callbacks?.onProgress
|
|
2399
|
+
},
|
|
2400
|
+
cwd
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
async function runTranslateStep(input) {
|
|
2404
|
+
const { config, callbacks } = input;
|
|
2405
|
+
const mode = config.mode ?? "keys";
|
|
2406
|
+
const locales = input.locales ?? config.targetLocales;
|
|
2407
|
+
let sourceFlat = input.sourceFlat;
|
|
2408
|
+
if (!sourceFlat) {
|
|
2409
|
+
if (mode === "inline") {
|
|
2410
|
+
const mapData = await loadMapFile(config.messagesDir);
|
|
2411
|
+
sourceFlat = {};
|
|
2412
|
+
for (const [text2, key] of Object.entries(mapData)) {
|
|
2413
|
+
sourceFlat[key] = text2;
|
|
2414
|
+
}
|
|
2415
|
+
} else {
|
|
2416
|
+
const sourceFile = join4(
|
|
2417
|
+
config.messagesDir,
|
|
2418
|
+
`${config.sourceLocale}.json`
|
|
2419
|
+
);
|
|
2420
|
+
const sourceRaw = await loadJsonFile(sourceFile);
|
|
2421
|
+
sourceFlat = flatten(sourceRaw);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
const localeResults = [];
|
|
2425
|
+
for (const locale of locales) {
|
|
2426
|
+
const start = Date.now();
|
|
2427
|
+
const targetFile = join4(config.messagesDir, `${locale}.json`);
|
|
2428
|
+
const targetRaw = await loadJsonFile(targetFile);
|
|
2429
|
+
const targetFlat = flatten(targetRaw);
|
|
2430
|
+
let lockData = await loadLockFile(config.messagesDir);
|
|
2431
|
+
if (input.force) {
|
|
2432
|
+
lockData = {};
|
|
2433
|
+
}
|
|
2434
|
+
const diffResult = computeDiff(sourceFlat, targetFlat, lockData);
|
|
2435
|
+
if (input.dryRun) {
|
|
2436
|
+
localeResults.push({
|
|
2437
|
+
locale,
|
|
2438
|
+
translated: 0,
|
|
2439
|
+
cached: Object.keys(diffResult.unchanged).length,
|
|
2440
|
+
removed: diffResult.removed.length,
|
|
2441
|
+
errors: 0,
|
|
2442
|
+
duration: Date.now() - start
|
|
2443
|
+
});
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
const toTranslate = { ...diffResult.added, ...diffResult.modified };
|
|
2447
|
+
let translated = {};
|
|
2448
|
+
let errors = 0;
|
|
2449
|
+
let translationFailed = false;
|
|
2450
|
+
if (Object.keys(toTranslate).length > 0) {
|
|
2451
|
+
try {
|
|
2452
|
+
translated = await translateAll({
|
|
2453
|
+
model: config.model,
|
|
2454
|
+
entries: toTranslate,
|
|
2455
|
+
sourceLocale: config.sourceLocale,
|
|
2456
|
+
targetLocale: locale,
|
|
2457
|
+
options: config.translation,
|
|
2458
|
+
onProgress: callbacks?.onLocaleProgress ? (c, t3) => callbacks.onLocaleProgress(locale, c, t3) : void 0,
|
|
2459
|
+
onUsage: callbacks?.onUsage
|
|
2460
|
+
});
|
|
2461
|
+
} catch (err) {
|
|
2462
|
+
errors = Object.keys(toTranslate).length;
|
|
2463
|
+
translationFailed = true;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (translationFailed) {
|
|
2467
|
+
localeResults.push({
|
|
2468
|
+
locale,
|
|
2469
|
+
translated: 0,
|
|
2470
|
+
cached: Object.keys(diffResult.unchanged).length,
|
|
2471
|
+
removed: 0,
|
|
2472
|
+
errors,
|
|
2473
|
+
duration: Date.now() - start
|
|
2474
|
+
});
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
const finalFlat = {
|
|
2478
|
+
...diffResult.unchanged,
|
|
2479
|
+
...translated
|
|
2480
|
+
};
|
|
2481
|
+
await writeTranslation(targetFile, finalFlat, {
|
|
2482
|
+
flat: mode === "inline"
|
|
2483
|
+
});
|
|
2484
|
+
const allTranslatedKeys = Object.keys(finalFlat);
|
|
2485
|
+
const currentLock = await loadLockFile(config.messagesDir);
|
|
2486
|
+
await writeLockFile(
|
|
2487
|
+
config.messagesDir,
|
|
2488
|
+
sourceFlat,
|
|
2489
|
+
currentLock,
|
|
2490
|
+
allTranslatedKeys
|
|
2491
|
+
);
|
|
2492
|
+
localeResults.push({
|
|
2493
|
+
locale,
|
|
2494
|
+
translated: Object.keys(translated).length,
|
|
2495
|
+
cached: Object.keys(diffResult.unchanged).length,
|
|
2496
|
+
removed: diffResult.removed.length,
|
|
2497
|
+
errors,
|
|
2498
|
+
duration: Date.now() - start
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
return { localeResults };
|
|
2502
|
+
}
|
|
2503
|
+
var init_pipeline = __esm({
|
|
2504
|
+
"src/pipeline.ts"() {
|
|
2505
|
+
"use strict";
|
|
2506
|
+
init_scanner();
|
|
2507
|
+
init_key_ai();
|
|
2508
|
+
init_codegen();
|
|
2509
|
+
init_translate();
|
|
2510
|
+
init_writer();
|
|
2511
|
+
init_diff();
|
|
2512
|
+
init_flatten();
|
|
2513
|
+
init_logger();
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
// src/usage.ts
|
|
2518
|
+
import { fetchModels, getTokenCosts } from "tokenlens";
|
|
2519
|
+
function createUsageTracker() {
|
|
2520
|
+
let inputTokens = 0;
|
|
2521
|
+
let outputTokens = 0;
|
|
2522
|
+
return {
|
|
2523
|
+
add(usage) {
|
|
2524
|
+
inputTokens += usage.inputTokens ?? 0;
|
|
2525
|
+
outputTokens += usage.outputTokens ?? 0;
|
|
2526
|
+
},
|
|
2527
|
+
get() {
|
|
2528
|
+
return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
async function estimateCost(model, usage) {
|
|
2533
|
+
try {
|
|
2534
|
+
const m = model;
|
|
2535
|
+
const provider = typeof m.provider === "string" ? m.provider : "unknown";
|
|
2536
|
+
const modelId = typeof m.modelId === "string" ? m.modelId : "unknown";
|
|
2537
|
+
const fullId = `${provider}/${modelId}`;
|
|
2538
|
+
const providers = await fetchModels(provider);
|
|
2539
|
+
const costs = getTokenCosts({
|
|
2540
|
+
modelId: fullId,
|
|
2541
|
+
usage: {
|
|
2542
|
+
prompt_tokens: usage.inputTokens,
|
|
2543
|
+
completion_tokens: usage.outputTokens
|
|
2544
|
+
},
|
|
2545
|
+
providers
|
|
2546
|
+
});
|
|
2547
|
+
if (costs.totalUSD == null) return null;
|
|
2548
|
+
return { totalUSD: costs.totalUSD, inputUSD: costs.inputUSD ?? 0, outputUSD: costs.outputUSD ?? 0 };
|
|
2549
|
+
} catch {
|
|
2550
|
+
return null;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
function formatUsage(usage) {
|
|
2554
|
+
return `${usage.inputTokens.toLocaleString()} in + ${usage.outputTokens.toLocaleString()} out = ${usage.totalTokens.toLocaleString()} tokens`;
|
|
2555
|
+
}
|
|
2556
|
+
function formatCost(usd) {
|
|
2557
|
+
return usd < 0.01 ? `~$${usd.toFixed(4)}` : `~$${usd.toFixed(2)}`;
|
|
2558
|
+
}
|
|
2559
|
+
var init_usage = __esm({
|
|
2560
|
+
"src/usage.ts"() {
|
|
2561
|
+
"use strict";
|
|
2562
|
+
}
|
|
2563
|
+
});
|
|
2564
|
+
|
|
2565
|
+
// src/cli-utils.ts
|
|
2566
|
+
function parseTranslateFlags(rawArgs) {
|
|
2567
|
+
const dryRun = rawArgs.includes("--dry-run");
|
|
2568
|
+
const force = rawArgs.includes("--force");
|
|
2569
|
+
const verbose = rawArgs.includes("--verbose");
|
|
2570
|
+
let locale;
|
|
2571
|
+
const equalsArg = rawArgs.find((a) => a.startsWith("--locale="));
|
|
2572
|
+
if (equalsArg) {
|
|
2573
|
+
locale = equalsArg.split("=")[1] || void 0;
|
|
2574
|
+
} else {
|
|
2575
|
+
const idx = rawArgs.indexOf("--locale");
|
|
2576
|
+
if (idx !== -1) {
|
|
2577
|
+
const next = rawArgs[idx + 1];
|
|
2578
|
+
locale = next && !next.startsWith("--") ? next : void 0;
|
|
1401
2579
|
}
|
|
1402
2580
|
}
|
|
1403
|
-
return {
|
|
1404
|
-
filesModified,
|
|
1405
|
-
stringsWrapped,
|
|
1406
|
-
filesProcessed: files.length
|
|
1407
|
-
};
|
|
2581
|
+
return { dryRun, force, verbose, locale };
|
|
1408
2582
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
2583
|
+
function validateLocale(locale) {
|
|
2584
|
+
return /^[a-zA-Z0-9_-]+$/.test(locale);
|
|
2585
|
+
}
|
|
2586
|
+
var init_cli_utils = __esm({
|
|
2587
|
+
"src/cli-utils.ts"() {
|
|
1411
2588
|
"use strict";
|
|
1412
|
-
init_parser();
|
|
1413
|
-
init_transform();
|
|
1414
|
-
init_logger();
|
|
1415
2589
|
}
|
|
1416
2590
|
});
|
|
1417
2591
|
|
|
1418
2592
|
// src/templates/t-component.ts
|
|
2593
|
+
function serverTemplate(clientBasename, opts) {
|
|
2594
|
+
if (!opts) {
|
|
2595
|
+
return `import type { ReactNode } from "react";
|
|
2596
|
+
import { cache } from "react";
|
|
2597
|
+
export { I18nProvider } from "./${clientBasename}";
|
|
2598
|
+
|
|
2599
|
+
type Messages = Record<string, string>;
|
|
2600
|
+
|
|
2601
|
+
// Per-request message store using React cache
|
|
2602
|
+
const getMessageStore = cache(() => ({ current: {} as Messages }));
|
|
2603
|
+
|
|
2604
|
+
export function setServerMessages(messages: Messages) {
|
|
2605
|
+
getMessageStore().current = messages;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
export function T({ id, children, messages }: { id?: string; children: ReactNode; messages?: Messages }) {
|
|
2609
|
+
if (!id) return <>{children}</>;
|
|
2610
|
+
const msgs = messages ?? getMessageStore().current;
|
|
2611
|
+
return <>{msgs[id] ?? children}</>;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
export function createT(messages?: Messages) {
|
|
2615
|
+
return (text: string, id?: string, values?: Record<string, string | number>): string => {
|
|
2616
|
+
const msgs = messages ?? getMessageStore().current;
|
|
2617
|
+
const raw = id ? (msgs[id] ?? text) : text;
|
|
2618
|
+
if (!values) return raw;
|
|
2619
|
+
return raw.replace(/\\{(\\w+)\\}/g, (_, k) => String(values[k] ?? \`{\${k}}\`));
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
`;
|
|
2623
|
+
}
|
|
2624
|
+
const allLocales = [opts.sourceLocale, ...opts.targetLocales];
|
|
2625
|
+
const allLocalesStr = allLocales.map((l) => `"${l}"`).join(", ");
|
|
2626
|
+
return `import type { ReactNode } from "react";
|
|
2627
|
+
import { cache } from "react";
|
|
2628
|
+
export { I18nProvider } from "./${clientBasename}";
|
|
2629
|
+
|
|
2630
|
+
type Messages = Record<string, string>;
|
|
2631
|
+
|
|
2632
|
+
const supported = [${allLocalesStr}] as const;
|
|
2633
|
+
type Locale = (typeof supported)[number];
|
|
2634
|
+
const defaultLocale: Locale = "${opts.sourceLocale}";
|
|
2635
|
+
const messagesDir = "${opts.messagesDir}";
|
|
2636
|
+
|
|
2637
|
+
function parseAcceptLanguage(header: string): Locale {
|
|
2638
|
+
const langs = header
|
|
2639
|
+
.split(",")
|
|
2640
|
+
.map((part) => {
|
|
2641
|
+
const [lang, q] = part.trim().split(";q=");
|
|
2642
|
+
return { lang: lang.split("-")[0].toLowerCase(), q: q ? parseFloat(q) : 1 };
|
|
2643
|
+
})
|
|
2644
|
+
.sort((a, b) => b.q - a.q);
|
|
2645
|
+
|
|
2646
|
+
for (const { lang } of langs) {
|
|
2647
|
+
if (supported.includes(lang as Locale)) return lang as Locale;
|
|
2648
|
+
}
|
|
2649
|
+
return defaultLocale;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// Per-request cached message loading \u2014 works even when layout is cached during client-side navigation
|
|
2653
|
+
// Uses dynamic imports so this file can be safely imported from client components
|
|
2654
|
+
const getCachedMessages = cache(async (): Promise<Messages> => {
|
|
2655
|
+
const { headers } = await import("next/headers");
|
|
2656
|
+
const { readFile } = await import("node:fs/promises");
|
|
2657
|
+
const { join } = await import("node:path");
|
|
2658
|
+
|
|
2659
|
+
const h = await headers();
|
|
2660
|
+
const acceptLang = h.get("accept-language") ?? "";
|
|
2661
|
+
const locale = parseAcceptLanguage(acceptLang);
|
|
2662
|
+
if (locale === defaultLocale) return {};
|
|
2663
|
+
try {
|
|
2664
|
+
const filePath = join(process.cwd(), messagesDir, \`\${locale}.json\`);
|
|
2665
|
+
const content = await readFile(filePath, "utf-8");
|
|
2666
|
+
return JSON.parse(content);
|
|
2667
|
+
} catch {
|
|
2668
|
+
return {};
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
|
|
2672
|
+
// Per-request message store (populated by setServerMessages in layout)
|
|
2673
|
+
const getMessageStore = cache(() => ({ current: null as Messages | null }));
|
|
2674
|
+
|
|
2675
|
+
export function setServerMessages(messages: Messages) {
|
|
2676
|
+
getMessageStore().current = messages;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
async function resolveMessages(explicit?: Messages): Promise<Messages> {
|
|
2680
|
+
if (explicit) return explicit;
|
|
2681
|
+
const store = getMessageStore().current;
|
|
2682
|
+
if (store) return store;
|
|
2683
|
+
return getCachedMessages();
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
export async function T({ id, children, messages }: { id?: string; children: ReactNode; messages?: Messages }) {
|
|
2687
|
+
if (!id) return <>{children}</>;
|
|
2688
|
+
const msgs = await resolveMessages(messages);
|
|
2689
|
+
// Populate store so sync createT() calls in the same request benefit
|
|
2690
|
+
if (!messages && !getMessageStore().current) {
|
|
2691
|
+
getMessageStore().current = msgs;
|
|
2692
|
+
}
|
|
2693
|
+
return <>{msgs[id] ?? children}</>;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
type TFn = (text: string, id?: string, values?: Record<string, string | number>) => string;
|
|
2697
|
+
|
|
2698
|
+
// Backward-compatible: works both as sync createT() and async await createT()
|
|
2699
|
+
// - Sync: reads from store (works when layout called setServerMessages)
|
|
2700
|
+
// - Async: lazily loads messages from filesystem (works during client-side navigation)
|
|
2701
|
+
export function createT(messages?: Messages): TFn & PromiseLike<TFn> {
|
|
2702
|
+
const t: TFn = (text, id, values) => {
|
|
2703
|
+
const msgs = messages ?? getMessageStore().current ?? {};
|
|
2704
|
+
const raw = id ? (msgs[id] ?? text) : text;
|
|
2705
|
+
if (!values) return raw;
|
|
2706
|
+
return raw.replace(/\\{(\\w+)\\}/g, (_, k) => String(values[k] ?? \`{\${k}}\`));
|
|
2707
|
+
};
|
|
2708
|
+
|
|
2709
|
+
const asyncResult = resolveMessages(messages).then(msgs => {
|
|
2710
|
+
if (!messages && !getMessageStore().current) {
|
|
2711
|
+
getMessageStore().current = msgs;
|
|
2712
|
+
}
|
|
2713
|
+
const bound: TFn = (text, id, values) => {
|
|
2714
|
+
const raw = id ? (msgs[id] ?? text) : text;
|
|
2715
|
+
if (!values) return raw;
|
|
2716
|
+
return raw.replace(/\\{(\\w+)\\}/g, (_, k) => String(values[k] ?? \`{\${k}}\`));
|
|
2717
|
+
};
|
|
2718
|
+
return bound;
|
|
2719
|
+
});
|
|
2720
|
+
|
|
2721
|
+
return Object.assign(t, { then: asyncResult.then.bind(asyncResult) });
|
|
2722
|
+
}
|
|
2723
|
+
`;
|
|
2724
|
+
}
|
|
1419
2725
|
function generateI18nHelper(opts) {
|
|
1420
2726
|
const allLocales = [opts.sourceLocale, ...opts.targetLocales];
|
|
1421
2727
|
const allLocalesStr = allLocales.map((l) => `"${l}"`).join(", ");
|
|
@@ -1460,7 +2766,7 @@ export async function getMessages(locale: string): Promise<Record<string, string
|
|
|
1460
2766
|
}
|
|
1461
2767
|
`;
|
|
1462
2768
|
}
|
|
1463
|
-
var CLIENT_TEMPLATE
|
|
2769
|
+
var CLIENT_TEMPLATE;
|
|
1464
2770
|
var init_t_component = __esm({
|
|
1465
2771
|
"src/templates/t-component.ts"() {
|
|
1466
2772
|
"use strict";
|
|
@@ -1482,25 +2788,10 @@ export function T({ id, children }: { id?: string; children: ReactNode }) {
|
|
|
1482
2788
|
|
|
1483
2789
|
export function useT() {
|
|
1484
2790
|
const msgs = useContext(I18nCtx);
|
|
1485
|
-
return (text: string, id?: string): string => {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
}
|
|
1490
|
-
`;
|
|
1491
|
-
SERVER_TEMPLATE = `import type { ReactNode } from "react";
|
|
1492
|
-
|
|
1493
|
-
type Messages = Record<string, string>;
|
|
1494
|
-
|
|
1495
|
-
export function T({ id, children, messages = {} }: { id?: string; children: ReactNode; messages?: Messages }) {
|
|
1496
|
-
if (!id) return <>{children}</>;
|
|
1497
|
-
return <>{messages[id] ?? children}</>;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
export function createT(messages: Messages = {}) {
|
|
1501
|
-
return (text: string, id?: string): string => {
|
|
1502
|
-
if (!id) return text;
|
|
1503
|
-
return messages[id] ?? text;
|
|
2791
|
+
return (text: string, id?: string, values?: Record<string, string | number>): string => {
|
|
2792
|
+
const raw = id ? (msgs[id] ?? text) : text;
|
|
2793
|
+
if (!values) return raw;
|
|
2794
|
+
return raw.replace(/\\{(\\w+)\\}/g, (_, k) => String(values[k] ?? \`{\${k}}\`));
|
|
1504
2795
|
};
|
|
1505
2796
|
}
|
|
1506
2797
|
`;
|
|
@@ -1514,17 +2805,17 @@ __export(init_exports, {
|
|
|
1514
2805
|
});
|
|
1515
2806
|
import * as p from "@clack/prompts";
|
|
1516
2807
|
import { existsSync } from "fs";
|
|
1517
|
-
import { join as
|
|
1518
|
-
import { readFile as
|
|
2808
|
+
import { basename, join as join5, relative } from "path";
|
|
2809
|
+
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
1519
2810
|
function detectIncludePatterns(cwd) {
|
|
1520
2811
|
const patterns = [];
|
|
1521
|
-
if (existsSync(
|
|
2812
|
+
if (existsSync(join5(cwd, "app")))
|
|
1522
2813
|
patterns.push("app/**/*.tsx", "app/**/*.jsx");
|
|
1523
|
-
if (existsSync(
|
|
2814
|
+
if (existsSync(join5(cwd, "src")))
|
|
1524
2815
|
patterns.push("src/**/*.tsx", "src/**/*.jsx");
|
|
1525
|
-
if (existsSync(
|
|
2816
|
+
if (existsSync(join5(cwd, "pages")))
|
|
1526
2817
|
patterns.push("pages/**/*.tsx", "pages/**/*.jsx");
|
|
1527
|
-
if (existsSync(
|
|
2818
|
+
if (existsSync(join5(cwd, "src", "app"))) {
|
|
1528
2819
|
return patterns.filter((p2) => !p2.startsWith("app/"));
|
|
1529
2820
|
}
|
|
1530
2821
|
return patterns.length > 0 ? patterns : ["**/*.tsx", "**/*.jsx"];
|
|
@@ -1537,10 +2828,10 @@ function findPackageInNodeModules(cwd, pkg) {
|
|
|
1537
2828
|
let dir = cwd;
|
|
1538
2829
|
const parts = pkg.split("/");
|
|
1539
2830
|
while (true) {
|
|
1540
|
-
if (existsSync(
|
|
2831
|
+
if (existsSync(join5(dir, "node_modules", ...parts, "package.json"))) {
|
|
1541
2832
|
return true;
|
|
1542
2833
|
}
|
|
1543
|
-
const parent =
|
|
2834
|
+
const parent = join5(dir, "..");
|
|
1544
2835
|
if (parent === dir) break;
|
|
1545
2836
|
dir = parent;
|
|
1546
2837
|
}
|
|
@@ -1608,7 +2899,7 @@ function canParse(content, filePath) {
|
|
|
1608
2899
|
return false;
|
|
1609
2900
|
}
|
|
1610
2901
|
}
|
|
1611
|
-
async function safeWriteModifiedFile(filePath,
|
|
2902
|
+
async function safeWriteModifiedFile(filePath, modified, label) {
|
|
1612
2903
|
if (!canParse(modified, filePath)) {
|
|
1613
2904
|
p.log.warn(
|
|
1614
2905
|
`Could not safely modify ${label}. Please apply changes manually:
|
|
@@ -1616,36 +2907,36 @@ async function safeWriteModifiedFile(filePath, original, modified, label) {
|
|
|
1616
2907
|
);
|
|
1617
2908
|
return false;
|
|
1618
2909
|
}
|
|
1619
|
-
await
|
|
2910
|
+
await writeFile4(filePath, modified, "utf-8");
|
|
1620
2911
|
return true;
|
|
1621
2912
|
}
|
|
1622
2913
|
function detectSrcDir(cwd) {
|
|
1623
|
-
return existsSync(
|
|
2914
|
+
return existsSync(join5(cwd, "src", "app"));
|
|
1624
2915
|
}
|
|
1625
2916
|
function resolveComponentPath(cwd, componentPath) {
|
|
1626
2917
|
if (componentPath.startsWith("@/")) {
|
|
1627
2918
|
const rel = componentPath.slice(2);
|
|
1628
|
-
const useSrc = existsSync(
|
|
1629
|
-
return
|
|
2919
|
+
const useSrc = existsSync(join5(cwd, "src"));
|
|
2920
|
+
return join5(cwd, useSrc ? "src" : "", rel);
|
|
1630
2921
|
}
|
|
1631
2922
|
if (componentPath.startsWith("~/")) {
|
|
1632
|
-
return
|
|
2923
|
+
return join5(cwd, componentPath.slice(2));
|
|
1633
2924
|
}
|
|
1634
|
-
return
|
|
2925
|
+
return join5(cwd, componentPath);
|
|
1635
2926
|
}
|
|
1636
2927
|
function findLayoutFile(base) {
|
|
1637
2928
|
for (const ext of ["tsx", "jsx", "ts", "js"]) {
|
|
1638
|
-
const candidate =
|
|
2929
|
+
const candidate = join5(base, "app", `layout.${ext}`);
|
|
1639
2930
|
if (existsSync(candidate)) return candidate;
|
|
1640
2931
|
}
|
|
1641
2932
|
return void 0;
|
|
1642
2933
|
}
|
|
1643
2934
|
async function createEmptyMessageFiles(msgDir, locales) {
|
|
1644
|
-
await
|
|
2935
|
+
await mkdir3(msgDir, { recursive: true });
|
|
1645
2936
|
for (const locale of locales) {
|
|
1646
|
-
const msgFile =
|
|
2937
|
+
const msgFile = join5(msgDir, `${locale}.json`);
|
|
1647
2938
|
if (!existsSync(msgFile)) {
|
|
1648
|
-
await
|
|
2939
|
+
await writeFile4(msgFile, "{}\n", "utf-8");
|
|
1649
2940
|
}
|
|
1650
2941
|
}
|
|
1651
2942
|
}
|
|
@@ -1663,16 +2954,16 @@ function ensureAsyncLayout(content) {
|
|
|
1663
2954
|
}
|
|
1664
2955
|
async function setupNextIntl(cwd, sourceLocale, targetLocales, messagesDir) {
|
|
1665
2956
|
const useSrc = detectSrcDir(cwd);
|
|
1666
|
-
const base = useSrc ?
|
|
2957
|
+
const base = useSrc ? join5(cwd, "src") : cwd;
|
|
1667
2958
|
const allLocales = [sourceLocale, ...targetLocales];
|
|
1668
2959
|
const filesCreated = [];
|
|
1669
|
-
const i18nDir =
|
|
1670
|
-
await
|
|
1671
|
-
const requestFile =
|
|
2960
|
+
const i18nDir = join5(base, "i18n");
|
|
2961
|
+
await mkdir3(i18nDir, { recursive: true });
|
|
2962
|
+
const requestFile = join5(i18nDir, "request.ts");
|
|
1672
2963
|
if (!existsSync(requestFile)) {
|
|
1673
|
-
const relMessages = relative(i18nDir,
|
|
2964
|
+
const relMessages = relative(i18nDir, join5(cwd, messagesDir));
|
|
1674
2965
|
const allLocalesStr = allLocales.map((l) => `"${l}"`).join(", ");
|
|
1675
|
-
await
|
|
2966
|
+
await writeFile4(
|
|
1676
2967
|
requestFile,
|
|
1677
2968
|
`import { getRequestConfig } from "next-intl/server";
|
|
1678
2969
|
import { headers } from "next/headers";
|
|
@@ -1711,9 +3002,9 @@ export default getRequestConfig(async () => {
|
|
|
1711
3002
|
);
|
|
1712
3003
|
filesCreated.push(relative(cwd, requestFile));
|
|
1713
3004
|
}
|
|
1714
|
-
const nextConfigPath =
|
|
3005
|
+
const nextConfigPath = join5(cwd, "next.config.ts");
|
|
1715
3006
|
if (existsSync(nextConfigPath)) {
|
|
1716
|
-
const content = await
|
|
3007
|
+
const content = await readFile5(nextConfigPath, "utf-8");
|
|
1717
3008
|
if (!content.includes("next-intl")) {
|
|
1718
3009
|
const importLine = `import createNextIntlPlugin from "next-intl/plugin";
|
|
1719
3010
|
`;
|
|
@@ -1726,7 +3017,6 @@ export default getRequestConfig(async () => {
|
|
|
1726
3017
|
const updated = importLine + "\n" + pluginLine + "\n" + wrapped;
|
|
1727
3018
|
if (await safeWriteModifiedFile(
|
|
1728
3019
|
nextConfigPath,
|
|
1729
|
-
content,
|
|
1730
3020
|
updated,
|
|
1731
3021
|
"next.config.ts"
|
|
1732
3022
|
)) {
|
|
@@ -1736,9 +3026,8 @@ export default getRequestConfig(async () => {
|
|
|
1736
3026
|
}
|
|
1737
3027
|
const layoutPath = findLayoutFile(base);
|
|
1738
3028
|
if (layoutPath) {
|
|
1739
|
-
let layoutContent = await
|
|
3029
|
+
let layoutContent = await readFile5(layoutPath, "utf-8");
|
|
1740
3030
|
if (!layoutContent.includes("NextIntlClientProvider")) {
|
|
1741
|
-
const original = layoutContent;
|
|
1742
3031
|
const importLines = 'import { NextIntlClientProvider } from "next-intl";\nimport { getMessages } from "next-intl/server";\n';
|
|
1743
3032
|
layoutContent = insertImportsAfterLast(layoutContent, importLines);
|
|
1744
3033
|
layoutContent = ensureAsyncLayout(layoutContent);
|
|
@@ -1756,7 +3045,6 @@ export default getRequestConfig(async () => {
|
|
|
1756
3045
|
);
|
|
1757
3046
|
if (await safeWriteModifiedFile(
|
|
1758
3047
|
layoutPath,
|
|
1759
|
-
original,
|
|
1760
3048
|
layoutContent,
|
|
1761
3049
|
"root layout"
|
|
1762
3050
|
)) {
|
|
@@ -1764,52 +3052,53 @@ export default getRequestConfig(async () => {
|
|
|
1764
3052
|
}
|
|
1765
3053
|
}
|
|
1766
3054
|
}
|
|
1767
|
-
await createEmptyMessageFiles(
|
|
3055
|
+
await createEmptyMessageFiles(join5(cwd, messagesDir), allLocales);
|
|
1768
3056
|
if (filesCreated.length > 0) {
|
|
1769
3057
|
p.log.success(`next-intl configured: ${filesCreated.join(", ")}`);
|
|
1770
3058
|
}
|
|
1771
3059
|
}
|
|
1772
|
-
async function dropInlineComponents(cwd, componentPath) {
|
|
3060
|
+
async function dropInlineComponents(cwd, componentPath, localeOpts) {
|
|
1773
3061
|
const fsPath = resolveComponentPath(cwd, componentPath);
|
|
1774
|
-
const dir =
|
|
1775
|
-
await
|
|
3062
|
+
const dir = join5(fsPath, "..");
|
|
3063
|
+
await mkdir3(dir, { recursive: true });
|
|
1776
3064
|
const clientFile = `${fsPath}.tsx`;
|
|
1777
3065
|
const serverFile = `${fsPath}-server.tsx`;
|
|
1778
|
-
|
|
1779
|
-
await
|
|
3066
|
+
const clientBasename = basename(fsPath);
|
|
3067
|
+
await writeFile4(clientFile, CLIENT_TEMPLATE, "utf-8");
|
|
3068
|
+
await writeFile4(serverFile, serverTemplate(clientBasename, localeOpts), "utf-8");
|
|
1780
3069
|
const relClient = relative(cwd, clientFile);
|
|
1781
3070
|
const relServer = relative(cwd, serverFile);
|
|
1782
3071
|
p.log.success(`Created inline components: ${relClient}, ${relServer}`);
|
|
1783
3072
|
}
|
|
1784
3073
|
async function setupInlineI18n(cwd, componentPath, sourceLocale, targetLocales, messagesDir) {
|
|
1785
|
-
const useSrc = existsSync(
|
|
1786
|
-
const base = useSrc ?
|
|
3074
|
+
const useSrc = existsSync(join5(cwd, "src"));
|
|
3075
|
+
const base = useSrc ? join5(cwd, "src") : cwd;
|
|
1787
3076
|
const filesCreated = [];
|
|
1788
|
-
const i18nDir =
|
|
1789
|
-
await
|
|
1790
|
-
const helperFile =
|
|
3077
|
+
const i18nDir = join5(base, "i18n");
|
|
3078
|
+
await mkdir3(i18nDir, { recursive: true });
|
|
3079
|
+
const helperFile = join5(i18nDir, "index.ts");
|
|
1791
3080
|
if (!existsSync(helperFile)) {
|
|
1792
3081
|
const helperContent = generateI18nHelper({
|
|
1793
3082
|
sourceLocale,
|
|
1794
3083
|
targetLocales,
|
|
1795
3084
|
messagesDir
|
|
1796
3085
|
});
|
|
1797
|
-
await
|
|
3086
|
+
await writeFile4(helperFile, helperContent, "utf-8");
|
|
1798
3087
|
filesCreated.push(relative(cwd, helperFile));
|
|
1799
3088
|
}
|
|
1800
3089
|
const layoutPath = findLayoutFile(base);
|
|
1801
3090
|
if (layoutPath) {
|
|
1802
|
-
let layoutContent = await
|
|
3091
|
+
let layoutContent = await readFile5(layoutPath, "utf-8");
|
|
1803
3092
|
if (!layoutContent.includes("I18nProvider")) {
|
|
1804
|
-
const original = layoutContent;
|
|
1805
3093
|
const importLines = `import { I18nProvider } from "${componentPath}";
|
|
3094
|
+
import { setServerMessages } from "${componentPath}-server";
|
|
1806
3095
|
import { getLocale, getMessages } from "@/i18n";
|
|
1807
3096
|
`;
|
|
1808
3097
|
layoutContent = insertImportsAfterLast(layoutContent, importLines);
|
|
1809
3098
|
layoutContent = ensureAsyncLayout(layoutContent);
|
|
1810
3099
|
layoutContent = layoutContent.replace(
|
|
1811
3100
|
/return\s*\(/,
|
|
1812
|
-
"const locale = await getLocale();\n const messages = await getMessages(locale);\n\n return ("
|
|
3101
|
+
"const locale = await getLocale();\n const messages = await getMessages(locale);\n setServerMessages(messages);\n\n return ("
|
|
1813
3102
|
);
|
|
1814
3103
|
layoutContent = layoutContent.replace(
|
|
1815
3104
|
/(<body[^>]*>)/,
|
|
@@ -1821,7 +3110,6 @@ import { getLocale, getMessages } from "@/i18n";
|
|
|
1821
3110
|
);
|
|
1822
3111
|
if (await safeWriteModifiedFile(
|
|
1823
3112
|
layoutPath,
|
|
1824
|
-
original,
|
|
1825
3113
|
layoutContent,
|
|
1826
3114
|
"root layout"
|
|
1827
3115
|
)) {
|
|
@@ -1829,7 +3117,7 @@ import { getLocale, getMessages } from "@/i18n";
|
|
|
1829
3117
|
}
|
|
1830
3118
|
}
|
|
1831
3119
|
}
|
|
1832
|
-
await createEmptyMessageFiles(
|
|
3120
|
+
await createEmptyMessageFiles(join5(cwd, messagesDir), [
|
|
1833
3121
|
sourceLocale,
|
|
1834
3122
|
...targetLocales
|
|
1835
3123
|
]);
|
|
@@ -1839,7 +3127,7 @@ import { getLocale, getMessages } from "@/i18n";
|
|
|
1839
3127
|
}
|
|
1840
3128
|
async function runInitWizard() {
|
|
1841
3129
|
const cwd = process.cwd();
|
|
1842
|
-
const configPath =
|
|
3130
|
+
const configPath = join5(cwd, "translate-kit.config.ts");
|
|
1843
3131
|
p.intro("translate-kit setup");
|
|
1844
3132
|
if (existsSync(configPath)) {
|
|
1845
3133
|
const overwrite = await p.confirm({
|
|
@@ -1884,7 +3172,12 @@ async function runInitWizard() {
|
|
|
1884
3172
|
if (p.isCancel(modelName)) cancel2();
|
|
1885
3173
|
const sourceLocale = await p.text({
|
|
1886
3174
|
message: "Source locale:",
|
|
1887
|
-
initialValue: "en"
|
|
3175
|
+
initialValue: "en",
|
|
3176
|
+
validate(value) {
|
|
3177
|
+
if (!validateLocale(value)) {
|
|
3178
|
+
return "Invalid locale. Use only letters, numbers, hyphens, and underscores.";
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
1888
3181
|
});
|
|
1889
3182
|
if (p.isCancel(sourceLocale)) cancel2();
|
|
1890
3183
|
const targetLocales = await p.multiselect({
|
|
@@ -1962,10 +3255,14 @@ async function runInitWizard() {
|
|
|
1962
3255
|
mode,
|
|
1963
3256
|
componentPath
|
|
1964
3257
|
});
|
|
1965
|
-
await
|
|
3258
|
+
await writeFile4(configPath, configContent, "utf-8");
|
|
1966
3259
|
p.log.success("Created translate-kit.config.ts");
|
|
1967
3260
|
if (mode === "inline" && componentPath) {
|
|
1968
|
-
await dropInlineComponents(cwd, componentPath
|
|
3261
|
+
await dropInlineComponents(cwd, componentPath, {
|
|
3262
|
+
sourceLocale,
|
|
3263
|
+
targetLocales,
|
|
3264
|
+
messagesDir
|
|
3265
|
+
});
|
|
1969
3266
|
await setupInlineI18n(
|
|
1970
3267
|
cwd,
|
|
1971
3268
|
componentPath,
|
|
@@ -1993,141 +3290,59 @@ async function runInitWizard() {
|
|
|
1993
3290
|
p.outro("Config created but pipeline skipped.");
|
|
1994
3291
|
return;
|
|
1995
3292
|
}
|
|
1996
|
-
const
|
|
1997
|
-
const scanOptions = {
|
|
1998
|
-
include: includePatterns,
|
|
1999
|
-
exclude: ["**/*.test.*", "**/*.spec.*"],
|
|
2000
|
-
i18nImport
|
|
2001
|
-
};
|
|
3293
|
+
const usageTracker = createUsageTracker();
|
|
2002
3294
|
const s1 = p.spinner();
|
|
2003
3295
|
s1.start("Scanning...");
|
|
2004
|
-
const scanResult = await
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
3296
|
+
const scanResult = await runScanStep({
|
|
3297
|
+
config,
|
|
3298
|
+
cwd,
|
|
3299
|
+
callbacks: {
|
|
3300
|
+
onScanProgress: (c, t3) => s1.message(`Scanning... ${c}/${t3} files`),
|
|
3301
|
+
onKeygenProgress: (c, t3) => s1.message(`Generating keys... ${c}/${t3}`),
|
|
3302
|
+
onUsage: (usage2) => usageTracker.add(usage2)
|
|
3303
|
+
}
|
|
3304
|
+
});
|
|
2008
3305
|
s1.stop(
|
|
2009
|
-
`
|
|
3306
|
+
`Found ${scanResult.bareStringCount} strings from ${scanResult.fileCount} files`
|
|
2010
3307
|
);
|
|
2011
|
-
if (
|
|
3308
|
+
if (scanResult.bareStringCount === 0) {
|
|
2012
3309
|
p.log.warn("No translatable strings found. Check your include patterns.");
|
|
2013
3310
|
p.outro("Config created, but no strings to process.");
|
|
2014
3311
|
return;
|
|
2015
3312
|
}
|
|
2016
|
-
const resolvedMessagesDir = join3(cwd, messagesDir);
|
|
2017
|
-
await mkdir2(resolvedMessagesDir, { recursive: true });
|
|
2018
|
-
let existingMap = {};
|
|
2019
|
-
const mapPath = join3(resolvedMessagesDir, ".translate-map.json");
|
|
2020
|
-
try {
|
|
2021
|
-
existingMap = JSON.parse(await readFile4(mapPath, "utf-8"));
|
|
2022
|
-
} catch {
|
|
2023
|
-
}
|
|
2024
|
-
const s2 = p.spinner();
|
|
2025
|
-
s2.start("Generating keys...");
|
|
2026
|
-
const textToKey = await generateSemanticKeys({
|
|
2027
|
-
model,
|
|
2028
|
-
strings: transformableStrings,
|
|
2029
|
-
existingMap,
|
|
2030
|
-
batchSize: config.translation?.batchSize ?? 50,
|
|
2031
|
-
concurrency: config.translation?.concurrency ?? 3,
|
|
2032
|
-
retries: config.translation?.retries ?? 2
|
|
2033
|
-
});
|
|
2034
|
-
s2.stop("Generating keys... done");
|
|
2035
|
-
await writeFile3(mapPath, JSON.stringify(textToKey, null, 2) + "\n", "utf-8");
|
|
2036
|
-
const messages = {};
|
|
2037
|
-
for (const [text2, key] of Object.entries(textToKey)) {
|
|
2038
|
-
messages[key] = text2;
|
|
2039
|
-
}
|
|
2040
|
-
let sourceFlat;
|
|
2041
|
-
if (mode === "inline") {
|
|
2042
|
-
sourceFlat = messages;
|
|
2043
|
-
} else {
|
|
2044
|
-
const sourceFile = join3(resolvedMessagesDir, `${sourceLocale}.json`);
|
|
2045
|
-
const nested = unflatten(messages);
|
|
2046
|
-
await writeFile3(
|
|
2047
|
-
sourceFile,
|
|
2048
|
-
JSON.stringify(nested, null, 2) + "\n",
|
|
2049
|
-
"utf-8"
|
|
2050
|
-
);
|
|
2051
|
-
sourceFlat = messages;
|
|
2052
|
-
}
|
|
2053
3313
|
const s3 = p.spinner();
|
|
2054
3314
|
s3.start("Codegen...");
|
|
2055
|
-
const codegenResult = await
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
},
|
|
2064
|
-
cwd
|
|
2065
|
-
);
|
|
3315
|
+
const codegenResult = await runCodegenStep({
|
|
3316
|
+
config,
|
|
3317
|
+
cwd,
|
|
3318
|
+
textToKey: scanResult.textToKey,
|
|
3319
|
+
callbacks: {
|
|
3320
|
+
onProgress: (c, t3) => s3.message(`Codegen... ${c}/${t3} files`)
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
2066
3323
|
s3.stop(
|
|
2067
3324
|
`Codegen... ${codegenResult.stringsWrapped} strings wrapped in ${codegenResult.filesModified} files`
|
|
2068
3325
|
);
|
|
2069
|
-
const postScan = await scan(scanOptions, cwd);
|
|
2070
|
-
const keyToText = {};
|
|
2071
|
-
for (const [text2, key] of Object.entries(textToKey)) {
|
|
2072
|
-
keyToText[key] = text2;
|
|
2073
|
-
}
|
|
2074
|
-
const reconciledMessages = {};
|
|
2075
|
-
if (mode === "inline") {
|
|
2076
|
-
const tComponents = postScan.strings.filter(
|
|
2077
|
-
(s) => s.type === "T-component" && s.id
|
|
2078
|
-
);
|
|
2079
|
-
const inlineTCalls = postScan.strings.filter(
|
|
2080
|
-
(s) => s.type === "t-call" && s.id
|
|
2081
|
-
);
|
|
2082
|
-
for (const tc of tComponents) {
|
|
2083
|
-
if (tc.id && tc.id in keyToText) {
|
|
2084
|
-
reconciledMessages[tc.id] = keyToText[tc.id];
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
for (const tc of inlineTCalls) {
|
|
2088
|
-
if (tc.id && tc.id in keyToText) {
|
|
2089
|
-
reconciledMessages[tc.id] = keyToText[tc.id];
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
} else {
|
|
2093
|
-
const tCalls = postScan.strings.filter((s) => s.type === "t-call");
|
|
2094
|
-
for (const tCall of tCalls) {
|
|
2095
|
-
const key = tCall.text;
|
|
2096
|
-
if (key in keyToText) {
|
|
2097
|
-
reconciledMessages[key] = keyToText[key];
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
const sourceFile = join3(resolvedMessagesDir, `${sourceLocale}.json`);
|
|
2101
|
-
const reconciledNested = unflatten(reconciledMessages);
|
|
2102
|
-
await writeFile3(
|
|
2103
|
-
sourceFile,
|
|
2104
|
-
JSON.stringify(reconciledNested, null, 2) + "\n",
|
|
2105
|
-
"utf-8"
|
|
2106
|
-
);
|
|
2107
|
-
}
|
|
2108
|
-
sourceFlat = reconciledMessages;
|
|
2109
|
-
const translationOpts = config.translation ?? {};
|
|
2110
3326
|
for (const locale of targetLocales) {
|
|
2111
3327
|
const st = p.spinner();
|
|
2112
3328
|
st.start(`Translating ${locale}...`);
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
3329
|
+
await runTranslateStep({
|
|
3330
|
+
config,
|
|
3331
|
+
sourceFlat: scanResult.sourceFlat,
|
|
3332
|
+
locales: [locale],
|
|
3333
|
+
callbacks: {
|
|
3334
|
+
onLocaleProgress: (_locale, c, t3) => st.message(`Translating ${locale}... ${c}/${t3} keys`),
|
|
3335
|
+
onUsage: (usage2) => usageTracker.add(usage2)
|
|
3336
|
+
}
|
|
2119
3337
|
});
|
|
2120
|
-
const targetFile = join3(resolvedMessagesDir, `${locale}.json`);
|
|
2121
|
-
await writeTranslation(targetFile, translated, { flat: mode === "inline" });
|
|
2122
|
-
const lockData = await loadLockFile(resolvedMessagesDir);
|
|
2123
|
-
await writeLockFile(
|
|
2124
|
-
resolvedMessagesDir,
|
|
2125
|
-
sourceFlat,
|
|
2126
|
-
lockData,
|
|
2127
|
-
Object.keys(translated)
|
|
2128
|
-
);
|
|
2129
3338
|
st.stop(`Translating ${locale}... done`);
|
|
2130
3339
|
}
|
|
3340
|
+
const usage = usageTracker.get();
|
|
3341
|
+
if (usage.totalTokens > 0) {
|
|
3342
|
+
const cost = await estimateCost(config.model, usage);
|
|
3343
|
+
const costStr = cost ? ` \xB7 ${formatCost(cost.totalUSD)}` : "";
|
|
3344
|
+
p.log.info(`${formatUsage(usage)}${costStr}`);
|
|
3345
|
+
}
|
|
2131
3346
|
p.outro("You're all set!");
|
|
2132
3347
|
}
|
|
2133
3348
|
var AI_PROVIDERS, LOCALE_OPTIONS;
|
|
@@ -2135,15 +3350,11 @@ var init_init = __esm({
|
|
|
2135
3350
|
"src/init.ts"() {
|
|
2136
3351
|
"use strict";
|
|
2137
3352
|
init_config();
|
|
2138
|
-
|
|
2139
|
-
init_key_ai();
|
|
2140
|
-
init_codegen();
|
|
2141
|
-
init_translate();
|
|
2142
|
-
init_writer();
|
|
2143
|
-
init_diff();
|
|
2144
|
-
init_flatten();
|
|
3353
|
+
init_pipeline();
|
|
2145
3354
|
init_t_component();
|
|
2146
3355
|
init_parser();
|
|
3356
|
+
init_usage();
|
|
3357
|
+
init_cli_utils();
|
|
2147
3358
|
AI_PROVIDERS = {
|
|
2148
3359
|
openai: {
|
|
2149
3360
|
pkg: "@ai-sdk/openai",
|
|
@@ -2190,31 +3401,14 @@ var init_init = __esm({
|
|
|
2190
3401
|
init_config();
|
|
2191
3402
|
init_flatten();
|
|
2192
3403
|
init_diff();
|
|
2193
|
-
init_translate();
|
|
2194
|
-
init_writer();
|
|
2195
3404
|
init_scanner();
|
|
2196
|
-
|
|
2197
|
-
init_codegen();
|
|
3405
|
+
init_pipeline();
|
|
2198
3406
|
init_logger();
|
|
3407
|
+
init_usage();
|
|
3408
|
+
init_cli_utils();
|
|
2199
3409
|
import "dotenv/config";
|
|
2200
3410
|
import { defineCommand, runMain } from "citty";
|
|
2201
|
-
import { join as
|
|
2202
|
-
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
2203
|
-
async function loadMapFile(messagesDir) {
|
|
2204
|
-
const mapPath = join4(messagesDir, ".translate-map.json");
|
|
2205
|
-
try {
|
|
2206
|
-
const content = await readFile5(mapPath, "utf-8");
|
|
2207
|
-
return JSON.parse(content);
|
|
2208
|
-
} catch {
|
|
2209
|
-
return {};
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
async function writeMapFile(messagesDir, map) {
|
|
2213
|
-
const mapPath = join4(messagesDir, ".translate-map.json");
|
|
2214
|
-
await mkdir3(messagesDir, { recursive: true });
|
|
2215
|
-
const content = JSON.stringify(map, null, 2) + "\n";
|
|
2216
|
-
await writeFile4(mapPath, content, "utf-8");
|
|
2217
|
-
}
|
|
3411
|
+
import { join as join6 } from "path";
|
|
2218
3412
|
var translateCommand = defineCommand({
|
|
2219
3413
|
meta: {
|
|
2220
3414
|
name: "translate",
|
|
@@ -2244,15 +3438,51 @@ var translateCommand = defineCommand({
|
|
|
2244
3438
|
async run({ args }) {
|
|
2245
3439
|
const config = await loadTranslateKitConfig();
|
|
2246
3440
|
const { sourceLocale, targetLocales, messagesDir, model } = config;
|
|
2247
|
-
const opts = config.translation ?? {};
|
|
2248
|
-
const verbose = args.verbose;
|
|
2249
3441
|
const mode = config.mode ?? "keys";
|
|
3442
|
+
if (args.locale && !validateLocale(args.locale)) {
|
|
3443
|
+
logError(
|
|
3444
|
+
`Invalid locale "${args.locale}". Locale must only contain letters, numbers, hyphens, and underscores.`
|
|
3445
|
+
);
|
|
3446
|
+
process.exit(1);
|
|
3447
|
+
}
|
|
2250
3448
|
const locales = args.locale ? [args.locale] : targetLocales;
|
|
2251
3449
|
if (args.locale && !targetLocales.includes(args.locale)) {
|
|
2252
3450
|
logWarning(
|
|
2253
3451
|
`Locale "${args.locale}" is not in targetLocales [${targetLocales.join(", ")}]`
|
|
2254
3452
|
);
|
|
2255
3453
|
}
|
|
3454
|
+
if (args["dry-run"]) {
|
|
3455
|
+
let sourceFlat2;
|
|
3456
|
+
if (mode === "inline") {
|
|
3457
|
+
const mapData = await loadMapFile(messagesDir);
|
|
3458
|
+
sourceFlat2 = {};
|
|
3459
|
+
for (const [text2, key] of Object.entries(mapData)) {
|
|
3460
|
+
sourceFlat2[key] = text2;
|
|
3461
|
+
}
|
|
3462
|
+
} else {
|
|
3463
|
+
const sourceFile = join6(messagesDir, `${sourceLocale}.json`);
|
|
3464
|
+
const sourceRaw = await loadJsonFile(sourceFile);
|
|
3465
|
+
sourceFlat2 = flatten(sourceRaw);
|
|
3466
|
+
}
|
|
3467
|
+
if (Object.keys(sourceFlat2).length === 0) {
|
|
3468
|
+
logError(
|
|
3469
|
+
mode === "inline" ? `No keys found in .translate-map.json. Run 'translate-kit scan' first.` : `No keys found in ${join6(messagesDir, `${sourceLocale}.json`)}`
|
|
3470
|
+
);
|
|
3471
|
+
process.exit(1);
|
|
3472
|
+
}
|
|
3473
|
+
const dryResult = await runTranslateStep({
|
|
3474
|
+
config,
|
|
3475
|
+
sourceFlat: sourceFlat2,
|
|
3476
|
+
locales,
|
|
3477
|
+
force: args.force,
|
|
3478
|
+
dryRun: true
|
|
3479
|
+
});
|
|
3480
|
+
logStart(sourceLocale, locales);
|
|
3481
|
+
for (const r of dryResult.localeResults) {
|
|
3482
|
+
logDryRun(r.locale, 0, 0, r.removed, r.cached);
|
|
3483
|
+
}
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
2256
3486
|
let sourceFlat;
|
|
2257
3487
|
if (mode === "inline") {
|
|
2258
3488
|
const mapData = await loadMapFile(messagesDir);
|
|
@@ -2261,92 +3491,48 @@ var translateCommand = defineCommand({
|
|
|
2261
3491
|
sourceFlat[key] = text2;
|
|
2262
3492
|
}
|
|
2263
3493
|
} else {
|
|
2264
|
-
const sourceFile =
|
|
3494
|
+
const sourceFile = join6(messagesDir, `${sourceLocale}.json`);
|
|
2265
3495
|
const sourceRaw = await loadJsonFile(sourceFile);
|
|
2266
3496
|
sourceFlat = flatten(sourceRaw);
|
|
2267
3497
|
}
|
|
2268
3498
|
if (Object.keys(sourceFlat).length === 0) {
|
|
2269
3499
|
logError(
|
|
2270
|
-
mode === "inline" ? `No keys found in .translate-map.json. Run 'translate-kit scan' first.` : `No keys found in ${
|
|
3500
|
+
mode === "inline" ? `No keys found in .translate-map.json. Run 'translate-kit scan' first.` : `No keys found in ${join6(messagesDir, `${sourceLocale}.json`)}`
|
|
2271
3501
|
);
|
|
2272
3502
|
process.exit(1);
|
|
2273
3503
|
}
|
|
2274
3504
|
logStart(sourceLocale, locales);
|
|
3505
|
+
const usageTracker = createUsageTracker();
|
|
2275
3506
|
const results = [];
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
lockData = {};
|
|
2285
|
-
}
|
|
2286
|
-
const diffResult = computeDiff(sourceFlat, targetFlat, lockData);
|
|
2287
|
-
const toTranslate = { ...diffResult.added, ...diffResult.modified };
|
|
2288
|
-
if (args["dry-run"]) {
|
|
2289
|
-
logDryRun(
|
|
2290
|
-
locale,
|
|
2291
|
-
Object.keys(diffResult.added).length,
|
|
2292
|
-
Object.keys(diffResult.modified).length,
|
|
2293
|
-
diffResult.removed.length,
|
|
2294
|
-
Object.keys(diffResult.unchanged).length
|
|
2295
|
-
);
|
|
2296
|
-
continue;
|
|
2297
|
-
}
|
|
2298
|
-
let translated = {};
|
|
2299
|
-
let errors = 0;
|
|
2300
|
-
if (Object.keys(toTranslate).length > 0) {
|
|
2301
|
-
try {
|
|
2302
|
-
translated = await translateAll({
|
|
2303
|
-
model,
|
|
2304
|
-
entries: toTranslate,
|
|
2305
|
-
sourceLocale,
|
|
2306
|
-
targetLocale: locale,
|
|
2307
|
-
options: opts,
|
|
2308
|
-
onBatchComplete: (batch) => {
|
|
2309
|
-
logVerbose(
|
|
2310
|
-
`Batch complete: ${Object.keys(batch).length} keys`,
|
|
2311
|
-
verbose
|
|
2312
|
-
);
|
|
2313
|
-
}
|
|
2314
|
-
});
|
|
2315
|
-
} catch (err) {
|
|
2316
|
-
errors = Object.keys(toTranslate).length;
|
|
2317
|
-
logError(
|
|
2318
|
-
`Translation failed for ${locale}: ${err instanceof Error ? err.message : String(err)}`
|
|
2319
|
-
);
|
|
2320
|
-
}
|
|
3507
|
+
const translateResult = await runTranslateStep({
|
|
3508
|
+
config,
|
|
3509
|
+
sourceFlat,
|
|
3510
|
+
locales,
|
|
3511
|
+
force: args.force,
|
|
3512
|
+
callbacks: {
|
|
3513
|
+
onLocaleProgress: (locale, c, t3) => logProgress(c, t3, `Translating ${locale}...`),
|
|
3514
|
+
onUsage: (usage2) => usageTracker.add(usage2)
|
|
2321
3515
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
await writeTranslation(targetFile, finalFlat, {
|
|
2327
|
-
flat: mode === "inline"
|
|
2328
|
-
});
|
|
2329
|
-
const allTranslatedKeys = Object.keys(finalFlat);
|
|
2330
|
-
const currentLock = await loadLockFile(messagesDir);
|
|
2331
|
-
await writeLockFile(
|
|
2332
|
-
messagesDir,
|
|
2333
|
-
sourceFlat,
|
|
2334
|
-
currentLock,
|
|
2335
|
-
allTranslatedKeys
|
|
2336
|
-
);
|
|
3516
|
+
});
|
|
3517
|
+
for (const r of translateResult.localeResults) {
|
|
3518
|
+
logLocaleStart(r.locale);
|
|
3519
|
+
logProgressClear();
|
|
2337
3520
|
const result = {
|
|
2338
|
-
locale,
|
|
2339
|
-
translated:
|
|
2340
|
-
cached:
|
|
2341
|
-
removed:
|
|
2342
|
-
errors,
|
|
2343
|
-
duration:
|
|
3521
|
+
locale: r.locale,
|
|
3522
|
+
translated: r.translated,
|
|
3523
|
+
cached: r.cached,
|
|
3524
|
+
removed: r.removed,
|
|
3525
|
+
errors: r.errors,
|
|
3526
|
+
duration: r.duration
|
|
2344
3527
|
};
|
|
2345
3528
|
logLocaleResult(result);
|
|
2346
3529
|
results.push(result);
|
|
2347
3530
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
3531
|
+
logSummary(results);
|
|
3532
|
+
const usage = usageTracker.get();
|
|
3533
|
+
if (usage.totalTokens > 0) {
|
|
3534
|
+
const cost = await estimateCost(model, usage);
|
|
3535
|
+
logUsage(formatUsage(usage), cost ? formatCost(cost.totalUSD) : void 0);
|
|
2350
3536
|
}
|
|
2351
3537
|
}
|
|
2352
3538
|
});
|
|
@@ -2371,14 +3557,17 @@ var scanCommand = defineCommand({
|
|
|
2371
3557
|
);
|
|
2372
3558
|
process.exit(1);
|
|
2373
3559
|
}
|
|
2374
|
-
const result = await scan(config.scan);
|
|
2375
|
-
const bareStrings = result.strings.filter((s) => {
|
|
2376
|
-
if (s.type === "t-call") return false;
|
|
2377
|
-
if (s.type === "T-component" && s.id) return false;
|
|
2378
|
-
return true;
|
|
2379
|
-
});
|
|
2380
|
-
logScanResult(bareStrings.length, result.fileCount);
|
|
2381
3560
|
if (args["dry-run"]) {
|
|
3561
|
+
const result = await scan(config.scan, process.cwd(), {
|
|
3562
|
+
onProgress: (c, t3) => logProgress(c, t3, "Scanning...")
|
|
3563
|
+
});
|
|
3564
|
+
logProgressClear();
|
|
3565
|
+
const bareStrings = result.strings.filter((s) => {
|
|
3566
|
+
if (s.type === "t-call") return false;
|
|
3567
|
+
if (s.type === "T-component" && s.id) return false;
|
|
3568
|
+
return true;
|
|
3569
|
+
});
|
|
3570
|
+
logScanResult(bareStrings.length, result.fileCount);
|
|
2382
3571
|
for (const str of bareStrings) {
|
|
2383
3572
|
logInfo(
|
|
2384
3573
|
`"${str.text}" (${str.componentName ?? "unknown"}, ${str.file})`
|
|
@@ -2391,57 +3580,37 @@ var scanCommand = defineCommand({
|
|
|
2391
3580
|
}
|
|
2392
3581
|
return;
|
|
2393
3582
|
}
|
|
2394
|
-
const
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
}
|
|
3583
|
+
const scanUsageTracker = createUsageTracker();
|
|
3584
|
+
const scanResult = await runScanStep({
|
|
3585
|
+
config,
|
|
3586
|
+
cwd: process.cwd(),
|
|
3587
|
+
callbacks: {
|
|
3588
|
+
onScanProgress: (c, t3) => logProgress(c, t3, "Scanning..."),
|
|
3589
|
+
onKeygenProgress: (c, t3) => logProgress(c, t3, "Generating keys..."),
|
|
3590
|
+
onUsage: (usage) => scanUsageTracker.add(usage)
|
|
2403
3591
|
}
|
|
2404
|
-
const existingInlineTCalls = result.strings.filter(
|
|
2405
|
-
(s) => s.type === "t-call" && s.id
|
|
2406
|
-
);
|
|
2407
|
-
for (const tc of existingInlineTCalls) {
|
|
2408
|
-
if (tc.id && !(tc.text in existingMap)) {
|
|
2409
|
-
existingMap[tc.text] = tc.id;
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
logInfo("Generating semantic keys...");
|
|
2414
|
-
const textToKey = await generateSemanticKeys({
|
|
2415
|
-
model: config.model,
|
|
2416
|
-
strings: bareStrings,
|
|
2417
|
-
existingMap,
|
|
2418
|
-
batchSize: config.translation?.batchSize ?? 50,
|
|
2419
|
-
concurrency: config.translation?.concurrency ?? 3,
|
|
2420
|
-
retries: config.translation?.retries ?? 2
|
|
2421
3592
|
});
|
|
2422
|
-
|
|
3593
|
+
logProgressClear();
|
|
3594
|
+
logScanResult(scanResult.bareStringCount, scanResult.fileCount);
|
|
2423
3595
|
logSuccess(
|
|
2424
|
-
`Written .translate-map.json (${Object.keys(textToKey).length} keys)`
|
|
3596
|
+
`Written .translate-map.json (${Object.keys(scanResult.textToKey).length} keys)`
|
|
2425
3597
|
);
|
|
2426
3598
|
if (mode === "inline") {
|
|
2427
3599
|
logInfo(
|
|
2428
3600
|
"Inline mode: source text stays in code, no source locale JSON created."
|
|
2429
3601
|
);
|
|
2430
3602
|
} else {
|
|
2431
|
-
const
|
|
2432
|
-
for (const [text2, key] of Object.entries(textToKey)) {
|
|
2433
|
-
messages[key] = text2;
|
|
2434
|
-
}
|
|
2435
|
-
const sourceFile = join4(
|
|
3603
|
+
const sourceFile = join6(
|
|
2436
3604
|
config.messagesDir,
|
|
2437
3605
|
`${config.sourceLocale}.json`
|
|
2438
3606
|
);
|
|
2439
|
-
await mkdir3(config.messagesDir, { recursive: true });
|
|
2440
|
-
const nested = unflatten(messages);
|
|
2441
|
-
const content = JSON.stringify(nested, null, 2) + "\n";
|
|
2442
|
-
await writeFile4(sourceFile, content, "utf-8");
|
|
2443
3607
|
logSuccess(`Written to ${sourceFile}`);
|
|
2444
3608
|
}
|
|
3609
|
+
const scanUsage = scanUsageTracker.get();
|
|
3610
|
+
if (scanUsage.totalTokens > 0) {
|
|
3611
|
+
const cost = await estimateCost(config.model, scanUsage);
|
|
3612
|
+
logUsage(formatUsage(scanUsage), cost ? formatCost(cost.totalUSD) : void 0);
|
|
3613
|
+
}
|
|
2445
3614
|
}
|
|
2446
3615
|
});
|
|
2447
3616
|
var codegenCommand = defineCommand({
|
|
@@ -2465,12 +3634,12 @@ var codegenCommand = defineCommand({
|
|
|
2465
3634
|
);
|
|
2466
3635
|
process.exit(1);
|
|
2467
3636
|
}
|
|
2468
|
-
const textToKey = await loadMapFile(config.messagesDir);
|
|
2469
|
-
if (Object.keys(textToKey).length === 0) {
|
|
2470
|
-
logError("No .translate-map.json found. Run 'translate-kit scan' first.");
|
|
2471
|
-
process.exit(1);
|
|
2472
|
-
}
|
|
2473
3637
|
if (args["dry-run"]) {
|
|
3638
|
+
const textToKey = await loadMapFile(config.messagesDir);
|
|
3639
|
+
if (Object.keys(textToKey).length === 0) {
|
|
3640
|
+
logError("No .translate-map.json found. Run 'translate-kit scan' first.");
|
|
3641
|
+
process.exit(1);
|
|
3642
|
+
}
|
|
2474
3643
|
if (mode === "inline") {
|
|
2475
3644
|
logInfo(
|
|
2476
3645
|
`
|
|
@@ -2492,17 +3661,22 @@ var codegenCommand = defineCommand({
|
|
|
2492
3661
|
}
|
|
2493
3662
|
return;
|
|
2494
3663
|
}
|
|
2495
|
-
const result = await
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
componentPath: config.inline?.componentPath
|
|
3664
|
+
const result = await runCodegenStep({
|
|
3665
|
+
config,
|
|
3666
|
+
cwd: process.cwd(),
|
|
3667
|
+
callbacks: {
|
|
3668
|
+
onProgress: (c, t3) => logProgress(c, t3, "Processing files...")
|
|
3669
|
+
}
|
|
2502
3670
|
});
|
|
3671
|
+
logProgressClear();
|
|
2503
3672
|
logSuccess(
|
|
2504
3673
|
`Codegen complete: ${result.stringsWrapped} strings wrapped in ${result.filesModified} files (${result.filesProcessed} files processed)`
|
|
2505
3674
|
);
|
|
3675
|
+
if (result.filesSkipped > 0) {
|
|
3676
|
+
logWarning(
|
|
3677
|
+
`${result.filesSkipped} file(s) skipped due to invalid generated syntax`
|
|
3678
|
+
);
|
|
3679
|
+
}
|
|
2506
3680
|
}
|
|
2507
3681
|
});
|
|
2508
3682
|
var initCommand = defineCommand({
|
|
@@ -2530,13 +3704,14 @@ var main = defineCommand({
|
|
|
2530
3704
|
// Default to translate command
|
|
2531
3705
|
async run({ rawArgs }) {
|
|
2532
3706
|
if (rawArgs.length === 0 || rawArgs[0]?.startsWith("-")) {
|
|
3707
|
+
const { dryRun, force, verbose, locale } = parseTranslateFlags(rawArgs);
|
|
2533
3708
|
await translateCommand.run({
|
|
2534
3709
|
args: {
|
|
2535
3710
|
_: rawArgs,
|
|
2536
|
-
"dry-run":
|
|
2537
|
-
force
|
|
2538
|
-
verbose
|
|
2539
|
-
locale: ""
|
|
3711
|
+
"dry-run": dryRun,
|
|
3712
|
+
force,
|
|
3713
|
+
verbose,
|
|
3714
|
+
locale: locale ?? ""
|
|
2540
3715
|
},
|
|
2541
3716
|
rawArgs,
|
|
2542
3717
|
cmd: translateCommand
|