@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.
@@ -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 resolveProviderModelSpec(model) {
182
+ function resolveCliLanguageModel(model) {
183
183
  assert(
184
184
  isRecord(model),
185
- 'Config requires model to be a non-empty string or openai("model-id", { apiKey }).'
185
+ "Config requires model to be a non-empty string or an AI SDK language model instance."
186
186
  );
187
187
  assert(
188
- model.kind === "provider-model",
189
- 'Config requires model to be a non-empty string or openai("model-id", { apiKey }).'
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 === "openai",
193
- 'Only openai("model-id", { apiKey }) is supported for built-in provider mode right now.'
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
- 'openai("model-id", { apiKey }) requires a non-empty model id.'
197
+ "AI SDK language model instances require a non-empty modelId string."
198
198
  );
199
199
  assert(
200
- typeof model.apiKey === "string" && model.apiKey.trim().length > 0,
201
- 'openai("model-id", { apiKey }) requires a non-empty apiKey string.'
200
+ typeof model.doGenerate === "function",
201
+ "AI SDK language model instances must provide a doGenerate function."
202
202
  );
203
- return {
204
- apiKey: model.apiKey.trim(),
205
- kind: "provider-model",
206
- modelId: model.modelId.trim(),
207
- provider: "openai"
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 "openai/gpt-4.1".'
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 created with openai(...)."
293
+ "Config must not include gateway when model is an AI SDK language model instance."
295
294
  );
296
- const resolvedModel = resolveProviderModelSpec(model);
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 writeFile2 } from "fs/promises";
327
- import path6 from "path";
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 readFile2 } from "fs/promises";
331
- import path4 from "path";
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 readdir(directory, {
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 = path4.join(directory, entry.name);
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 readFile2(sourcePath, "utf8");
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 = path4.relative(rootDir, sourcePath).split(path4.sep).join("/");
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 = path4.basename(rootDir);
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 path4.join(path4.dirname(rootDir), targetLocale);
1054
+ return path6.join(path6.dirname(rootDir), targetLocale);
447
1055
  }
448
1056
  function deriveTargetMarkdownPath(rootDir, sourceLocale, targetLocale, relativePath) {
449
- return path4.join(
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(path6.dirname(write.targetPath), {
1160
+ await mkdir(path7.dirname(write.targetPath), {
642
1161
  recursive: true
643
1162
  });
644
- await writeFile2(write.targetPath, write.content, "utf8");
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-WPQCTPGA.js");
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 built-in OpenAI provider model: ${config.model.modelId}`,
674
- model: provider(config.model.modelId)
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
  };