@better-translate/cli 1.0.1 → 2.0.1
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 +74 -1
- package/dist/ai-sdk-generator-F2X3W7NC.js +89 -0
- package/dist/ai-sdk-generator.d.ts.map +1 -1
- package/dist/bin.js +75 -13
- package/dist/{chunk-JFSWNLL6.js → chunk-55TGASZJ.js} +643 -127
- package/dist/chunk-VYOBAIBH.js +8 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config.d.ts +1 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -5
- package/dist/extract.d.ts +3 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/generate.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/logger.d.ts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/types.d.ts +22 -12
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -5
- package/dist/ai-sdk-generator-WPQCTPGA.js +0 -37
- package/dist/chunk-WMIZO3GE.js +0 -19
- package/dist/provider-models.d.ts +0 -5
- package/dist/provider-models.d.ts.map +0 -1
|
@@ -179,33 +179,32 @@ function toJavaScriptIdentifier(value) {
|
|
|
179
179
|
|
|
180
180
|
// src/config-loader.ts
|
|
181
181
|
var DEFAULT_CONFIG_FILE = "better-translate.config.ts";
|
|
182
|
-
function
|
|
182
|
+
function resolveCliLanguageModel(model) {
|
|
183
183
|
assert(
|
|
184
184
|
isRecord(model),
|
|
185
|
-
|
|
185
|
+
"Config requires model to be a non-empty string or an AI SDK language model instance."
|
|
186
186
|
);
|
|
187
187
|
assert(
|
|
188
|
-
model.
|
|
189
|
-
|
|
188
|
+
model.specificationVersion === "v3",
|
|
189
|
+
"Config requires model to be a non-empty string or an AI SDK language model instance."
|
|
190
190
|
);
|
|
191
191
|
assert(
|
|
192
|
-
model.provider === "
|
|
193
|
-
|
|
192
|
+
typeof model.provider === "string" && model.provider.trim().length > 0,
|
|
193
|
+
"AI SDK language model instances require a non-empty provider string."
|
|
194
194
|
);
|
|
195
195
|
assert(
|
|
196
196
|
typeof model.modelId === "string" && model.modelId.trim().length > 0,
|
|
197
|
-
|
|
197
|
+
"AI SDK language model instances require a non-empty modelId string."
|
|
198
198
|
);
|
|
199
199
|
assert(
|
|
200
|
-
typeof model.
|
|
201
|
-
|
|
200
|
+
typeof model.doGenerate === "function",
|
|
201
|
+
"AI SDK language model instances must provide a doGenerate function."
|
|
202
202
|
);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
203
|
+
assert(
|
|
204
|
+
typeof model.doStream === "function",
|
|
205
|
+
"AI SDK language model instances must provide a doStream function."
|
|
206
|
+
);
|
|
207
|
+
return model;
|
|
209
208
|
}
|
|
210
209
|
function resolveConfig(rawConfig, configDirectory) {
|
|
211
210
|
assert(
|
|
@@ -271,7 +270,7 @@ function resolveConfig(rawConfig, configDirectory) {
|
|
|
271
270
|
if (typeof model === "string") {
|
|
272
271
|
assert(
|
|
273
272
|
model.trim().length > 0,
|
|
274
|
-
'Config requires a non-empty model string, for example "
|
|
273
|
+
'Config requires a non-empty model string, for example "provider/model-id".'
|
|
275
274
|
);
|
|
276
275
|
assert(
|
|
277
276
|
isRecord(gateway),
|
|
@@ -291,9 +290,9 @@ function resolveConfig(rawConfig, configDirectory) {
|
|
|
291
290
|
}
|
|
292
291
|
assert(
|
|
293
292
|
gateway === void 0,
|
|
294
|
-
"Config must not include gateway when model is
|
|
293
|
+
"Config must not include gateway when model is an AI SDK language model instance."
|
|
295
294
|
);
|
|
296
|
-
const resolvedModel =
|
|
295
|
+
const resolvedModel = resolveCliLanguageModel(model);
|
|
297
296
|
return {
|
|
298
297
|
...resolvedBase,
|
|
299
298
|
model: resolvedModel
|
|
@@ -322,21 +321,630 @@ async function loadCliConfig(options = {}) {
|
|
|
322
321
|
};
|
|
323
322
|
}
|
|
324
323
|
|
|
324
|
+
// src/extract.ts
|
|
325
|
+
import { readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
326
|
+
import path5 from "path";
|
|
327
|
+
import ts2 from "typescript";
|
|
328
|
+
|
|
329
|
+
// src/messages.ts
|
|
330
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
331
|
+
import path4 from "path";
|
|
332
|
+
import {
|
|
333
|
+
createTranslationJsonSchema
|
|
334
|
+
} from "@better-translate/core";
|
|
335
|
+
function getExportedMessages(module, sourceLocale) {
|
|
336
|
+
const value = module.default ?? module[sourceLocale];
|
|
337
|
+
assert(
|
|
338
|
+
value !== void 0,
|
|
339
|
+
`The source translation module must export a default object or a named "${sourceLocale}" export.`
|
|
340
|
+
);
|
|
341
|
+
assertTranslationMessages(
|
|
342
|
+
value,
|
|
343
|
+
"The source translation file must export nested objects with string leaves only."
|
|
344
|
+
);
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
async function loadSourceMessages(sourcePath, sourceLocale) {
|
|
348
|
+
const extension = path4.extname(sourcePath);
|
|
349
|
+
const sourceText = await readFile2(sourcePath, "utf8");
|
|
350
|
+
if (extension === ".json") {
|
|
351
|
+
const parsed = JSON.parse(sourceText);
|
|
352
|
+
assertTranslationMessages(
|
|
353
|
+
parsed,
|
|
354
|
+
"The source JSON file must contain nested objects with string leaves only."
|
|
355
|
+
);
|
|
356
|
+
return {
|
|
357
|
+
format: "json",
|
|
358
|
+
keyPaths: flattenTranslationKeys(parsed),
|
|
359
|
+
messages: parsed,
|
|
360
|
+
schema: createTranslationJsonSchema(parsed),
|
|
361
|
+
sourceText,
|
|
362
|
+
sourcePath
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
assert(
|
|
366
|
+
extension === ".ts",
|
|
367
|
+
`Unsupported source translation extension "${extension}". Use .json or .ts.`
|
|
368
|
+
);
|
|
369
|
+
const module = await importModule(sourcePath);
|
|
370
|
+
const messages = getExportedMessages(module, sourceLocale);
|
|
371
|
+
return {
|
|
372
|
+
format: "ts",
|
|
373
|
+
keyPaths: flattenTranslationKeys(messages),
|
|
374
|
+
messages,
|
|
375
|
+
schema: createTranslationJsonSchema(messages),
|
|
376
|
+
sourceText,
|
|
377
|
+
sourcePath
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function replaceLocaleSegment(basename, sourceLocale, targetLocale) {
|
|
381
|
+
if (basename === sourceLocale) {
|
|
382
|
+
return targetLocale;
|
|
383
|
+
}
|
|
384
|
+
const escapedSourceLocale = sourceLocale.replace(
|
|
385
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
386
|
+
"\\$&"
|
|
387
|
+
);
|
|
388
|
+
const pattern = new RegExp(`(^|[._-])${escapedSourceLocale}(?=$|[._-])`);
|
|
389
|
+
const match = basename.match(pattern);
|
|
390
|
+
if (!match) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
return basename.replace(pattern, `${match[1]}${targetLocale}`);
|
|
394
|
+
}
|
|
395
|
+
function deriveTargetMessagesPath(sourcePath, sourceLocale, targetLocale) {
|
|
396
|
+
const extension = path4.extname(sourcePath);
|
|
397
|
+
const basename = path4.basename(sourcePath, extension);
|
|
398
|
+
const replaced = replaceLocaleSegment(basename, sourceLocale, targetLocale);
|
|
399
|
+
assert(
|
|
400
|
+
replaced,
|
|
401
|
+
`Could not derive a target messages filename from "${sourcePath}". The basename must contain the source locale "${sourceLocale}".`
|
|
402
|
+
);
|
|
403
|
+
return path4.join(path4.dirname(sourcePath), `${replaced}${extension}`);
|
|
404
|
+
}
|
|
405
|
+
function formatTsPropertyKey(key) {
|
|
406
|
+
if (key === "__proto__") {
|
|
407
|
+
return JSON.stringify(key);
|
|
408
|
+
}
|
|
409
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(key) ? key : JSON.stringify(key);
|
|
410
|
+
}
|
|
411
|
+
function serializeTsObject(messages, indentLevel = 0) {
|
|
412
|
+
const entries = Object.entries(messages);
|
|
413
|
+
if (entries.length === 0) {
|
|
414
|
+
return "{}";
|
|
415
|
+
}
|
|
416
|
+
const indent = " ".repeat(indentLevel);
|
|
417
|
+
const childIndent = " ".repeat(indentLevel + 1);
|
|
418
|
+
const lines = entries.map(([key, value]) => {
|
|
419
|
+
const propertyKey = formatTsPropertyKey(key);
|
|
420
|
+
if (typeof value === "string") {
|
|
421
|
+
return `${childIndent}${propertyKey}: ${JSON.stringify(value)},`;
|
|
422
|
+
}
|
|
423
|
+
return `${childIndent}${propertyKey}: ${serializeTsObject(
|
|
424
|
+
value,
|
|
425
|
+
indentLevel + 1
|
|
426
|
+
)},`;
|
|
427
|
+
});
|
|
428
|
+
return `{
|
|
429
|
+
${lines.join("\n")}
|
|
430
|
+
${indent}}`;
|
|
431
|
+
}
|
|
432
|
+
function serializeMessages(messages, format, locale) {
|
|
433
|
+
if (format === "json") {
|
|
434
|
+
return `${JSON.stringify(messages, null, 2)}
|
|
435
|
+
`;
|
|
436
|
+
}
|
|
437
|
+
const identifier = toJavaScriptIdentifier(locale);
|
|
438
|
+
const objectLiteral = serializeTsObject(messages);
|
|
439
|
+
return `// generated by @better-translate/cli
|
|
440
|
+
export const ${identifier} = ${objectLiteral} as const;
|
|
441
|
+
|
|
442
|
+
export default ${identifier};
|
|
443
|
+
`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/extract.ts
|
|
447
|
+
var DEFAULT_MAX_LENGTH = 40;
|
|
448
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
449
|
+
".cjs",
|
|
450
|
+
".cts",
|
|
451
|
+
".js",
|
|
452
|
+
".jsx",
|
|
453
|
+
".mjs",
|
|
454
|
+
".mts",
|
|
455
|
+
".ts",
|
|
456
|
+
".tsx"
|
|
457
|
+
]);
|
|
458
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
459
|
+
".git",
|
|
460
|
+
".next",
|
|
461
|
+
".turbo",
|
|
462
|
+
"build",
|
|
463
|
+
"coverage",
|
|
464
|
+
"dist",
|
|
465
|
+
"node_modules"
|
|
466
|
+
]);
|
|
467
|
+
var NAMESPACE_ROOTS = /* @__PURE__ */ new Set([
|
|
468
|
+
"app",
|
|
469
|
+
"components",
|
|
470
|
+
"lib",
|
|
471
|
+
"pages",
|
|
472
|
+
"routes",
|
|
473
|
+
"src"
|
|
474
|
+
]);
|
|
475
|
+
function createDefaultLogger() {
|
|
476
|
+
return {
|
|
477
|
+
error(message) {
|
|
478
|
+
console.error(message);
|
|
479
|
+
},
|
|
480
|
+
info(message) {
|
|
481
|
+
console.log(message);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function cloneMessages(messages) {
|
|
486
|
+
return JSON.parse(JSON.stringify(messages));
|
|
487
|
+
}
|
|
488
|
+
function slugifySegment(value) {
|
|
489
|
+
const normalized = value.normalize("NFKD").replace(/\p{M}+/gu, "").trim();
|
|
490
|
+
const parts = normalized.split(/[^\p{L}\p{N}]+/u).map((part) => part.trim()).filter(Boolean);
|
|
491
|
+
if (parts.length === 0) {
|
|
492
|
+
return "message";
|
|
493
|
+
}
|
|
494
|
+
const [head, ...tail] = parts;
|
|
495
|
+
return `${head.toLowerCase()}${tail.map((part) => `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}`).join("")}`;
|
|
496
|
+
}
|
|
497
|
+
function createLeafKey(value, maxLength) {
|
|
498
|
+
const slug = slugifySegment(value);
|
|
499
|
+
if (slug.length <= maxLength) {
|
|
500
|
+
return slug;
|
|
501
|
+
}
|
|
502
|
+
return slug.slice(0, maxLength);
|
|
503
|
+
}
|
|
504
|
+
function deriveNamespace(configDirectory, sourcePath) {
|
|
505
|
+
const relativePath = path5.relative(configDirectory, sourcePath);
|
|
506
|
+
const withoutExtension = relativePath.slice(
|
|
507
|
+
0,
|
|
508
|
+
relativePath.length - path5.extname(relativePath).length
|
|
509
|
+
);
|
|
510
|
+
const rawSegments = withoutExtension.split(path5.sep).map((segment) => segment.trim()).filter(Boolean);
|
|
511
|
+
const segments = [...rawSegments];
|
|
512
|
+
while (segments.length > 1 && NAMESPACE_ROOTS.has(segments[0].toLowerCase())) {
|
|
513
|
+
segments.shift();
|
|
514
|
+
}
|
|
515
|
+
return segments.map((segment) => slugifySegment(segment)).join(".");
|
|
516
|
+
}
|
|
517
|
+
function getMessageValue(messages, keyPath) {
|
|
518
|
+
let current = messages;
|
|
519
|
+
for (const segment of keyPath.split(".")) {
|
|
520
|
+
if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
521
|
+
return void 0;
|
|
522
|
+
}
|
|
523
|
+
current = current[segment];
|
|
524
|
+
}
|
|
525
|
+
return typeof current === "string" ? current : void 0;
|
|
526
|
+
}
|
|
527
|
+
function setMessageValue(messages, keyPath, value, logWarning) {
|
|
528
|
+
const segments = keyPath.split(".");
|
|
529
|
+
let current = messages;
|
|
530
|
+
for (const segment of segments.slice(0, -1)) {
|
|
531
|
+
const existing = Object.prototype.hasOwnProperty.call(current, segment) ? current[segment] : void 0;
|
|
532
|
+
if (existing !== void 0 && !isRecord(existing)) {
|
|
533
|
+
logWarning?.(
|
|
534
|
+
`key "${keyPath}" conflicts with existing leaf value at segment "${segment}"; skipping.`
|
|
535
|
+
);
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
if (!isRecord(existing)) {
|
|
539
|
+
current[segment] = {};
|
|
540
|
+
}
|
|
541
|
+
current = current[segment];
|
|
542
|
+
}
|
|
543
|
+
const finalSegment = segments[segments.length - 1];
|
|
544
|
+
const finalExisting = Object.prototype.hasOwnProperty.call(current, finalSegment) ? current[finalSegment] : void 0;
|
|
545
|
+
if (isRecord(finalExisting)) {
|
|
546
|
+
logWarning?.(
|
|
547
|
+
`key "${keyPath}" conflicts with existing nested object at "${finalSegment}"; skipping.`
|
|
548
|
+
);
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
current[finalSegment] = value;
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
function createScriptKind(sourcePath) {
|
|
555
|
+
const extension = path5.extname(sourcePath);
|
|
556
|
+
switch (extension) {
|
|
557
|
+
case ".js":
|
|
558
|
+
case ".cjs":
|
|
559
|
+
case ".mjs":
|
|
560
|
+
return ts2.ScriptKind.JS;
|
|
561
|
+
case ".jsx":
|
|
562
|
+
return ts2.ScriptKind.JSX;
|
|
563
|
+
case ".tsx":
|
|
564
|
+
return ts2.ScriptKind.TSX;
|
|
565
|
+
default:
|
|
566
|
+
return ts2.ScriptKind.TS;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function getLiteralArgument(node) {
|
|
570
|
+
if (ts2.isStringLiteral(node)) {
|
|
571
|
+
return node.text;
|
|
572
|
+
}
|
|
573
|
+
if (ts2.isNoSubstitutionTemplateLiteral(node)) {
|
|
574
|
+
return node.text;
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
function getObjectPropertyName(node) {
|
|
579
|
+
if (ts2.isIdentifier(node) || ts2.isStringLiteral(node)) {
|
|
580
|
+
return node.text;
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
function getBtMarkerState(node) {
|
|
585
|
+
if (!node) {
|
|
586
|
+
return "none";
|
|
587
|
+
}
|
|
588
|
+
if (!ts2.isObjectLiteralExpression(node)) {
|
|
589
|
+
return "none";
|
|
590
|
+
}
|
|
591
|
+
let sawBt = false;
|
|
592
|
+
for (const property of node.properties) {
|
|
593
|
+
if (!ts2.isPropertyAssignment(property)) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const name = getObjectPropertyName(property.name);
|
|
597
|
+
if (name !== "bt") {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
sawBt = true;
|
|
601
|
+
if (property.initializer.kind !== ts2.SyntaxKind.TrueKeyword) {
|
|
602
|
+
return "invalid";
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return sawBt ? "valid" : "none";
|
|
606
|
+
}
|
|
607
|
+
function buildPreservedOptionsText(sourceFile, node) {
|
|
608
|
+
if (!node || !ts2.isObjectLiteralExpression(node)) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const remainingProperties = node.properties.filter((property) => {
|
|
612
|
+
if (!ts2.isPropertyAssignment(property)) {
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
const name = getObjectPropertyName(property.name);
|
|
616
|
+
return name !== "bt";
|
|
617
|
+
});
|
|
618
|
+
if (remainingProperties.length === 0) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
const printer = ts2.createPrinter();
|
|
622
|
+
const objectLiteral = ts2.factory.updateObjectLiteralExpression(
|
|
623
|
+
node,
|
|
624
|
+
remainingProperties
|
|
625
|
+
);
|
|
626
|
+
return printer.printNode(
|
|
627
|
+
ts2.EmitHint.Unspecified,
|
|
628
|
+
objectLiteral,
|
|
629
|
+
sourceFile
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
function createWarning(sourceFile, node, message) {
|
|
633
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
634
|
+
return `${sourceFile.fileName}:${position.line + 1}: ${message}`;
|
|
635
|
+
}
|
|
636
|
+
function analyzeFile(options) {
|
|
637
|
+
const { configDirectory, maxLength, messages, sourcePath, sourceText } = options;
|
|
638
|
+
const sourceFile = ts2.createSourceFile(
|
|
639
|
+
sourcePath,
|
|
640
|
+
sourceText,
|
|
641
|
+
ts2.ScriptTarget.Latest,
|
|
642
|
+
true,
|
|
643
|
+
createScriptKind(sourcePath)
|
|
644
|
+
);
|
|
645
|
+
const namespace = deriveNamespace(configDirectory, sourcePath);
|
|
646
|
+
const literalCandidates = [];
|
|
647
|
+
const warnings = [];
|
|
648
|
+
const seenWarnings = /* @__PURE__ */ new Set();
|
|
649
|
+
const logWarning = (warning) => {
|
|
650
|
+
if (seenWarnings.has(warning)) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
seenWarnings.add(warning);
|
|
654
|
+
warnings.push(warning);
|
|
655
|
+
};
|
|
656
|
+
const visit = (node) => {
|
|
657
|
+
if (!ts2.isCallExpression(node)) {
|
|
658
|
+
ts2.forEachChild(node, visit);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const callee = node.expression;
|
|
662
|
+
const isIdentifierCall = ts2.isIdentifier(callee) && callee.text === "t";
|
|
663
|
+
const isPropertyCall = ts2.isPropertyAccessExpression(callee) && callee.name.text === "t";
|
|
664
|
+
if (!isIdentifierCall && !isPropertyCall) {
|
|
665
|
+
ts2.forEachChild(node, visit);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const markerState = getBtMarkerState(node.arguments[1]);
|
|
669
|
+
if (markerState === "none") {
|
|
670
|
+
ts2.forEachChild(node, visit);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (markerState === "invalid") {
|
|
674
|
+
logWarning(
|
|
675
|
+
createWarning(
|
|
676
|
+
sourceFile,
|
|
677
|
+
node,
|
|
678
|
+
"skipped marked t() call because the bt marker must be written as bt: true."
|
|
679
|
+
)
|
|
680
|
+
);
|
|
681
|
+
ts2.forEachChild(node, visit);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const firstArgument = node.arguments[0];
|
|
685
|
+
if (!firstArgument) {
|
|
686
|
+
logWarning(
|
|
687
|
+
createWarning(
|
|
688
|
+
sourceFile,
|
|
689
|
+
node,
|
|
690
|
+
"skipped marked t() call because the first argument is missing."
|
|
691
|
+
)
|
|
692
|
+
);
|
|
693
|
+
ts2.forEachChild(node, visit);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const literalValue = getLiteralArgument(firstArgument);
|
|
697
|
+
if (literalValue === null) {
|
|
698
|
+
logWarning(
|
|
699
|
+
createWarning(
|
|
700
|
+
sourceFile,
|
|
701
|
+
node,
|
|
702
|
+
"skipped marked t() call because the first argument is not a static string literal."
|
|
703
|
+
)
|
|
704
|
+
);
|
|
705
|
+
ts2.forEachChild(node, visit);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const leafKey = createLeafKey(literalValue, maxLength);
|
|
709
|
+
const fullKey = namespace ? `${namespace}.${leafKey}` : leafKey;
|
|
710
|
+
const nodeStart = node.getStart(sourceFile);
|
|
711
|
+
const nodeEnd = node.getEnd();
|
|
712
|
+
const overlapping = literalCandidates.some((existing) => {
|
|
713
|
+
const existingStart = existing.callExpression.getStart(sourceFile);
|
|
714
|
+
const existingEnd = existing.callExpression.getEnd();
|
|
715
|
+
return nodeStart < existingEnd && nodeEnd > existingStart;
|
|
716
|
+
});
|
|
717
|
+
if (overlapping) {
|
|
718
|
+
logWarning(
|
|
719
|
+
createWarning(
|
|
720
|
+
sourceFile,
|
|
721
|
+
node,
|
|
722
|
+
"skipped marked t() call because it overlaps with another marked t() call."
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
ts2.forEachChild(node, visit);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
literalCandidates.push({
|
|
729
|
+
callExpression: node,
|
|
730
|
+
fullKey,
|
|
731
|
+
rewrittenOptionsText: buildPreservedOptionsText(sourceFile, node.arguments[1]),
|
|
732
|
+
sourceValue: literalValue
|
|
733
|
+
});
|
|
734
|
+
ts2.forEachChild(node, visit);
|
|
735
|
+
};
|
|
736
|
+
visit(sourceFile);
|
|
737
|
+
const duplicateStrings = /* @__PURE__ */ new Set();
|
|
738
|
+
const duplicateCounts = /* @__PURE__ */ new Map();
|
|
739
|
+
for (const candidate of literalCandidates) {
|
|
740
|
+
duplicateCounts.set(
|
|
741
|
+
candidate.sourceValue,
|
|
742
|
+
(duplicateCounts.get(candidate.sourceValue) ?? 0) + 1
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
for (const [sourceValue, count] of duplicateCounts) {
|
|
746
|
+
if (count > 1) {
|
|
747
|
+
duplicateStrings.add(sourceValue);
|
|
748
|
+
logWarning(
|
|
749
|
+
`${sourcePath}: skipped ${count} marked t() calls for "${sourceValue}" because the same string appears more than once in the file.`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const collisionMap = /* @__PURE__ */ new Map();
|
|
754
|
+
for (const candidate of literalCandidates) {
|
|
755
|
+
const values = collisionMap.get(candidate.fullKey) ?? /* @__PURE__ */ new Set();
|
|
756
|
+
values.add(candidate.sourceValue);
|
|
757
|
+
collisionMap.set(candidate.fullKey, values);
|
|
758
|
+
}
|
|
759
|
+
const collidedKeys = /* @__PURE__ */ new Set();
|
|
760
|
+
for (const [key, values] of collisionMap) {
|
|
761
|
+
if (values.size > 1) {
|
|
762
|
+
collidedKeys.add(key);
|
|
763
|
+
logWarning(
|
|
764
|
+
`${sourcePath}: skipped marked t() calls for "${key}" because multiple strings would generate the same key.`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const successfulCandidates = [];
|
|
769
|
+
const addedKeys = [];
|
|
770
|
+
for (const candidate of literalCandidates) {
|
|
771
|
+
if (duplicateStrings.has(candidate.sourceValue)) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (collidedKeys.has(candidate.fullKey)) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const existingValue = getMessageValue(messages, candidate.fullKey);
|
|
778
|
+
if (existingValue === void 0) {
|
|
779
|
+
const written = setMessageValue(
|
|
780
|
+
messages,
|
|
781
|
+
candidate.fullKey,
|
|
782
|
+
candidate.sourceValue,
|
|
783
|
+
logWarning
|
|
784
|
+
);
|
|
785
|
+
if (written) {
|
|
786
|
+
addedKeys.push(candidate.fullKey);
|
|
787
|
+
successfulCandidates.push(candidate);
|
|
788
|
+
}
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (existingValue !== candidate.sourceValue) {
|
|
792
|
+
logWarning(
|
|
793
|
+
`${sourcePath}: skipped marked t() call for "${candidate.fullKey}" because the key already exists with a different value.`
|
|
794
|
+
);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
successfulCandidates.push(candidate);
|
|
798
|
+
}
|
|
799
|
+
if (successfulCandidates.length === 0) {
|
|
800
|
+
return {
|
|
801
|
+
addedKeys,
|
|
802
|
+
updatedSource: null,
|
|
803
|
+
warnings
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const updatedSource = successfulCandidates.sort(
|
|
807
|
+
(left, right) => right.callExpression.getStart(sourceFile) - left.callExpression.getStart(sourceFile)
|
|
808
|
+
).reduce((currentText, candidate) => {
|
|
809
|
+
const start = candidate.callExpression.getStart(sourceFile);
|
|
810
|
+
const end = candidate.callExpression.getEnd();
|
|
811
|
+
const expressionText = candidate.callExpression.expression.getText(sourceFile);
|
|
812
|
+
const replacement = candidate.rewrittenOptionsText ? `${expressionText}(${JSON.stringify(candidate.fullKey)}, ${candidate.rewrittenOptionsText})` : `${expressionText}(${JSON.stringify(candidate.fullKey)})`;
|
|
813
|
+
return `${currentText.slice(0, start)}${replacement}${currentText.slice(end)}`;
|
|
814
|
+
}, sourceText);
|
|
815
|
+
return {
|
|
816
|
+
addedKeys,
|
|
817
|
+
updatedSource: updatedSource === sourceText ? null : updatedSource,
|
|
818
|
+
warnings
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
async function collectSourceFiles(directory, ignoredPaths) {
|
|
822
|
+
const entries = (await readdir(directory, {
|
|
823
|
+
withFileTypes: true
|
|
824
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
825
|
+
const files = [];
|
|
826
|
+
for (const entry of entries) {
|
|
827
|
+
const entryPath = path5.join(directory, entry.name);
|
|
828
|
+
if (ignoredPaths.has(entryPath)) {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (entry.isDirectory()) {
|
|
832
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
files.push(...await collectSourceFiles(entryPath, ignoredPaths));
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (SOURCE_EXTENSIONS.has(path5.extname(entry.name))) {
|
|
839
|
+
files.push(entryPath);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return files;
|
|
843
|
+
}
|
|
844
|
+
async function extractProject(options = {}) {
|
|
845
|
+
const logger = options.logger ?? createDefaultLogger();
|
|
846
|
+
const dryRun = options.dryRun ?? false;
|
|
847
|
+
const maxLength = options.maxLength ?? DEFAULT_MAX_LENGTH;
|
|
848
|
+
assert(
|
|
849
|
+
Number.isInteger(maxLength) && maxLength > 0,
|
|
850
|
+
"--max-length must be a positive integer."
|
|
851
|
+
);
|
|
852
|
+
logger.info("Loading Better Translate config...");
|
|
853
|
+
const loadedConfig = await loadCliConfig({
|
|
854
|
+
configPath: options.configPath,
|
|
855
|
+
cwd: options.cwd
|
|
856
|
+
});
|
|
857
|
+
logger.info(`Using config: ${loadedConfig.path}`);
|
|
858
|
+
logger.info(`Source locale: ${loadedConfig.config.sourceLocale}`);
|
|
859
|
+
const loadedSourceMessages = await loadSourceMessages(
|
|
860
|
+
loadedConfig.config.messages.entry,
|
|
861
|
+
loadedConfig.config.sourceLocale
|
|
862
|
+
);
|
|
863
|
+
const messages = cloneMessages(loadedSourceMessages.messages);
|
|
864
|
+
const ignoredPaths = /* @__PURE__ */ new Set([loadedConfig.config.messages.entry]);
|
|
865
|
+
const sourcePaths = await collectSourceFiles(
|
|
866
|
+
loadedConfig.directory,
|
|
867
|
+
ignoredPaths
|
|
868
|
+
);
|
|
869
|
+
const rewrittenPaths = [];
|
|
870
|
+
const warnings = [];
|
|
871
|
+
const updatedKeys = [];
|
|
872
|
+
for (const sourcePath of sourcePaths) {
|
|
873
|
+
const sourceText = await readFile3(sourcePath, "utf8");
|
|
874
|
+
const analysis = analyzeFile({
|
|
875
|
+
configDirectory: loadedConfig.directory,
|
|
876
|
+
maxLength,
|
|
877
|
+
messages,
|
|
878
|
+
sourcePath,
|
|
879
|
+
sourceText
|
|
880
|
+
});
|
|
881
|
+
warnings.push(...analysis.warnings);
|
|
882
|
+
updatedKeys.push(...analysis.addedKeys);
|
|
883
|
+
if (!analysis.updatedSource) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
rewrittenPaths.push(sourcePath);
|
|
887
|
+
if (dryRun) {
|
|
888
|
+
logger.info(`[dry-run] rewrote ${sourcePath}`);
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
await writeFile2(sourcePath, analysis.updatedSource, "utf8");
|
|
892
|
+
logger.info(`rewrote ${sourcePath}`);
|
|
893
|
+
}
|
|
894
|
+
const hasMessageChanges = updatedKeys.length > 0 || JSON.stringify(messages) !== JSON.stringify(loadedSourceMessages.messages);
|
|
895
|
+
if (hasMessageChanges) {
|
|
896
|
+
const serialized = serializeMessages(
|
|
897
|
+
messages,
|
|
898
|
+
loadedSourceMessages.format,
|
|
899
|
+
loadedConfig.config.sourceLocale
|
|
900
|
+
);
|
|
901
|
+
if (loadedSourceMessages.format === "ts" && !loadedSourceMessages.sourceText.startsWith(
|
|
902
|
+
"// generated by @better-translate/cli"
|
|
903
|
+
)) {
|
|
904
|
+
logger.info(
|
|
905
|
+
`warn source messages file "${loadedConfig.config.messages.entry}" was not generated by the CLI; skipping update to avoid overwriting manual content. Use a .json source file or replace the file with a CLI-generated one.`
|
|
906
|
+
);
|
|
907
|
+
} else if (dryRun) {
|
|
908
|
+
logger.info(
|
|
909
|
+
`[dry-run] updated messages ${loadedConfig.config.messages.entry}`
|
|
910
|
+
);
|
|
911
|
+
} else {
|
|
912
|
+
await writeFile2(loadedConfig.config.messages.entry, serialized, "utf8");
|
|
913
|
+
logger.info(`updated messages ${loadedConfig.config.messages.entry}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
for (const warning of warnings) {
|
|
917
|
+
logger.error(`warn ${warning}`);
|
|
918
|
+
}
|
|
919
|
+
const fileLabel = rewrittenPaths.length === 1 ? "file" : "files";
|
|
920
|
+
const keyLabel = updatedKeys.length === 1 ? "key" : "keys";
|
|
921
|
+
logger.info(
|
|
922
|
+
`processed ${rewrittenPaths.length} ${fileLabel} and synced ${updatedKeys.length} ${keyLabel}.`
|
|
923
|
+
);
|
|
924
|
+
return {
|
|
925
|
+
dryRun,
|
|
926
|
+
filePaths: rewrittenPaths,
|
|
927
|
+
loadedConfig,
|
|
928
|
+
updatedMessages: updatedKeys,
|
|
929
|
+
warnings
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
325
933
|
// src/generate.ts
|
|
326
|
-
import { mkdir, writeFile as
|
|
327
|
-
import
|
|
934
|
+
import { mkdir, writeFile as writeFile3 } from "fs/promises";
|
|
935
|
+
import path7 from "path";
|
|
328
936
|
|
|
329
937
|
// src/markdown.ts
|
|
330
|
-
import { readdir, readFile as
|
|
331
|
-
import
|
|
938
|
+
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
939
|
+
import path6 from "path";
|
|
332
940
|
import matter from "gray-matter";
|
|
333
941
|
async function walkDirectory(directory) {
|
|
334
|
-
const entries = await
|
|
942
|
+
const entries = await readdir2(directory, {
|
|
335
943
|
withFileTypes: true
|
|
336
944
|
});
|
|
337
945
|
const files = await Promise.all(
|
|
338
946
|
entries.map(async (entry) => {
|
|
339
|
-
const entryPath =
|
|
947
|
+
const entryPath = path6.join(directory, entry.name);
|
|
340
948
|
if (entry.isDirectory()) {
|
|
341
949
|
return walkDirectory(entryPath);
|
|
342
950
|
}
|
|
@@ -422,11 +1030,11 @@ async function listMarkdownSourceFiles(rootDir, extensions) {
|
|
|
422
1030
|
).sort();
|
|
423
1031
|
}
|
|
424
1032
|
async function loadMarkdownDocument(rootDir, sourcePath) {
|
|
425
|
-
const sourceText = await
|
|
1033
|
+
const sourceText = await readFile4(sourcePath, "utf8");
|
|
426
1034
|
const parsed = matter(sourceText);
|
|
427
1035
|
const frontmatter = isRecord(parsed.data) ? parsed.data : {};
|
|
428
1036
|
const frontmatterStrings = extractFrontmatterStrings(frontmatter);
|
|
429
|
-
const relativePath =
|
|
1037
|
+
const relativePath = path6.relative(rootDir, sourcePath).split(path6.sep).join("/");
|
|
430
1038
|
return {
|
|
431
1039
|
body: parsed.content,
|
|
432
1040
|
frontmatter,
|
|
@@ -438,109 +1046,20 @@ async function loadMarkdownDocument(rootDir, sourcePath) {
|
|
|
438
1046
|
};
|
|
439
1047
|
}
|
|
440
1048
|
function deriveTargetMarkdownRoot(rootDir, sourceLocale, targetLocale) {
|
|
441
|
-
const basename =
|
|
1049
|
+
const basename = path6.basename(rootDir);
|
|
442
1050
|
assert(
|
|
443
1051
|
basename === sourceLocale,
|
|
444
1052
|
`markdown.rootDir must end with the source locale "${sourceLocale}" so the CLI can mirror sibling locale folders.`
|
|
445
1053
|
);
|
|
446
|
-
return
|
|
1054
|
+
return path6.join(path6.dirname(rootDir), targetLocale);
|
|
447
1055
|
}
|
|
448
1056
|
function deriveTargetMarkdownPath(rootDir, sourceLocale, targetLocale, relativePath) {
|
|
449
|
-
return
|
|
1057
|
+
return path6.join(
|
|
450
1058
|
deriveTargetMarkdownRoot(rootDir, sourceLocale, targetLocale),
|
|
451
1059
|
relativePath
|
|
452
1060
|
);
|
|
453
1061
|
}
|
|
454
1062
|
|
|
455
|
-
// src/messages.ts
|
|
456
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
457
|
-
import path5 from "path";
|
|
458
|
-
import {
|
|
459
|
-
createTranslationJsonSchema
|
|
460
|
-
} from "@better-translate/core";
|
|
461
|
-
function getExportedMessages(module, sourceLocale) {
|
|
462
|
-
const value = module.default ?? module[sourceLocale];
|
|
463
|
-
assert(
|
|
464
|
-
value !== void 0,
|
|
465
|
-
`The source translation module must export a default object or a named "${sourceLocale}" export.`
|
|
466
|
-
);
|
|
467
|
-
assertTranslationMessages(
|
|
468
|
-
value,
|
|
469
|
-
"The source translation file must export nested objects with string leaves only."
|
|
470
|
-
);
|
|
471
|
-
return value;
|
|
472
|
-
}
|
|
473
|
-
async function loadSourceMessages(sourcePath, sourceLocale) {
|
|
474
|
-
const extension = path5.extname(sourcePath);
|
|
475
|
-
const sourceText = await readFile3(sourcePath, "utf8");
|
|
476
|
-
if (extension === ".json") {
|
|
477
|
-
const parsed = JSON.parse(sourceText);
|
|
478
|
-
assertTranslationMessages(
|
|
479
|
-
parsed,
|
|
480
|
-
"The source JSON file must contain nested objects with string leaves only."
|
|
481
|
-
);
|
|
482
|
-
return {
|
|
483
|
-
format: "json",
|
|
484
|
-
keyPaths: flattenTranslationKeys(parsed),
|
|
485
|
-
messages: parsed,
|
|
486
|
-
schema: createTranslationJsonSchema(parsed),
|
|
487
|
-
sourceText,
|
|
488
|
-
sourcePath
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
assert(
|
|
492
|
-
extension === ".ts",
|
|
493
|
-
`Unsupported source translation extension "${extension}". Use .json or .ts.`
|
|
494
|
-
);
|
|
495
|
-
const module = await importModule(sourcePath);
|
|
496
|
-
const messages = getExportedMessages(module, sourceLocale);
|
|
497
|
-
return {
|
|
498
|
-
format: "ts",
|
|
499
|
-
keyPaths: flattenTranslationKeys(messages),
|
|
500
|
-
messages,
|
|
501
|
-
schema: createTranslationJsonSchema(messages),
|
|
502
|
-
sourceText,
|
|
503
|
-
sourcePath
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
function replaceLocaleSegment(basename, sourceLocale, targetLocale) {
|
|
507
|
-
if (basename === sourceLocale) {
|
|
508
|
-
return targetLocale;
|
|
509
|
-
}
|
|
510
|
-
const escapedSourceLocale = sourceLocale.replace(
|
|
511
|
-
/[.*+?^${}()|[\]\\]/g,
|
|
512
|
-
"\\$&"
|
|
513
|
-
);
|
|
514
|
-
const pattern = new RegExp(`(^|[._-])${escapedSourceLocale}(?=$|[._-])`);
|
|
515
|
-
const match = basename.match(pattern);
|
|
516
|
-
if (!match) {
|
|
517
|
-
return null;
|
|
518
|
-
}
|
|
519
|
-
return basename.replace(pattern, `${match[1]}${targetLocale}`);
|
|
520
|
-
}
|
|
521
|
-
function deriveTargetMessagesPath(sourcePath, sourceLocale, targetLocale) {
|
|
522
|
-
const extension = path5.extname(sourcePath);
|
|
523
|
-
const basename = path5.basename(sourcePath, extension);
|
|
524
|
-
const replaced = replaceLocaleSegment(basename, sourceLocale, targetLocale);
|
|
525
|
-
assert(
|
|
526
|
-
replaced,
|
|
527
|
-
`Could not derive a target messages filename from "${sourcePath}". The basename must contain the source locale "${sourceLocale}".`
|
|
528
|
-
);
|
|
529
|
-
return path5.join(path5.dirname(sourcePath), `${replaced}${extension}`);
|
|
530
|
-
}
|
|
531
|
-
function serializeMessages(messages, format, locale) {
|
|
532
|
-
if (format === "json") {
|
|
533
|
-
return `${JSON.stringify(messages, null, 2)}
|
|
534
|
-
`;
|
|
535
|
-
}
|
|
536
|
-
const identifier = toJavaScriptIdentifier(locale);
|
|
537
|
-
const objectLiteral = JSON.stringify(messages, null, 2);
|
|
538
|
-
return `export const ${identifier} = ${objectLiteral} as const;
|
|
539
|
-
|
|
540
|
-
export default ${identifier};
|
|
541
|
-
`;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
1063
|
// src/prompts.ts
|
|
545
1064
|
var MESSAGE_SYSTEM_INSTRUCTIONS = [
|
|
546
1065
|
"You are translating application locale files.",
|
|
@@ -638,10 +1157,10 @@ async function persistWrite(write, options) {
|
|
|
638
1157
|
);
|
|
639
1158
|
return;
|
|
640
1159
|
}
|
|
641
|
-
await mkdir(
|
|
1160
|
+
await mkdir(path7.dirname(write.targetPath), {
|
|
642
1161
|
recursive: true
|
|
643
1162
|
});
|
|
644
|
-
await
|
|
1163
|
+
await writeFile3(write.targetPath, write.content, "utf8");
|
|
645
1164
|
options.logger.info(
|
|
646
1165
|
`wrote ${write.kind}:${write.locale} ${write.targetPath}`
|
|
647
1166
|
);
|
|
@@ -654,7 +1173,7 @@ function prepareGatewayEnvironment(apiKey) {
|
|
|
654
1173
|
process.env.AI_GATEWAY_API_KEY = apiKey;
|
|
655
1174
|
}
|
|
656
1175
|
async function createDefaultGenerator(model) {
|
|
657
|
-
const { generateWithAiSdk } = await import("./ai-sdk-generator-
|
|
1176
|
+
const { generateWithAiSdk } = await import("./ai-sdk-generator-F2X3W7NC.js");
|
|
658
1177
|
return async (request) => generateWithAiSdk(model, request);
|
|
659
1178
|
}
|
|
660
1179
|
async function resolveRuntimeModel(config) {
|
|
@@ -665,13 +1184,9 @@ async function resolveRuntimeModel(config) {
|
|
|
665
1184
|
model: config.model
|
|
666
1185
|
};
|
|
667
1186
|
}
|
|
668
|
-
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
669
|
-
const provider = createOpenAI({
|
|
670
|
-
apiKey: config.model.apiKey
|
|
671
|
-
});
|
|
672
1187
|
return {
|
|
673
|
-
description: `Using
|
|
674
|
-
model:
|
|
1188
|
+
description: `Using configured provider model: ${config.model.provider}/${config.model.modelId}`,
|
|
1189
|
+
model: config.model
|
|
675
1190
|
};
|
|
676
1191
|
}
|
|
677
1192
|
function validateMarkdownTranslation(frontmatterStrings, value) {
|
|
@@ -850,5 +1365,6 @@ async function generateProject(options = {}) {
|
|
|
850
1365
|
|
|
851
1366
|
export {
|
|
852
1367
|
loadCliConfig,
|
|
1368
|
+
extractProject,
|
|
853
1369
|
generateProject
|
|
854
1370
|
};
|