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/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
- /^[^a-zA-Z]*$/
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" && value.expression.type === "StringLiteral") {
521
- text2 = value.expression.value;
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 !== "StringLiteral") return;
564
- const text2 = valueNode.value.trim();
565
- if (shouldIgnore(text2)) return;
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/key-generator.ts
649
- import { createHash as createHash2 } from "crypto";
650
- function slugify(text2) {
651
- return text2.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "").slice(0, 32);
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 generateKey(extracted, strategy) {
654
- if (strategy === "hash") {
655
- const hash = createHash2("sha256").update(extracted.text).digest("hex").slice(0, 12);
656
- return hash;
657
- }
658
- const parts = [];
659
- if (extracted.componentName) {
660
- parts.push(extracted.componentName);
661
- }
662
- if (extracted.parentTag) {
663
- parts.push(extracted.parentTag);
664
- }
665
- const slug = slugify(extracted.text);
666
- parts.push(slug);
667
- return parts.join(".");
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 init_key_generator = __esm({
670
- "src/scanner/key-generator.ts"() {
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
- async function scan(options, cwd = process.cwd()) {
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 messages = {};
761
- const keyStrategy = options.keyStrategy ?? "hash";
762
- for (const filePath of files) {
763
- const code = await readFile2(filePath, "utf-8");
764
- let ast;
765
- try {
766
- ast = parseFile(code, filePath);
767
- } catch (err) {
768
- logVerbose(
769
- `Skipping unparseable file ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
770
- true
771
- );
772
- continue;
773
- }
774
- const strings = extractStrings(ast, filePath, options.translatableProps);
775
- for (const str of strings) {
776
- const key = generateKey(str, keyStrategy);
777
- if (!(key in messages)) {
778
- messages[key] = str.text;
779
- }
780
- allStrings.push(str);
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
- init_key_generator();
841
+ init_context_enricher();
795
842
  init_logger();
796
843
  }
797
844
  });
798
845
 
799
846
  // src/scanner/key-ai.ts
800
- import { generateObject as generateObject2 } from "ai";
801
- import { z as z3 } from "zod";
847
+ import { generateObject } from "ai";
848
+ import { z as z2 } from "zod";
802
849
  import pLimit2 from "p-limit";
803
- function buildPrompt2(strings) {
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 = buildPrompt2(strings);
885
+ const prompt = buildPrompt(strings);
834
886
  const texts = strings.map((s) => s.text);
835
- const schema = z3.object({
836
- mappings: z3.array(
837
- z3.object({
838
- index: z3.number().describe("Zero-based index of the string"),
839
- key: z3.string().describe("Semantic i18n 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 generateObject2({
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((resolve) => setTimeout(resolve, delay));
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 newStrings = strings.filter((s) => !(s.text in existingMap));
893
- if (newStrings.length === 0) return { ...existingMap };
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
- const resolved = resolveCollisions(allNewKeys, existingMap);
916
- return { ...existingMap, ...resolved };
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 t from "@babel/types";
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 hasUseTranslationsCall(ast) {
950
- let found = false;
951
- traverse2(ast, {
952
- VariableDeclarator(path) {
953
- const init = path.node.init;
954
- if (init?.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "useTranslations") {
955
- found = true;
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
- noScope: true
960
- });
961
- return found;
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 = t.jsxExpressionContainer(
978
- t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
1157
+ const tCall = t2.jsxExpressionContainer(
1158
+ t2.callExpression(t2.identifier("t"), [t2.stringLiteral(key)])
979
1159
  );
980
- const siblings = parent.node.children.filter((child) => {
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(t.jsxExpressionContainer(t.stringLiteral(" ")));
1168
+ nodes.push(t2.jsxExpressionContainer(t2.stringLiteral(" ")));
993
1169
  }
994
1170
  nodes.push(tCall);
995
1171
  if (hasTrailing) {
996
- nodes.push(t.jsxExpressionContainer(t.stringLiteral(" ")));
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
- JSXAttribute(path) {
1005
- const value = path.node.value;
1006
- if (!value) return;
1007
- let text2;
1008
- if (value.type === "StringLiteral") {
1009
- text2 = value.value;
1010
- } else if (value.type === "JSXExpressionContainer" && value.expression.type === "StringLiteral") {
1011
- text2 = value.expression.value;
1012
- }
1013
- if (!text2 || !(text2 in textToKey)) return;
1014
- if (value.type === "JSXExpressionContainer" && value.expression.type === "CallExpression" && value.expression.callee.type === "Identifier" && value.expression.callee.name === "t") {
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
- path.node.value = t.jsxExpressionContainer(
1019
- t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
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 !== "StringLiteral") return;
1034
- const text2 = valueNode.value;
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
- path.node.value = t.callExpression(t.identifier("t"), [
1038
- t.stringLiteral(key)
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
- const compName = getComponentName(path);
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 (!hasUseTranslationsImport(ast, importSource)) {
1049
- const importDecl = t.importDeclaration(
1050
- [
1051
- t.importSpecifier(
1052
- t.identifier("useTranslations"),
1053
- t.identifier("useTranslations")
1054
- )
1055
- ],
1056
- t.stringLiteral(importSource)
1057
- );
1058
- const lastImportIndex = findLastImportIndex(ast);
1059
- if (lastImportIndex >= 0) {
1060
- ast.program.body.splice(lastImportIndex + 1, 0, importDecl);
1061
- } else {
1062
- ast.program.body.unshift(importDecl);
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 = t.variableDeclaration("const", [
1104
- t.variableDeclarator(
1105
- t.identifier("t"),
1106
- t.callExpression(t.identifier("useTranslations"), [])
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 isClientFile(ast) {
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
- return false;
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 hasInlineHookCall(ast, hookName) {
1145
- let found = false;
1146
- traverse2(ast, {
1147
- VariableDeclarator(path) {
1148
- const init = path.node.init;
1149
- if (init?.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === hookName) {
1150
- found = true;
1151
- path.stop();
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
- noScope: true
1155
- });
1156
- return found;
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 = isClientFile(ast);
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 = t.jsxElement(
1194
- t.jsxOpeningElement(t.jsxIdentifier("T"), [
1195
- t.jsxAttribute(t.jsxIdentifier("id"), t.stringLiteral(key))
1552
+ const tElement = t2.jsxElement(
1553
+ t2.jsxOpeningElement(t2.jsxIdentifier("T"), [
1554
+ t2.jsxAttribute(t2.jsxIdentifier("id"), t2.stringLiteral(key))
1196
1555
  ]),
1197
- t.jsxClosingElement(t.jsxIdentifier("T")),
1198
- [t.jsxText(text2)],
1556
+ t2.jsxClosingElement(t2.jsxIdentifier("T")),
1557
+ [t2.jsxText(text2)],
1199
1558
  false
1200
1559
  );
1201
- const siblings = parent.node.children.filter((child) => {
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(t.jsxExpressionContainer(t.stringLiteral(" ")));
1568
+ nodes.push(t2.jsxExpressionContainer(t2.stringLiteral(" ")));
1214
1569
  }
1215
1570
  nodes.push(tElement);
1216
1571
  if (hasTrailing) {
1217
- nodes.push(t.jsxExpressionContainer(t.stringLiteral(" ")));
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" && value.expression.type === "StringLiteral") {
1230
- text2 = value.expression.value;
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
- path.node.value = t.jsxExpressionContainer(
1238
- t.callExpression(t.identifier("t"), [
1239
- t.stringLiteral(text2),
1240
- t.stringLiteral(key)
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 !== "StringLiteral") return;
1256
- const text2 = valueNode.value;
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
- path.node.value = t.callExpression(t.identifier("t"), [
1260
- t.stringLiteral(text2),
1261
- t.stringLiteral(key)
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
- const compName = getComponentName(path);
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(t.importSpecifier(t.identifier("T"), t.identifier("T")));
1736
+ specifiers.push(t2.importSpecifier(t2.identifier("T"), t2.identifier("T")));
1282
1737
  }
1283
1738
  if (needsHook && !existing.hasHook) {
1284
1739
  specifiers.push(
1285
- t.importSpecifier(t.identifier(hookName), t.identifier(hookName))
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 = t.importDeclaration(
1754
+ const importDecl = t2.importDeclaration(
1300
1755
  specifiers,
1301
- t.stringLiteral(importPath)
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 && !hasInlineHookCall(ast, hookName)) {
1316
- const hookCall = isClient ? t.callExpression(t.identifier("useT"), []) : t.callExpression(t.identifier("createT"), []);
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 = t.variableDeclaration("const", [
1352
- t.variableDeclarator(t.identifier("t"), hookCall)
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 { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1370
- import { glob as glob2 } from "tinyglobby";
1371
- async function codegen(options, cwd = process.cwd()) {
1372
- const files = await glob2(options.include, {
1373
- ignore: options.exclude ?? [],
1374
- cwd,
1375
- absolute: true
1376
- });
1377
- let filesModified = 0;
1378
- let stringsWrapped = 0;
1379
- const transformOpts = {
1380
- i18nImport: options.i18nImport,
1381
- mode: options.mode,
1382
- componentPath: options.componentPath
1383
- };
1384
- for (const filePath of files) {
1385
- const code = await readFile3(filePath, "utf-8");
1386
- let ast;
1387
- try {
1388
- ast = parseFile(code, filePath);
1389
- } catch (err) {
1390
- logVerbose(
1391
- `Skipping unparseable file ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
1392
- true
1393
- );
1394
- continue;
1395
- }
1396
- const result = transform(ast, options.textToKey, transformOpts);
1397
- if (result.modified) {
1398
- await writeFile2(filePath, result.code, "utf-8");
1399
- filesModified++;
1400
- stringsWrapped += result.stringsWrapped;
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
- var init_codegen = __esm({
1410
- "src/codegen/index.ts"() {
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, SERVER_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
- if (!id) return text;
1487
- return msgs[id] ?? text;
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 join3, relative } from "path";
1518
- import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
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(join3(cwd, "app")))
2812
+ if (existsSync(join5(cwd, "app")))
1522
2813
  patterns.push("app/**/*.tsx", "app/**/*.jsx");
1523
- if (existsSync(join3(cwd, "src")))
2814
+ if (existsSync(join5(cwd, "src")))
1524
2815
  patterns.push("src/**/*.tsx", "src/**/*.jsx");
1525
- if (existsSync(join3(cwd, "pages")))
2816
+ if (existsSync(join5(cwd, "pages")))
1526
2817
  patterns.push("pages/**/*.tsx", "pages/**/*.jsx");
1527
- if (existsSync(join3(cwd, "src", "app"))) {
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(join3(dir, "node_modules", ...parts, "package.json"))) {
2831
+ if (existsSync(join5(dir, "node_modules", ...parts, "package.json"))) {
1541
2832
  return true;
1542
2833
  }
1543
- const parent = join3(dir, "..");
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, original, modified, label) {
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 writeFile3(filePath, modified, "utf-8");
2910
+ await writeFile4(filePath, modified, "utf-8");
1620
2911
  return true;
1621
2912
  }
1622
2913
  function detectSrcDir(cwd) {
1623
- return existsSync(join3(cwd, "src", "app"));
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(join3(cwd, "src"));
1629
- return join3(cwd, useSrc ? "src" : "", rel);
2919
+ const useSrc = existsSync(join5(cwd, "src"));
2920
+ return join5(cwd, useSrc ? "src" : "", rel);
1630
2921
  }
1631
2922
  if (componentPath.startsWith("~/")) {
1632
- return join3(cwd, componentPath.slice(2));
2923
+ return join5(cwd, componentPath.slice(2));
1633
2924
  }
1634
- return join3(cwd, componentPath);
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 = join3(base, "app", `layout.${ext}`);
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 mkdir2(msgDir, { recursive: true });
2935
+ await mkdir3(msgDir, { recursive: true });
1645
2936
  for (const locale of locales) {
1646
- const msgFile = join3(msgDir, `${locale}.json`);
2937
+ const msgFile = join5(msgDir, `${locale}.json`);
1647
2938
  if (!existsSync(msgFile)) {
1648
- await writeFile3(msgFile, "{}\n", "utf-8");
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 ? join3(cwd, "src") : cwd;
2957
+ const base = useSrc ? join5(cwd, "src") : cwd;
1667
2958
  const allLocales = [sourceLocale, ...targetLocales];
1668
2959
  const filesCreated = [];
1669
- const i18nDir = join3(base, "i18n");
1670
- await mkdir2(i18nDir, { recursive: true });
1671
- const requestFile = join3(i18nDir, "request.ts");
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, join3(cwd, messagesDir));
2964
+ const relMessages = relative(i18nDir, join5(cwd, messagesDir));
1674
2965
  const allLocalesStr = allLocales.map((l) => `"${l}"`).join(", ");
1675
- await writeFile3(
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 = join3(cwd, "next.config.ts");
3005
+ const nextConfigPath = join5(cwd, "next.config.ts");
1715
3006
  if (existsSync(nextConfigPath)) {
1716
- const content = await readFile4(nextConfigPath, "utf-8");
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 readFile4(layoutPath, "utf-8");
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(join3(cwd, messagesDir), allLocales);
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 = join3(fsPath, "..");
1775
- await mkdir2(dir, { recursive: true });
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
- await writeFile3(clientFile, CLIENT_TEMPLATE, "utf-8");
1779
- await writeFile3(serverFile, SERVER_TEMPLATE, "utf-8");
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(join3(cwd, "src"));
1786
- const base = useSrc ? join3(cwd, "src") : cwd;
3074
+ const useSrc = existsSync(join5(cwd, "src"));
3075
+ const base = useSrc ? join5(cwd, "src") : cwd;
1787
3076
  const filesCreated = [];
1788
- const i18nDir = join3(base, "i18n");
1789
- await mkdir2(i18nDir, { recursive: true });
1790
- const helperFile = join3(i18nDir, "index.ts");
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 writeFile3(helperFile, helperContent, "utf-8");
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 readFile4(layoutPath, "utf-8");
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(join3(cwd, messagesDir), [
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 = join3(cwd, "translate-kit.config.ts");
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 writeFile3(configPath, configContent, "utf-8");
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 { model } = config;
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 scan(scanOptions, cwd);
2005
- const transformableStrings = scanResult.strings.filter(
2006
- (s) => s.type === "jsx-text" || s.type === "jsx-attribute" || s.type === "object-property"
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
- `Scanning... ${transformableStrings.length} strings from ${scanResult.fileCount} files`
3306
+ `Found ${scanResult.bareStringCount} strings from ${scanResult.fileCount} files`
2010
3307
  );
2011
- if (transformableStrings.length === 0) {
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 codegen(
2056
- {
2057
- include: includePatterns,
2058
- exclude: ["**/*.test.*", "**/*.spec.*"],
2059
- textToKey,
2060
- i18nImport,
2061
- mode,
2062
- componentPath
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
- const translated = await translateAll({
2114
- model,
2115
- entries: sourceFlat,
2116
- sourceLocale,
2117
- targetLocale: locale,
2118
- options: translationOpts
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
- init_scanner();
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
- init_key_ai();
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 join4 } from "path";
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 = join4(messagesDir, `${sourceLocale}.json`);
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 ${join4(messagesDir, `${sourceLocale}.json`)}`
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
- for (const locale of locales) {
2277
- const start = Date.now();
2278
- const targetFile = join4(messagesDir, `${locale}.json`);
2279
- logLocaleStart(locale);
2280
- const targetRaw = await loadJsonFile(targetFile);
2281
- const targetFlat = flatten(targetRaw);
2282
- let lockData = await loadLockFile(messagesDir);
2283
- if (args.force) {
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
- const finalFlat = {
2323
- ...diffResult.unchanged,
2324
- ...translated
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: Object.keys(translated).length,
2340
- cached: Object.keys(diffResult.unchanged).length,
2341
- removed: diffResult.removed.length,
2342
- errors,
2343
- duration: Date.now() - start
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
- if (!args["dry-run"]) {
2349
- logSummary(results);
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 existingMap = await loadMapFile(config.messagesDir);
2395
- if (mode === "inline") {
2396
- const existingTComponents = result.strings.filter(
2397
- (s) => s.type === "T-component" && s.id
2398
- );
2399
- for (const tc of existingTComponents) {
2400
- if (tc.id && !(tc.text in existingMap)) {
2401
- existingMap[tc.text] = tc.id;
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
- await writeMapFile(config.messagesDir, textToKey);
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 messages = {};
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 codegen({
2496
- include: config.scan.include,
2497
- exclude: config.scan.exclude,
2498
- textToKey,
2499
- i18nImport: config.scan.i18nImport,
2500
- mode,
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": false,
2537
- force: false,
2538
- verbose: false,
2539
- locale: ""
3711
+ "dry-run": dryRun,
3712
+ force,
3713
+ verbose,
3714
+ locale: locale ?? ""
2540
3715
  },
2541
3716
  rawArgs,
2542
3717
  cmd: translateCommand