translate-kit 0.1.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 ADDED
@@ -0,0 +1,2548 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/config.ts
13
+ import { loadConfig } from "c12";
14
+ import { z } from "zod";
15
+ async function loadTranslateKitConfig() {
16
+ const { config } = await loadConfig({
17
+ name: "translate-kit"
18
+ });
19
+ if (!config || Object.keys(config).length === 0) {
20
+ throw new Error(
21
+ "No config found. Create a translate-kit.config.ts file or run `translate-kit init`."
22
+ );
23
+ }
24
+ const result = configSchema.safeParse(config);
25
+ if (!result.success) {
26
+ const errors = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
27
+ throw new Error(`Invalid config:
28
+ ${errors}`);
29
+ }
30
+ return result.data;
31
+ }
32
+ var configSchema;
33
+ var init_config = __esm({
34
+ "src/config.ts"() {
35
+ "use strict";
36
+ configSchema = z.object({
37
+ model: z.custom(
38
+ (val) => val != null && typeof val === "object",
39
+ { message: "model must be an AI SDK LanguageModel instance" }
40
+ ),
41
+ mode: z.enum(["keys", "inline"]).default("keys"),
42
+ sourceLocale: z.string().min(1),
43
+ targetLocales: z.array(z.string().min(1)).min(1),
44
+ messagesDir: z.string().min(1),
45
+ translation: z.object({
46
+ batchSize: z.number().int().positive().default(50),
47
+ context: z.string().optional(),
48
+ glossary: z.record(z.string()).optional(),
49
+ tone: z.string().optional(),
50
+ retries: z.number().int().min(0).default(2),
51
+ concurrency: z.number().int().positive().default(3)
52
+ }).optional(),
53
+ scan: z.object({
54
+ include: z.array(z.string()),
55
+ exclude: z.array(z.string()).optional(),
56
+ keyStrategy: z.enum(["hash", "path"]).default("hash"),
57
+ translatableProps: z.array(z.string()).default(["placeholder", "title", "alt", "aria-label"]),
58
+ i18nImport: z.string().default("next-intl")
59
+ }).optional(),
60
+ inline: z.object({
61
+ componentPath: z.string().min(1)
62
+ }).optional()
63
+ }).refine((data) => data.mode !== "inline" || data.inline != null, {
64
+ message: "inline options are required when mode is 'inline'",
65
+ path: ["inline"]
66
+ });
67
+ }
68
+ });
69
+
70
+ // src/flatten.ts
71
+ function flatten(obj, prefix = "") {
72
+ const result = {};
73
+ for (const [key, value] of Object.entries(obj)) {
74
+ const fullKey = prefix ? `${prefix}.${key}` : key;
75
+ if (typeof value === "string") {
76
+ result[fullKey] = value;
77
+ } else if (value != null && typeof value === "object" && !Array.isArray(value)) {
78
+ Object.assign(result, flatten(value, fullKey));
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ function unflatten(obj) {
84
+ const result = {};
85
+ for (const [key, value] of Object.entries(obj)) {
86
+ const parts = key.split(".");
87
+ let current = result;
88
+ for (let i = 0; i < parts.length - 1; i++) {
89
+ const part = parts[i];
90
+ if (part in current && typeof current[part] !== "object") {
91
+ console.warn(
92
+ `[translate-kit] Key conflict: "${parts.slice(0, i + 1).join(".")}" is a value but "${key}" expects it to be an object`
93
+ );
94
+ current[part] = {};
95
+ } else if (!(part in current)) {
96
+ current[part] = {};
97
+ }
98
+ current = current[part];
99
+ }
100
+ current[parts[parts.length - 1]] = value;
101
+ }
102
+ return result;
103
+ }
104
+ var init_flatten = __esm({
105
+ "src/flatten.ts"() {
106
+ "use strict";
107
+ }
108
+ });
109
+
110
+ // src/diff.ts
111
+ import { createHash } from "crypto";
112
+ import { readFile } from "fs/promises";
113
+ import { join } from "path";
114
+ function hashValue(value) {
115
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
116
+ }
117
+ function isFileNotFound(err) {
118
+ return err instanceof Error && "code" in err && err.code === "ENOENT";
119
+ }
120
+ async function loadJsonFile(filePath) {
121
+ try {
122
+ const content = await readFile(filePath, "utf-8");
123
+ return JSON.parse(content);
124
+ } catch (err) {
125
+ if (isFileNotFound(err)) return {};
126
+ throw new Error(
127
+ `Failed to load ${filePath}: ${err instanceof Error ? err.message : String(err)}`
128
+ );
129
+ }
130
+ }
131
+ async function loadLockFile(messagesDir) {
132
+ const lockPath = join(messagesDir, ".translate-lock.json");
133
+ try {
134
+ const content = await readFile(lockPath, "utf-8");
135
+ return JSON.parse(content);
136
+ } catch (err) {
137
+ if (isFileNotFound(err)) return {};
138
+ throw new Error(
139
+ `Failed to load lock file: ${err instanceof Error ? err.message : String(err)}`
140
+ );
141
+ }
142
+ }
143
+ function computeDiff(sourceFlat, targetFlat, lockData) {
144
+ const added = {};
145
+ const modified = {};
146
+ const removed = [];
147
+ const unchanged = {};
148
+ for (const [key, value] of Object.entries(sourceFlat)) {
149
+ const currentHash = hashValue(value);
150
+ const lockedHash = lockData[key];
151
+ if (!(key in targetFlat)) {
152
+ added[key] = value;
153
+ } else if (!lockedHash || lockedHash !== currentHash) {
154
+ modified[key] = value;
155
+ } else {
156
+ unchanged[key] = targetFlat[key];
157
+ }
158
+ }
159
+ for (const key of Object.keys(targetFlat)) {
160
+ if (!(key in sourceFlat)) {
161
+ removed.push(key);
162
+ }
163
+ }
164
+ return { added, modified, removed, unchanged };
165
+ }
166
+ var init_diff = __esm({
167
+ "src/diff.ts"() {
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
+ }
315
+ });
316
+
317
+ // src/scanner/parser.ts
318
+ import { parse } from "@babel/parser";
319
+ function parseFile(code, filename) {
320
+ return parse(code, {
321
+ sourceType: "module",
322
+ plugins: filename.endsWith(".tsx") || filename.endsWith(".ts") ? plugins : plugins.filter((p2) => p2 !== "typescript"),
323
+ sourceFilename: filename
324
+ });
325
+ }
326
+ var plugins;
327
+ var init_parser = __esm({
328
+ "src/scanner/parser.ts"() {
329
+ "use strict";
330
+ plugins = ["typescript", "jsx", "decorators-legacy"];
331
+ }
332
+ });
333
+
334
+ // src/scanner/filters.ts
335
+ function isContentProperty(propName) {
336
+ return CONTENT_PROPERTY_NAMES.includes(propName);
337
+ }
338
+ function isTranslatableProp(propName, customProps) {
339
+ if (NEVER_TRANSLATE_PROPS.includes(propName)) return false;
340
+ const allowed = customProps ?? DEFAULT_TRANSLATABLE_PROPS;
341
+ return allowed.includes(propName);
342
+ }
343
+ function isIgnoredTag(tagName) {
344
+ return IGNORE_TAGS.includes(tagName.toLowerCase());
345
+ }
346
+ function shouldIgnore(text2) {
347
+ const trimmed = text2.trim();
348
+ if (trimmed.length === 0) return true;
349
+ return IGNORE_PATTERNS.some((pattern) => pattern.test(trimmed));
350
+ }
351
+ var DEFAULT_TRANSLATABLE_PROPS, NEVER_TRANSLATE_PROPS, IGNORE_TAGS, IGNORE_PATTERNS, CONTENT_PROPERTY_NAMES;
352
+ var init_filters = __esm({
353
+ "src/scanner/filters.ts"() {
354
+ "use strict";
355
+ DEFAULT_TRANSLATABLE_PROPS = [
356
+ "placeholder",
357
+ "title",
358
+ "alt",
359
+ "aria-label",
360
+ "aria-description",
361
+ "aria-placeholder",
362
+ "label"
363
+ ];
364
+ NEVER_TRANSLATE_PROPS = [
365
+ "className",
366
+ "class",
367
+ "id",
368
+ "key",
369
+ "ref",
370
+ "href",
371
+ "src",
372
+ "type",
373
+ "name",
374
+ "value",
375
+ "htmlFor",
376
+ "for",
377
+ "role",
378
+ "style",
379
+ "data-testid",
380
+ "data-cy",
381
+ "onClick",
382
+ "onChange",
383
+ "onSubmit",
384
+ "onFocus",
385
+ "onBlur"
386
+ ];
387
+ IGNORE_TAGS = [
388
+ "script",
389
+ "style",
390
+ "code",
391
+ "pre",
392
+ "svg",
393
+ "path",
394
+ "circle",
395
+ "rect",
396
+ "line",
397
+ "polyline",
398
+ "polygon"
399
+ ];
400
+ IGNORE_PATTERNS = [
401
+ /^\s*$/,
402
+ // Whitespace only
403
+ /^https?:\/\//,
404
+ // URLs
405
+ /^[a-z]+(-[a-z]+)+$/,
406
+ // kebab-case identifiers
407
+ /^[A-Z_]+$/,
408
+ // CONSTANT_CASE
409
+ /^[\d.,%$€£¥]+$/,
410
+ // Numbers, currency
411
+ /^[^a-zA-Z]*$/
412
+ // No letters at all
413
+ ];
414
+ CONTENT_PROPERTY_NAMES = [
415
+ "title",
416
+ "description",
417
+ "label",
418
+ "text",
419
+ "content",
420
+ "heading",
421
+ "subtitle",
422
+ "caption",
423
+ "summary",
424
+ "message",
425
+ "placeholder",
426
+ "alt"
427
+ ];
428
+ }
429
+ });
430
+
431
+ // src/utils/ast-helpers.ts
432
+ function resolveDefault(mod) {
433
+ if (typeof mod === "function") return mod;
434
+ return mod.default;
435
+ }
436
+ function isInsideFunction(path) {
437
+ let current = path.parentPath;
438
+ while (current) {
439
+ if (current.isFunctionDeclaration() || current.isFunctionExpression() || current.isArrowFunctionExpression()) {
440
+ return true;
441
+ }
442
+ current = current.parentPath;
443
+ }
444
+ return false;
445
+ }
446
+ function getComponentName(path) {
447
+ let current = path;
448
+ while (current) {
449
+ if (current.isFunctionDeclaration() && current.node.id) {
450
+ return current.node.id.name;
451
+ }
452
+ if (current.isVariableDeclarator() && current.node.id?.type === "Identifier") {
453
+ const init = current.node.init;
454
+ if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
455
+ return current.node.id.name;
456
+ }
457
+ }
458
+ if (current.isExportDefaultDeclaration()) {
459
+ const decl = current.node.declaration;
460
+ if (decl.type === "FunctionDeclaration" && decl.id) {
461
+ return decl.id.name;
462
+ }
463
+ }
464
+ current = current.parentPath;
465
+ }
466
+ return void 0;
467
+ }
468
+ function getParentTagName(path) {
469
+ let current = path.parentPath;
470
+ while (current) {
471
+ if (current.isJSXElement()) {
472
+ const opening = current.node.openingElement;
473
+ if (opening.name.type === "JSXIdentifier") {
474
+ return opening.name.name;
475
+ }
476
+ if (opening.name.type === "JSXMemberExpression" && opening.name.object.type === "JSXIdentifier") {
477
+ return `${opening.name.object.name}.${opening.name.property.name}`;
478
+ }
479
+ }
480
+ current = current.parentPath;
481
+ }
482
+ return void 0;
483
+ }
484
+ var init_ast_helpers = __esm({
485
+ "src/utils/ast-helpers.ts"() {
486
+ "use strict";
487
+ }
488
+ });
489
+
490
+ // src/scanner/extractor.ts
491
+ import _traverse from "@babel/traverse";
492
+ function extractStrings(ast, filePath, translatableProps) {
493
+ const results = [];
494
+ traverse(ast, {
495
+ JSXText(path) {
496
+ const text2 = path.node.value.trim();
497
+ if (shouldIgnore(text2)) return;
498
+ const parentTag = getParentTagName(path);
499
+ if (parentTag && isIgnoredTag(parentTag)) return;
500
+ if (parentTag === "T") return;
501
+ results.push({
502
+ text: text2,
503
+ type: "jsx-text",
504
+ file: filePath,
505
+ line: path.node.loc?.start.line ?? 0,
506
+ column: path.node.loc?.start.column ?? 0,
507
+ componentName: getComponentName(path),
508
+ parentTag
509
+ });
510
+ },
511
+ JSXAttribute(path) {
512
+ const name = path.node.name;
513
+ const propName = name.type === "JSXIdentifier" ? name.name : name.name.name;
514
+ if (!isTranslatableProp(propName, translatableProps)) return;
515
+ const value = path.node.value;
516
+ if (!value) return;
517
+ let text2;
518
+ if (value.type === "StringLiteral") {
519
+ text2 = value.value;
520
+ } else if (value.type === "JSXExpressionContainer" && value.expression.type === "StringLiteral") {
521
+ text2 = value.expression.value;
522
+ }
523
+ if (!text2 || shouldIgnore(text2)) return;
524
+ const parentTag = getParentTagName(path);
525
+ if (parentTag && isIgnoredTag(parentTag)) return;
526
+ results.push({
527
+ text: text2,
528
+ type: "jsx-attribute",
529
+ file: filePath,
530
+ line: path.node.loc?.start.line ?? 0,
531
+ column: path.node.loc?.start.column ?? 0,
532
+ componentName: getComponentName(path),
533
+ propName,
534
+ parentTag
535
+ });
536
+ },
537
+ 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
+ if (path.parent.type === "JSXAttribute") return;
543
+ const parentTag = getParentTagName(path);
544
+ if (parentTag && isIgnoredTag(parentTag)) return;
545
+ results.push({
546
+ text: text2,
547
+ type: "jsx-expression",
548
+ file: filePath,
549
+ line: path.node.loc?.start.line ?? 0,
550
+ column: path.node.loc?.start.column ?? 0,
551
+ componentName: getComponentName(path),
552
+ parentTag
553
+ });
554
+ },
555
+ ObjectProperty(path) {
556
+ if (!isInsideFunction(path)) return;
557
+ const keyNode = path.node.key;
558
+ if (keyNode.type !== "Identifier" && keyNode.type !== "StringLiteral")
559
+ return;
560
+ const propName = keyNode.type === "Identifier" ? keyNode.name : keyNode.value;
561
+ if (!isContentProperty(propName)) return;
562
+ const valueNode = path.node.value;
563
+ if (valueNode.type !== "StringLiteral") return;
564
+ const text2 = valueNode.value.trim();
565
+ if (shouldIgnore(text2)) return;
566
+ results.push({
567
+ text: text2,
568
+ type: "object-property",
569
+ file: filePath,
570
+ line: valueNode.loc?.start.line ?? 0,
571
+ column: valueNode.loc?.start.column ?? 0,
572
+ componentName: getComponentName(path),
573
+ propName
574
+ });
575
+ },
576
+ CallExpression(path) {
577
+ const callee = path.node.callee;
578
+ if (callee.type !== "Identifier" || callee.name !== "t") return;
579
+ const args = path.node.arguments;
580
+ if (args.length === 0) return;
581
+ const firstArg = args[0];
582
+ if (firstArg.type !== "StringLiteral") return;
583
+ if (args.length >= 2 && args[1].type === "StringLiteral") {
584
+ results.push({
585
+ text: firstArg.value,
586
+ type: "t-call",
587
+ file: filePath,
588
+ line: path.node.loc?.start.line ?? 0,
589
+ column: path.node.loc?.start.column ?? 0,
590
+ componentName: getComponentName(path),
591
+ parentTag: getParentTagName(path),
592
+ id: args[1].value
593
+ });
594
+ return;
595
+ }
596
+ results.push({
597
+ text: firstArg.value,
598
+ type: "t-call",
599
+ file: filePath,
600
+ line: path.node.loc?.start.line ?? 0,
601
+ column: path.node.loc?.start.column ?? 0,
602
+ componentName: getComponentName(path),
603
+ parentTag: getParentTagName(path)
604
+ });
605
+ },
606
+ JSXElement(path) {
607
+ const opening = path.node.openingElement;
608
+ if (opening.name.type !== "JSXIdentifier" || opening.name.name !== "T")
609
+ return;
610
+ let id;
611
+ for (const attr of opening.attributes) {
612
+ if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === "id" && attr.value?.type === "StringLiteral") {
613
+ id = attr.value.value;
614
+ }
615
+ }
616
+ let text2 = "";
617
+ for (const child of path.node.children) {
618
+ if (child.type === "JSXText") {
619
+ text2 += child.value;
620
+ }
621
+ }
622
+ text2 = text2.trim();
623
+ if (!text2) return;
624
+ results.push({
625
+ text: text2,
626
+ type: "T-component",
627
+ file: filePath,
628
+ line: path.node.loc?.start.line ?? 0,
629
+ column: path.node.loc?.start.column ?? 0,
630
+ componentName: getComponentName(path),
631
+ parentTag: getParentTagName(path),
632
+ id
633
+ });
634
+ }
635
+ });
636
+ return results;
637
+ }
638
+ var traverse;
639
+ var init_extractor = __esm({
640
+ "src/scanner/extractor.ts"() {
641
+ "use strict";
642
+ init_filters();
643
+ init_ast_helpers();
644
+ traverse = resolveDefault(_traverse);
645
+ }
646
+ });
647
+
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);
652
+ }
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(".");
668
+ }
669
+ var init_key_generator = __esm({
670
+ "src/scanner/key-generator.ts"() {
671
+ "use strict";
672
+ }
673
+ });
674
+
675
+ // src/logger.ts
676
+ import pc from "picocolors";
677
+ function logStart(sourceLocale, targetLocales) {
678
+ console.log(
679
+ `
680
+ ${pc.bold("translate-kit")} ${pc.dim("\xB7")} ${sourceLocale} ${pc.dim("\u2192")} ${targetLocales.join(", ")}
681
+ `
682
+ );
683
+ }
684
+ function logLocaleStart(locale) {
685
+ console.log(`${pc.cyan("\u25CF")} ${pc.bold(locale)}`);
686
+ }
687
+ function logLocaleResult(result) {
688
+ const parts = [];
689
+ if (result.translated > 0) {
690
+ parts.push(pc.green(`${result.translated} translated`));
691
+ }
692
+ if (result.cached > 0) {
693
+ parts.push(pc.dim(`${result.cached} cached`));
694
+ }
695
+ if (result.removed > 0) {
696
+ parts.push(pc.yellow(`${result.removed} removed`));
697
+ }
698
+ if (result.errors > 0) {
699
+ parts.push(pc.red(`${result.errors} errors`));
700
+ }
701
+ const time = pc.dim(`${(result.duration / 1e3).toFixed(1)}s`);
702
+ console.log(` ${parts.join(pc.dim(" \xB7 "))} ${time}`);
703
+ }
704
+ function logSummary(results) {
705
+ const totalTranslated = results.reduce((s, r) => s + r.translated, 0);
706
+ const totalCached = results.reduce((s, r) => s + r.cached, 0);
707
+ const totalDuration = results.reduce((s, r) => s + r.duration, 0);
708
+ console.log(
709
+ `
710
+ ${pc.bold("Done!")} ${totalTranslated} keys translated, ${totalCached} cached ${pc.dim(`(${(totalDuration / 1e3).toFixed(1)}s)`)}
711
+ `
712
+ );
713
+ }
714
+ function logDryRun(locale, added, modified, removed, unchanged) {
715
+ console.log(`${pc.cyan("\u25CF")} ${pc.bold(locale)} ${pc.dim("(dry run)")}`);
716
+ console.log(
717
+ ` ${pc.green(`+${added}`)} added, ${pc.yellow(`~${modified}`)} modified, ${pc.red(`-${removed}`)} removed, ${pc.dim(`${unchanged} unchanged`)}`
718
+ );
719
+ }
720
+ function logScanResult(total, files) {
721
+ console.log(
722
+ `
723
+ ${pc.bold("Scan complete:")} ${pc.green(`${total} strings`)} from ${files} files
724
+ `
725
+ );
726
+ }
727
+ function logError(message) {
728
+ console.error(`${pc.red("\u2716")} ${message}`);
729
+ }
730
+ function logWarning(message) {
731
+ console.log(`${pc.yellow("\u26A0")} ${message}`);
732
+ }
733
+ function logInfo(message) {
734
+ console.log(` ${message}`);
735
+ }
736
+ function logSuccess(message) {
737
+ console.log(`${pc.green("\u2714")} ${message}`);
738
+ }
739
+ function logVerbose(message, verbose) {
740
+ if (verbose) {
741
+ console.log(pc.dim(` ${message}`));
742
+ }
743
+ }
744
+ var init_logger = __esm({
745
+ "src/logger.ts"() {
746
+ "use strict";
747
+ }
748
+ });
749
+
750
+ // src/scanner/index.ts
751
+ import { readFile as readFile2 } from "fs/promises";
752
+ import { glob } from "tinyglobby";
753
+ async function scan(options, cwd = process.cwd()) {
754
+ const files = await glob(options.include, {
755
+ ignore: options.exclude ?? [],
756
+ cwd,
757
+ absolute: true
758
+ });
759
+ 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
+ }
782
+ }
783
+ return {
784
+ strings: allStrings,
785
+ messages,
786
+ fileCount: files.length
787
+ };
788
+ }
789
+ var init_scanner = __esm({
790
+ "src/scanner/index.ts"() {
791
+ "use strict";
792
+ init_parser();
793
+ init_extractor();
794
+ init_key_generator();
795
+ init_logger();
796
+ }
797
+ });
798
+
799
+ // src/scanner/key-ai.ts
800
+ import { generateObject as generateObject2 } from "ai";
801
+ import { z as z3 } from "zod";
802
+ import pLimit2 from "p-limit";
803
+ function buildPrompt2(strings) {
804
+ const lines = [
805
+ "Generate semantic i18n keys for these UI strings.",
806
+ "",
807
+ "Rules:",
808
+ "- Use dot notation with max 2 levels (namespace.key)",
809
+ "- Group by feature/section based on file path and component",
810
+ "- Use camelCase for key segments",
811
+ '- Common UI strings use "common." prefix (Save, Cancel, Loading, Submit, Close, Delete, Edit, Back, Next, etc.)',
812
+ '- Auth-related use "auth." prefix (Sign in, Log out, Register, Forgot password, etc.)',
813
+ '- Navigation use "nav." prefix',
814
+ '- Form-related use "form." prefix for generic form labels',
815
+ '- Error messages use "error." prefix',
816
+ "- Be consistent: same text should always get the same key",
817
+ "- Keys should be concise but descriptive",
818
+ "",
819
+ "Strings:"
820
+ ];
821
+ for (let i = 0; i < strings.length; i++) {
822
+ const str = strings[i];
823
+ const parts = [`[${i}] "${str.text}"`];
824
+ if (str.componentName) parts.push(`component: ${str.componentName}`);
825
+ if (str.parentTag) parts.push(`tag: ${str.parentTag}`);
826
+ if (str.propName) parts.push(`prop: ${str.propName}`);
827
+ if (str.file) parts.push(`file: ${str.file}`);
828
+ lines.push(` ${parts.join(", ")}`);
829
+ }
830
+ return lines.join("\n");
831
+ }
832
+ async function generateKeysBatchWithRetry(model, strings, retries) {
833
+ const prompt = buildPrompt2(strings);
834
+ 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")
840
+ })
841
+ )
842
+ });
843
+ let lastError;
844
+ for (let attempt = 0; attempt <= retries; attempt++) {
845
+ try {
846
+ const { object } = await generateObject2({
847
+ model,
848
+ prompt,
849
+ schema
850
+ });
851
+ const result = {};
852
+ for (const mapping of object.mappings) {
853
+ if (mapping.index >= 0 && mapping.index < texts.length) {
854
+ result[texts[mapping.index]] = mapping.key;
855
+ }
856
+ }
857
+ return result;
858
+ } catch (error) {
859
+ lastError = error;
860
+ if (attempt < retries) {
861
+ const delay = Math.min(Math.pow(2, attempt) * 1e3, 3e4);
862
+ await new Promise((resolve) => setTimeout(resolve, delay));
863
+ }
864
+ }
865
+ }
866
+ throw lastError;
867
+ }
868
+ function resolveCollisions(newKeys, existingMap) {
869
+ const usedKeys = new Set(Object.values(existingMap));
870
+ const result = {};
871
+ for (const [text2, key] of Object.entries(newKeys)) {
872
+ let finalKey = key;
873
+ let suffix = 2;
874
+ while (usedKeys.has(finalKey)) {
875
+ finalKey = `${key}${suffix}`;
876
+ suffix++;
877
+ }
878
+ usedKeys.add(finalKey);
879
+ result[text2] = finalKey;
880
+ }
881
+ return result;
882
+ }
883
+ async function generateSemanticKeys(input) {
884
+ const {
885
+ model,
886
+ strings,
887
+ existingMap = {},
888
+ batchSize = 50,
889
+ concurrency = 3,
890
+ retries = 2
891
+ } = input;
892
+ const newStrings = strings.filter((s) => !(s.text in existingMap));
893
+ if (newStrings.length === 0) return { ...existingMap };
894
+ const uniqueMap = /* @__PURE__ */ new Map();
895
+ for (const str of newStrings) {
896
+ if (!uniqueMap.has(str.text)) {
897
+ uniqueMap.set(str.text, str);
898
+ }
899
+ }
900
+ const uniqueStrings = Array.from(uniqueMap.values());
901
+ const limit = pLimit2(concurrency);
902
+ const batches = [];
903
+ for (let i = 0; i < uniqueStrings.length; i += batchSize) {
904
+ batches.push(uniqueStrings.slice(i, i + batchSize));
905
+ }
906
+ const allNewKeys = {};
907
+ await Promise.all(
908
+ batches.map(
909
+ (batch) => limit(async () => {
910
+ const keys = await generateKeysBatchWithRetry(model, batch, retries);
911
+ Object.assign(allNewKeys, keys);
912
+ })
913
+ )
914
+ );
915
+ const resolved = resolveCollisions(allNewKeys, existingMap);
916
+ return { ...existingMap, ...resolved };
917
+ }
918
+ var init_key_ai = __esm({
919
+ "src/scanner/key-ai.ts"() {
920
+ "use strict";
921
+ }
922
+ });
923
+
924
+ // src/codegen/transform.ts
925
+ import _traverse2 from "@babel/traverse";
926
+ import _generate from "@babel/generator";
927
+ import * as t from "@babel/types";
928
+ function findLastImportIndex(ast) {
929
+ let idx = -1;
930
+ for (let i = 0; i < ast.program.body.length; i++) {
931
+ if (ast.program.body[i].type === "ImportDeclaration") {
932
+ idx = i;
933
+ }
934
+ }
935
+ return idx;
936
+ }
937
+ function hasUseTranslationsImport(ast, importSource) {
938
+ for (const node of ast.program.body) {
939
+ if (node.type === "ImportDeclaration" && node.source.value === importSource) {
940
+ for (const spec of node.specifiers) {
941
+ if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "useTranslations") {
942
+ return true;
943
+ }
944
+ }
945
+ }
946
+ }
947
+ return false;
948
+ }
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();
957
+ }
958
+ },
959
+ noScope: true
960
+ });
961
+ return found;
962
+ }
963
+ function transform(ast, textToKey, options = {}) {
964
+ if (options.mode === "inline") {
965
+ return transformInline(ast, textToKey, options);
966
+ }
967
+ const importSource = options.i18nImport ?? "next-intl";
968
+ let stringsWrapped = 0;
969
+ const componentsNeedingT = /* @__PURE__ */ new Set();
970
+ traverse2(ast, {
971
+ JSXText(path) {
972
+ const text2 = path.node.value.trim();
973
+ if (!text2 || !(text2 in textToKey)) return;
974
+ const parent = path.parentPath;
975
+ if (!parent?.isJSXElement()) return;
976
+ const key = textToKey[text2];
977
+ const tCall = t.jsxExpressionContainer(
978
+ t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
979
+ );
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) {
985
+ path.replaceWith(tCall);
986
+ } else {
987
+ const raw = path.node.value;
988
+ const hasLeading = raw !== raw.trimStart();
989
+ const hasTrailing = raw !== raw.trimEnd();
990
+ const nodes = [];
991
+ if (hasLeading) {
992
+ nodes.push(t.jsxExpressionContainer(t.stringLiteral(" ")));
993
+ }
994
+ nodes.push(tCall);
995
+ if (hasTrailing) {
996
+ nodes.push(t.jsxExpressionContainer(t.stringLiteral(" ")));
997
+ }
998
+ path.replaceWithMultiple(nodes);
999
+ }
1000
+ stringsWrapped++;
1001
+ const compName = getComponentName(path);
1002
+ if (compName) componentsNeedingT.add(compName);
1003
+ },
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") {
1015
+ return;
1016
+ }
1017
+ const key = textToKey[text2];
1018
+ path.node.value = t.jsxExpressionContainer(
1019
+ t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
1020
+ );
1021
+ stringsWrapped++;
1022
+ const compName = getComponentName(path);
1023
+ if (compName) componentsNeedingT.add(compName);
1024
+ },
1025
+ ObjectProperty(path) {
1026
+ if (!isInsideFunction(path)) return;
1027
+ const keyNode = path.node.key;
1028
+ if (keyNode.type !== "Identifier" && keyNode.type !== "StringLiteral")
1029
+ return;
1030
+ const propName = keyNode.type === "Identifier" ? keyNode.name : keyNode.value;
1031
+ if (!isContentProperty(propName)) return;
1032
+ const valueNode = path.node.value;
1033
+ if (valueNode.type !== "StringLiteral") return;
1034
+ const text2 = valueNode.value;
1035
+ if (!text2 || !(text2 in textToKey)) return;
1036
+ const key = textToKey[text2];
1037
+ path.node.value = t.callExpression(t.identifier("t"), [
1038
+ t.stringLiteral(key)
1039
+ ]);
1040
+ stringsWrapped++;
1041
+ const compName = getComponentName(path);
1042
+ if (compName) componentsNeedingT.add(compName);
1043
+ }
1044
+ });
1045
+ if (stringsWrapped === 0) {
1046
+ return { code: generate(ast).code, stringsWrapped: 0, modified: false };
1047
+ }
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);
1063
+ }
1064
+ }
1065
+ if (!hasUseTranslationsCall(ast)) {
1066
+ traverse2(ast, {
1067
+ FunctionDeclaration(path) {
1068
+ const name = path.node.id?.name;
1069
+ if (!name || !componentsNeedingT.has(name)) return;
1070
+ injectTDeclaration(path);
1071
+ },
1072
+ VariableDeclarator(path) {
1073
+ if (path.node.id.type !== "Identifier") return;
1074
+ const name = path.node.id.name;
1075
+ if (!componentsNeedingT.has(name)) return;
1076
+ const init = path.node.init;
1077
+ if (!init) return;
1078
+ if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
1079
+ if (init.body.type === "BlockStatement") {
1080
+ injectTIntoBlock(init.body);
1081
+ }
1082
+ }
1083
+ },
1084
+ noScope: true
1085
+ });
1086
+ }
1087
+ const output = generate(ast, { retainLines: false });
1088
+ return { code: output.code, stringsWrapped, modified: true };
1089
+ }
1090
+ function injectTDeclaration(path) {
1091
+ const body = path.node.body;
1092
+ if (body.type !== "BlockStatement") return;
1093
+ injectTIntoBlock(body);
1094
+ }
1095
+ function injectTIntoBlock(block) {
1096
+ for (const stmt of block.body) {
1097
+ 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"
1099
+ )) {
1100
+ return;
1101
+ }
1102
+ }
1103
+ const tDecl = t.variableDeclaration("const", [
1104
+ t.variableDeclarator(
1105
+ t.identifier("t"),
1106
+ t.callExpression(t.identifier("useTranslations"), [])
1107
+ )
1108
+ ]);
1109
+ block.body.unshift(tDecl);
1110
+ }
1111
+ function isClientFile(ast) {
1112
+ if (ast.program.directives) {
1113
+ for (const directive of ast.program.directives) {
1114
+ if (directive.value?.value === "use client") {
1115
+ return true;
1116
+ }
1117
+ }
1118
+ }
1119
+ for (const node of ast.program.body) {
1120
+ if (node.type === "ExpressionStatement" && node.expression.type === "StringLiteral" && node.expression.value === "use client") {
1121
+ return true;
1122
+ }
1123
+ }
1124
+ return false;
1125
+ }
1126
+ function hasInlineImport(ast, componentPath) {
1127
+ let hasT = false;
1128
+ let hasHook = false;
1129
+ for (const node of ast.program.body) {
1130
+ if (node.type !== "ImportDeclaration") continue;
1131
+ const src = node.source.value;
1132
+ if (src !== componentPath && src !== `${componentPath}-server` && src !== `${componentPath}/t-server`)
1133
+ continue;
1134
+ for (const spec of node.specifiers) {
1135
+ if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") {
1136
+ if (spec.imported.name === "T") hasT = true;
1137
+ if (spec.imported.name === "useT" || spec.imported.name === "createT")
1138
+ hasHook = true;
1139
+ }
1140
+ }
1141
+ }
1142
+ return { hasT, hasHook };
1143
+ }
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();
1152
+ }
1153
+ },
1154
+ noScope: true
1155
+ });
1156
+ return found;
1157
+ }
1158
+ function transformInline(ast, textToKey, options) {
1159
+ const componentPath = options.componentPath ?? "@/components/t";
1160
+ const isClient = isClientFile(ast);
1161
+ let stringsWrapped = 0;
1162
+ const componentsNeedingT = /* @__PURE__ */ new Set();
1163
+ let needsTComponent = false;
1164
+ let repaired = false;
1165
+ if (!isClient) {
1166
+ traverse2(ast, {
1167
+ CallExpression(path) {
1168
+ if (path.node.callee.type === "Identifier" && path.node.callee.name === "createT" && path.node.arguments.length > 0 && path.node.arguments[0].type === "Identifier") {
1169
+ const argName = path.node.arguments[0].name;
1170
+ if (!path.scope.hasBinding(argName)) {
1171
+ logWarning(
1172
+ `Repaired createT(${argName}) \u2192 createT() \u2014 "${argName}" was not in scope`
1173
+ );
1174
+ path.node.arguments = [];
1175
+ repaired = true;
1176
+ }
1177
+ }
1178
+ }
1179
+ });
1180
+ }
1181
+ traverse2(ast, {
1182
+ JSXText(path) {
1183
+ const text2 = path.node.value.trim();
1184
+ if (!text2 || !(text2 in textToKey)) return;
1185
+ const parent = path.parentPath;
1186
+ if (!parent?.isJSXElement()) return;
1187
+ const parentOpening = parent.node.openingElement;
1188
+ if (parentOpening.name.type === "JSXIdentifier" && parentOpening.name.name === "T") {
1189
+ return;
1190
+ }
1191
+ const key = textToKey[text2];
1192
+ needsTComponent = true;
1193
+ const tElement = t.jsxElement(
1194
+ t.jsxOpeningElement(t.jsxIdentifier("T"), [
1195
+ t.jsxAttribute(t.jsxIdentifier("id"), t.stringLiteral(key))
1196
+ ]),
1197
+ t.jsxClosingElement(t.jsxIdentifier("T")),
1198
+ [t.jsxText(text2)],
1199
+ false
1200
+ );
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) {
1206
+ path.replaceWith(tElement);
1207
+ } else {
1208
+ const raw = path.node.value;
1209
+ const hasLeading = raw !== raw.trimStart();
1210
+ const hasTrailing = raw !== raw.trimEnd();
1211
+ const nodes = [];
1212
+ if (hasLeading) {
1213
+ nodes.push(t.jsxExpressionContainer(t.stringLiteral(" ")));
1214
+ }
1215
+ nodes.push(tElement);
1216
+ if (hasTrailing) {
1217
+ nodes.push(t.jsxExpressionContainer(t.stringLiteral(" ")));
1218
+ }
1219
+ path.replaceWithMultiple(nodes);
1220
+ }
1221
+ stringsWrapped++;
1222
+ },
1223
+ JSXAttribute(path) {
1224
+ const value = path.node.value;
1225
+ if (!value) return;
1226
+ let text2;
1227
+ if (value.type === "StringLiteral") {
1228
+ text2 = value.value;
1229
+ } else if (value.type === "JSXExpressionContainer" && value.expression.type === "StringLiteral") {
1230
+ text2 = value.expression.value;
1231
+ }
1232
+ if (!text2 || !(text2 in textToKey)) return;
1233
+ if (value.type === "JSXExpressionContainer" && value.expression.type === "CallExpression" && value.expression.callee.type === "Identifier" && value.expression.callee.name === "t") {
1234
+ return;
1235
+ }
1236
+ 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
+ ])
1242
+ );
1243
+ stringsWrapped++;
1244
+ const compName = getComponentName(path);
1245
+ if (compName) componentsNeedingT.add(compName);
1246
+ },
1247
+ ObjectProperty(path) {
1248
+ if (!isInsideFunction(path)) return;
1249
+ const keyNode = path.node.key;
1250
+ if (keyNode.type !== "Identifier" && keyNode.type !== "StringLiteral")
1251
+ return;
1252
+ const propName = keyNode.type === "Identifier" ? keyNode.name : keyNode.value;
1253
+ if (!isContentProperty(propName)) return;
1254
+ const valueNode = path.node.value;
1255
+ if (valueNode.type !== "StringLiteral") return;
1256
+ const text2 = valueNode.value;
1257
+ if (!text2 || !(text2 in textToKey)) return;
1258
+ const key = textToKey[text2];
1259
+ path.node.value = t.callExpression(t.identifier("t"), [
1260
+ t.stringLiteral(text2),
1261
+ t.stringLiteral(key)
1262
+ ]);
1263
+ stringsWrapped++;
1264
+ const compName = getComponentName(path);
1265
+ if (compName) componentsNeedingT.add(compName);
1266
+ }
1267
+ });
1268
+ if (stringsWrapped === 0 && !repaired) {
1269
+ return { code: generate(ast).code, stringsWrapped: 0, modified: false };
1270
+ }
1271
+ if (stringsWrapped === 0 && repaired) {
1272
+ const output2 = generate(ast, { retainLines: false });
1273
+ return { code: output2.code, stringsWrapped: 0, modified: true };
1274
+ }
1275
+ const needsHook = componentsNeedingT.size > 0;
1276
+ const hookName = isClient ? "useT" : "createT";
1277
+ const importPath = isClient ? componentPath : `${componentPath}-server`;
1278
+ const existing = hasInlineImport(ast, componentPath);
1279
+ const specifiers = [];
1280
+ if (needsTComponent && !existing.hasT) {
1281
+ specifiers.push(t.importSpecifier(t.identifier("T"), t.identifier("T")));
1282
+ }
1283
+ if (needsHook && !existing.hasHook) {
1284
+ specifiers.push(
1285
+ t.importSpecifier(t.identifier(hookName), t.identifier(hookName))
1286
+ );
1287
+ }
1288
+ if (specifiers.length > 0) {
1289
+ let appended = false;
1290
+ for (const node of ast.program.body) {
1291
+ if (node.type === "ImportDeclaration" && (node.source.value === importPath || node.source.value === componentPath)) {
1292
+ node.specifiers.push(...specifiers);
1293
+ node.source.value = importPath;
1294
+ appended = true;
1295
+ break;
1296
+ }
1297
+ }
1298
+ if (!appended) {
1299
+ const importDecl = t.importDeclaration(
1300
+ specifiers,
1301
+ t.stringLiteral(importPath)
1302
+ );
1303
+ const lastImportIndex = findLastImportIndex(ast);
1304
+ if (lastImportIndex >= 0) {
1305
+ ast.program.body.splice(lastImportIndex + 1, 0, importDecl);
1306
+ } else {
1307
+ let insertIdx = 0;
1308
+ if (ast.program.body[0]?.type === "ExpressionStatement" && ast.program.body[0].expression.type === "StringLiteral" && ast.program.body[0].expression.value === "use client") {
1309
+ insertIdx = 1;
1310
+ }
1311
+ ast.program.body.splice(insertIdx, 0, importDecl);
1312
+ }
1313
+ }
1314
+ }
1315
+ if (needsHook && !hasInlineHookCall(ast, hookName)) {
1316
+ const hookCall = isClient ? t.callExpression(t.identifier("useT"), []) : t.callExpression(t.identifier("createT"), []);
1317
+ traverse2(ast, {
1318
+ FunctionDeclaration(path) {
1319
+ const name = path.node.id?.name;
1320
+ if (!name || !componentsNeedingT.has(name)) return;
1321
+ const body = path.node.body;
1322
+ if (body.type !== "BlockStatement") return;
1323
+ injectInlineHookIntoBlock(body, hookCall);
1324
+ },
1325
+ VariableDeclarator(path) {
1326
+ if (path.node.id.type !== "Identifier") return;
1327
+ const name = path.node.id.name;
1328
+ if (!componentsNeedingT.has(name)) return;
1329
+ const init = path.node.init;
1330
+ if (!init) return;
1331
+ if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
1332
+ if (init.body.type === "BlockStatement") {
1333
+ injectInlineHookIntoBlock(init.body, hookCall);
1334
+ }
1335
+ }
1336
+ },
1337
+ noScope: true
1338
+ });
1339
+ }
1340
+ const output = generate(ast, { retainLines: false });
1341
+ return { code: output.code, stringsWrapped, modified: true };
1342
+ }
1343
+ function injectInlineHookIntoBlock(block, hookCall) {
1344
+ for (const stmt of block.body) {
1345
+ if (stmt.type === "VariableDeclaration" && stmt.declarations.some(
1346
+ (d) => d.id.type === "Identifier" && d.id.name === "t" && d.init?.type === "CallExpression" && d.init.callee.type === "Identifier" && (d.init.callee.name === "useT" || d.init.callee.name === "createT")
1347
+ )) {
1348
+ return;
1349
+ }
1350
+ }
1351
+ const tDecl = t.variableDeclaration("const", [
1352
+ t.variableDeclarator(t.identifier("t"), hookCall)
1353
+ ]);
1354
+ block.body.unshift(tDecl);
1355
+ }
1356
+ var traverse2, generate;
1357
+ var init_transform = __esm({
1358
+ "src/codegen/transform.ts"() {
1359
+ "use strict";
1360
+ init_filters();
1361
+ init_ast_helpers();
1362
+ init_logger();
1363
+ traverse2 = resolveDefault(_traverse2);
1364
+ generate = resolveDefault(_generate);
1365
+ }
1366
+ });
1367
+
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;
1401
+ }
1402
+ }
1403
+ return {
1404
+ filesModified,
1405
+ stringsWrapped,
1406
+ filesProcessed: files.length
1407
+ };
1408
+ }
1409
+ var init_codegen = __esm({
1410
+ "src/codegen/index.ts"() {
1411
+ "use strict";
1412
+ init_parser();
1413
+ init_transform();
1414
+ init_logger();
1415
+ }
1416
+ });
1417
+
1418
+ // src/templates/t-component.ts
1419
+ function generateI18nHelper(opts) {
1420
+ const allLocales = [opts.sourceLocale, ...opts.targetLocales];
1421
+ const allLocalesStr = allLocales.map((l) => `"${l}"`).join(", ");
1422
+ return `import { headers } from "next/headers";
1423
+ import { readFile } from "node:fs/promises";
1424
+ import { join } from "node:path";
1425
+
1426
+ const supported = [${allLocalesStr}] as const;
1427
+ type Locale = (typeof supported)[number];
1428
+ const defaultLocale: Locale = "${opts.sourceLocale}";
1429
+
1430
+ function parseAcceptLanguage(header: string): Locale {
1431
+ const langs = header
1432
+ .split(",")
1433
+ .map((part) => {
1434
+ const [lang, q] = part.trim().split(";q=");
1435
+ return { lang: lang.split("-")[0].toLowerCase(), q: q ? parseFloat(q) : 1 };
1436
+ })
1437
+ .sort((a, b) => b.q - a.q);
1438
+
1439
+ for (const { lang } of langs) {
1440
+ if (supported.includes(lang as Locale)) return lang as Locale;
1441
+ }
1442
+ return defaultLocale;
1443
+ }
1444
+
1445
+ export async function getLocale(): Promise<Locale> {
1446
+ const h = await headers();
1447
+ const acceptLang = h.get("accept-language") ?? "";
1448
+ return parseAcceptLanguage(acceptLang);
1449
+ }
1450
+
1451
+ export async function getMessages(locale: string): Promise<Record<string, string>> {
1452
+ if (locale === defaultLocale) return {};
1453
+ try {
1454
+ const filePath = join(process.cwd(), "${opts.messagesDir}", \`\${locale}.json\`);
1455
+ const content = await readFile(filePath, "utf-8");
1456
+ return JSON.parse(content);
1457
+ } catch {
1458
+ return {};
1459
+ }
1460
+ }
1461
+ `;
1462
+ }
1463
+ var CLIENT_TEMPLATE, SERVER_TEMPLATE;
1464
+ var init_t_component = __esm({
1465
+ "src/templates/t-component.ts"() {
1466
+ "use strict";
1467
+ CLIENT_TEMPLATE = `"use client";
1468
+ import { createContext, useContext, type ReactNode } from "react";
1469
+
1470
+ type Messages = Record<string, string>;
1471
+ const I18nCtx = createContext<Messages>({});
1472
+
1473
+ export function I18nProvider({ messages = {}, children }: { messages?: Messages; children: ReactNode }) {
1474
+ return <I18nCtx.Provider value={messages}>{children}</I18nCtx.Provider>;
1475
+ }
1476
+
1477
+ export function T({ id, children }: { id?: string; children: ReactNode }) {
1478
+ const msgs = useContext(I18nCtx);
1479
+ if (!id) return <>{children}</>;
1480
+ return <>{msgs[id] ?? children}</>;
1481
+ }
1482
+
1483
+ export function useT() {
1484
+ 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;
1504
+ };
1505
+ }
1506
+ `;
1507
+ }
1508
+ });
1509
+
1510
+ // src/init.ts
1511
+ var init_exports = {};
1512
+ __export(init_exports, {
1513
+ runInitWizard: () => runInitWizard
1514
+ });
1515
+ import * as p from "@clack/prompts";
1516
+ 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";
1519
+ function detectIncludePatterns(cwd) {
1520
+ const patterns = [];
1521
+ if (existsSync(join3(cwd, "app")))
1522
+ patterns.push("app/**/*.tsx", "app/**/*.jsx");
1523
+ if (existsSync(join3(cwd, "src")))
1524
+ patterns.push("src/**/*.tsx", "src/**/*.jsx");
1525
+ if (existsSync(join3(cwd, "pages")))
1526
+ patterns.push("pages/**/*.tsx", "pages/**/*.jsx");
1527
+ if (existsSync(join3(cwd, "src", "app"))) {
1528
+ return patterns.filter((p2) => !p2.startsWith("app/"));
1529
+ }
1530
+ return patterns.length > 0 ? patterns : ["**/*.tsx", "**/*.jsx"];
1531
+ }
1532
+ function cancel2() {
1533
+ p.cancel("Setup cancelled.");
1534
+ process.exit(0);
1535
+ }
1536
+ function findPackageInNodeModules(cwd, pkg) {
1537
+ let dir = cwd;
1538
+ const parts = pkg.split("/");
1539
+ while (true) {
1540
+ if (existsSync(join3(dir, "node_modules", ...parts, "package.json"))) {
1541
+ return true;
1542
+ }
1543
+ const parent = join3(dir, "..");
1544
+ if (parent === dir) break;
1545
+ dir = parent;
1546
+ }
1547
+ return false;
1548
+ }
1549
+ async function ensurePackageInstalled(cwd, pkg, label) {
1550
+ while (!findPackageInNodeModules(cwd, pkg)) {
1551
+ p.log.warn(`${label} (${pkg}) is not installed.`);
1552
+ const retry = await p.confirm({
1553
+ message: `Install it now with your package manager, then press Enter. Continue?`
1554
+ });
1555
+ if (p.isCancel(retry) || !retry) cancel2();
1556
+ }
1557
+ p.log.success(`${label} found.`);
1558
+ }
1559
+ function generateConfigFile(opts) {
1560
+ const provider = AI_PROVIDERS[opts.providerKey];
1561
+ const lines = [];
1562
+ lines.push(`import { ${provider.fn} } from "${provider.pkg}";`);
1563
+ lines.push(``);
1564
+ lines.push(`export default {`);
1565
+ lines.push(` model: ${provider.fn}("${opts.modelName}"),`);
1566
+ if (opts.mode === "inline") {
1567
+ lines.push(` mode: "inline",`);
1568
+ }
1569
+ lines.push(` sourceLocale: "${opts.sourceLocale}",`);
1570
+ lines.push(
1571
+ ` targetLocales: [${opts.targetLocales.map((l) => `"${l}"`).join(", ")}],`
1572
+ );
1573
+ lines.push(` messagesDir: "${opts.messagesDir}",`);
1574
+ const hasTranslation = opts.context || opts.tone !== "neutral";
1575
+ if (hasTranslation) {
1576
+ lines.push(` translation: {`);
1577
+ if (opts.context) {
1578
+ lines.push(` context: "${opts.context}",`);
1579
+ }
1580
+ if (opts.tone !== "neutral") {
1581
+ lines.push(` tone: "${opts.tone}",`);
1582
+ }
1583
+ lines.push(` },`);
1584
+ }
1585
+ lines.push(` scan: {`);
1586
+ lines.push(
1587
+ ` include: [${opts.includePatterns.map((p2) => `"${p2}"`).join(", ")}],`
1588
+ );
1589
+ lines.push(` exclude: ["**/*.test.*", "**/*.spec.*"],`);
1590
+ if (opts.mode === "keys" && opts.i18nImport) {
1591
+ lines.push(` i18nImport: "${opts.i18nImport}",`);
1592
+ }
1593
+ lines.push(` },`);
1594
+ if (opts.mode === "inline" && opts.componentPath) {
1595
+ lines.push(` inline: {`);
1596
+ lines.push(` componentPath: "${opts.componentPath}",`);
1597
+ lines.push(` },`);
1598
+ }
1599
+ lines.push(`};`);
1600
+ lines.push(``);
1601
+ return lines.join("\n");
1602
+ }
1603
+ function canParse(content, filePath) {
1604
+ try {
1605
+ parseFile(content, filePath);
1606
+ return true;
1607
+ } catch {
1608
+ return false;
1609
+ }
1610
+ }
1611
+ async function safeWriteModifiedFile(filePath, original, modified, label) {
1612
+ if (!canParse(modified, filePath)) {
1613
+ p.log.warn(
1614
+ `Could not safely modify ${label}. Please apply changes manually:
1615
+ File: ${filePath}`
1616
+ );
1617
+ return false;
1618
+ }
1619
+ await writeFile3(filePath, modified, "utf-8");
1620
+ return true;
1621
+ }
1622
+ function detectSrcDir(cwd) {
1623
+ return existsSync(join3(cwd, "src", "app"));
1624
+ }
1625
+ function resolveComponentPath(cwd, componentPath) {
1626
+ if (componentPath.startsWith("@/")) {
1627
+ const rel = componentPath.slice(2);
1628
+ const useSrc = existsSync(join3(cwd, "src"));
1629
+ return join3(cwd, useSrc ? "src" : "", rel);
1630
+ }
1631
+ if (componentPath.startsWith("~/")) {
1632
+ return join3(cwd, componentPath.slice(2));
1633
+ }
1634
+ return join3(cwd, componentPath);
1635
+ }
1636
+ function findLayoutFile(base) {
1637
+ for (const ext of ["tsx", "jsx", "ts", "js"]) {
1638
+ const candidate = join3(base, "app", `layout.${ext}`);
1639
+ if (existsSync(candidate)) return candidate;
1640
+ }
1641
+ return void 0;
1642
+ }
1643
+ async function createEmptyMessageFiles(msgDir, locales) {
1644
+ await mkdir2(msgDir, { recursive: true });
1645
+ for (const locale of locales) {
1646
+ const msgFile = join3(msgDir, `${locale}.json`);
1647
+ if (!existsSync(msgFile)) {
1648
+ await writeFile3(msgFile, "{}\n", "utf-8");
1649
+ }
1650
+ }
1651
+ }
1652
+ function insertImportsAfterLast(content, importLines) {
1653
+ const lastImportIdx = content.lastIndexOf("import ");
1654
+ const endOfLastImport = content.indexOf("\n", lastImportIdx);
1655
+ return content.slice(0, endOfLastImport + 1) + importLines + content.slice(endOfLastImport + 1);
1656
+ }
1657
+ function ensureAsyncLayout(content) {
1658
+ if (content.match(/async\s+function\s+\w*Layout/)) return content;
1659
+ return content.replace(
1660
+ /export\s+default\s+function\s+(\w*Layout)/,
1661
+ "export default async function $1"
1662
+ );
1663
+ }
1664
+ async function setupNextIntl(cwd, sourceLocale, targetLocales, messagesDir) {
1665
+ const useSrc = detectSrcDir(cwd);
1666
+ const base = useSrc ? join3(cwd, "src") : cwd;
1667
+ const allLocales = [sourceLocale, ...targetLocales];
1668
+ const filesCreated = [];
1669
+ const i18nDir = join3(base, "i18n");
1670
+ await mkdir2(i18nDir, { recursive: true });
1671
+ const requestFile = join3(i18nDir, "request.ts");
1672
+ if (!existsSync(requestFile)) {
1673
+ const relMessages = relative(i18nDir, join3(cwd, messagesDir));
1674
+ const allLocalesStr = allLocales.map((l) => `"${l}"`).join(", ");
1675
+ await writeFile3(
1676
+ requestFile,
1677
+ `import { getRequestConfig } from "next-intl/server";
1678
+ import { headers } from "next/headers";
1679
+
1680
+ const supported = [${allLocalesStr}] as const;
1681
+ type Locale = (typeof supported)[number];
1682
+ const defaultLocale: Locale = "${sourceLocale}";
1683
+
1684
+ function parseAcceptLanguage(header: string): Locale {
1685
+ const langs = header
1686
+ .split(",")
1687
+ .map((part) => {
1688
+ const [lang, q] = part.trim().split(";q=");
1689
+ return { lang: lang.split("-")[0].toLowerCase(), q: q ? parseFloat(q) : 1 };
1690
+ })
1691
+ .sort((a, b) => b.q - a.q);
1692
+
1693
+ for (const { lang } of langs) {
1694
+ if (supported.includes(lang as Locale)) return lang as Locale;
1695
+ }
1696
+ return defaultLocale;
1697
+ }
1698
+
1699
+ export default getRequestConfig(async () => {
1700
+ const h = await headers();
1701
+ const acceptLang = h.get("accept-language") ?? "";
1702
+ const locale = parseAcceptLanguage(acceptLang);
1703
+
1704
+ return {
1705
+ locale,
1706
+ messages: (await import(\`${relMessages}/\${locale}.json\`)).default,
1707
+ };
1708
+ });
1709
+ `,
1710
+ "utf-8"
1711
+ );
1712
+ filesCreated.push(relative(cwd, requestFile));
1713
+ }
1714
+ const nextConfigPath = join3(cwd, "next.config.ts");
1715
+ if (existsSync(nextConfigPath)) {
1716
+ const content = await readFile4(nextConfigPath, "utf-8");
1717
+ if (!content.includes("next-intl")) {
1718
+ const importLine = `import createNextIntlPlugin from "next-intl/plugin";
1719
+ `;
1720
+ const pluginLine = `const withNextIntl = createNextIntlPlugin();
1721
+ `;
1722
+ const wrapped = content.replace(
1723
+ /export default (.+?);/,
1724
+ "export default withNextIntl($1);"
1725
+ );
1726
+ const updated = importLine + "\n" + pluginLine + "\n" + wrapped;
1727
+ if (await safeWriteModifiedFile(
1728
+ nextConfigPath,
1729
+ content,
1730
+ updated,
1731
+ "next.config.ts"
1732
+ )) {
1733
+ filesCreated.push("next.config.ts (updated)");
1734
+ }
1735
+ }
1736
+ }
1737
+ const layoutPath = findLayoutFile(base);
1738
+ if (layoutPath) {
1739
+ let layoutContent = await readFile4(layoutPath, "utf-8");
1740
+ if (!layoutContent.includes("NextIntlClientProvider")) {
1741
+ const original = layoutContent;
1742
+ const importLines = 'import { NextIntlClientProvider } from "next-intl";\nimport { getMessages } from "next-intl/server";\n';
1743
+ layoutContent = insertImportsAfterLast(layoutContent, importLines);
1744
+ layoutContent = ensureAsyncLayout(layoutContent);
1745
+ layoutContent = layoutContent.replace(
1746
+ /return\s*\(/,
1747
+ "const messages = await getMessages();\n\n return ("
1748
+ );
1749
+ layoutContent = layoutContent.replace(
1750
+ /(<body[^>]*>)/,
1751
+ "$1\n <NextIntlClientProvider messages={messages}>"
1752
+ );
1753
+ layoutContent = layoutContent.replace(
1754
+ /<\/body>/,
1755
+ " </NextIntlClientProvider>\n </body>"
1756
+ );
1757
+ if (await safeWriteModifiedFile(
1758
+ layoutPath,
1759
+ original,
1760
+ layoutContent,
1761
+ "root layout"
1762
+ )) {
1763
+ filesCreated.push(relative(cwd, layoutPath) + " (updated)");
1764
+ }
1765
+ }
1766
+ }
1767
+ await createEmptyMessageFiles(join3(cwd, messagesDir), allLocales);
1768
+ if (filesCreated.length > 0) {
1769
+ p.log.success(`next-intl configured: ${filesCreated.join(", ")}`);
1770
+ }
1771
+ }
1772
+ async function dropInlineComponents(cwd, componentPath) {
1773
+ const fsPath = resolveComponentPath(cwd, componentPath);
1774
+ const dir = join3(fsPath, "..");
1775
+ await mkdir2(dir, { recursive: true });
1776
+ const clientFile = `${fsPath}.tsx`;
1777
+ const serverFile = `${fsPath}-server.tsx`;
1778
+ await writeFile3(clientFile, CLIENT_TEMPLATE, "utf-8");
1779
+ await writeFile3(serverFile, SERVER_TEMPLATE, "utf-8");
1780
+ const relClient = relative(cwd, clientFile);
1781
+ const relServer = relative(cwd, serverFile);
1782
+ p.log.success(`Created inline components: ${relClient}, ${relServer}`);
1783
+ }
1784
+ async function setupInlineI18n(cwd, componentPath, sourceLocale, targetLocales, messagesDir) {
1785
+ const useSrc = existsSync(join3(cwd, "src"));
1786
+ const base = useSrc ? join3(cwd, "src") : cwd;
1787
+ const filesCreated = [];
1788
+ const i18nDir = join3(base, "i18n");
1789
+ await mkdir2(i18nDir, { recursive: true });
1790
+ const helperFile = join3(i18nDir, "index.ts");
1791
+ if (!existsSync(helperFile)) {
1792
+ const helperContent = generateI18nHelper({
1793
+ sourceLocale,
1794
+ targetLocales,
1795
+ messagesDir
1796
+ });
1797
+ await writeFile3(helperFile, helperContent, "utf-8");
1798
+ filesCreated.push(relative(cwd, helperFile));
1799
+ }
1800
+ const layoutPath = findLayoutFile(base);
1801
+ if (layoutPath) {
1802
+ let layoutContent = await readFile4(layoutPath, "utf-8");
1803
+ if (!layoutContent.includes("I18nProvider")) {
1804
+ const original = layoutContent;
1805
+ const importLines = `import { I18nProvider } from "${componentPath}";
1806
+ import { getLocale, getMessages } from "@/i18n";
1807
+ `;
1808
+ layoutContent = insertImportsAfterLast(layoutContent, importLines);
1809
+ layoutContent = ensureAsyncLayout(layoutContent);
1810
+ layoutContent = layoutContent.replace(
1811
+ /return\s*\(/,
1812
+ "const locale = await getLocale();\n const messages = await getMessages(locale);\n\n return ("
1813
+ );
1814
+ layoutContent = layoutContent.replace(
1815
+ /(<body[^>]*>)/,
1816
+ "$1\n <I18nProvider messages={messages}>"
1817
+ );
1818
+ layoutContent = layoutContent.replace(
1819
+ /<\/body>/,
1820
+ " </I18nProvider>\n </body>"
1821
+ );
1822
+ if (await safeWriteModifiedFile(
1823
+ layoutPath,
1824
+ original,
1825
+ layoutContent,
1826
+ "root layout"
1827
+ )) {
1828
+ filesCreated.push(relative(cwd, layoutPath) + " (updated)");
1829
+ }
1830
+ }
1831
+ }
1832
+ await createEmptyMessageFiles(join3(cwd, messagesDir), [
1833
+ sourceLocale,
1834
+ ...targetLocales
1835
+ ]);
1836
+ if (filesCreated.length > 0) {
1837
+ p.log.success(`Inline i18n configured: ${filesCreated.join(", ")}`);
1838
+ }
1839
+ }
1840
+ async function runInitWizard() {
1841
+ const cwd = process.cwd();
1842
+ const configPath = join3(cwd, "translate-kit.config.ts");
1843
+ p.intro("translate-kit setup");
1844
+ if (existsSync(configPath)) {
1845
+ const overwrite = await p.confirm({
1846
+ message: "translate-kit.config.ts already exists. Overwrite?"
1847
+ });
1848
+ if (p.isCancel(overwrite)) cancel2();
1849
+ if (!overwrite) {
1850
+ p.outro("Keeping existing config.");
1851
+ return;
1852
+ }
1853
+ }
1854
+ const mode = await p.select({
1855
+ message: "Translation mode:",
1856
+ options: [
1857
+ {
1858
+ value: "keys",
1859
+ label: "Keys mode",
1860
+ hint: "t('key') + JSON files"
1861
+ },
1862
+ {
1863
+ value: "inline",
1864
+ label: "Inline mode",
1865
+ hint: "<T id='key'>text</T>, text stays in code"
1866
+ }
1867
+ ]
1868
+ });
1869
+ if (p.isCancel(mode)) cancel2();
1870
+ const providerKey = await p.select({
1871
+ message: "AI provider:",
1872
+ options: Object.entries(AI_PROVIDERS).map(([key, val]) => ({
1873
+ value: key,
1874
+ label: key.charAt(0).toUpperCase() + key.slice(1),
1875
+ hint: val.pkg
1876
+ }))
1877
+ });
1878
+ if (p.isCancel(providerKey)) cancel2();
1879
+ const provider = AI_PROVIDERS[providerKey];
1880
+ const modelName = await p.text({
1881
+ message: "Model:",
1882
+ initialValue: provider.defaultModel
1883
+ });
1884
+ if (p.isCancel(modelName)) cancel2();
1885
+ const sourceLocale = await p.text({
1886
+ message: "Source locale:",
1887
+ initialValue: "en"
1888
+ });
1889
+ if (p.isCancel(sourceLocale)) cancel2();
1890
+ const targetLocales = await p.multiselect({
1891
+ message: "Target locales:",
1892
+ options: LOCALE_OPTIONS.filter((o) => o.value !== sourceLocale),
1893
+ required: true
1894
+ });
1895
+ if (p.isCancel(targetLocales)) cancel2();
1896
+ const messagesDir = await p.text({
1897
+ message: "Messages directory:",
1898
+ initialValue: "./messages"
1899
+ });
1900
+ if (p.isCancel(messagesDir)) cancel2();
1901
+ const detected = detectIncludePatterns(cwd);
1902
+ let includePatterns;
1903
+ const useDetected = await p.confirm({
1904
+ message: `Detected: ${detected.join(", ")} \u2014 Use these patterns?`
1905
+ });
1906
+ if (p.isCancel(useDetected)) cancel2();
1907
+ if (useDetected) {
1908
+ includePatterns = detected;
1909
+ } else {
1910
+ const customPatterns = await p.text({
1911
+ message: "Include patterns (comma-separated):",
1912
+ initialValue: "src/**/*.tsx, src/**/*.jsx"
1913
+ });
1914
+ if (p.isCancel(customPatterns)) cancel2();
1915
+ includePatterns = customPatterns.split(",").map((s) => s.trim());
1916
+ }
1917
+ let i18nImport = "";
1918
+ let componentPath;
1919
+ if (mode === "inline") {
1920
+ const cp = await p.text({
1921
+ message: "Component import path:",
1922
+ initialValue: "@/components/t"
1923
+ });
1924
+ if (p.isCancel(cp)) cancel2();
1925
+ componentPath = cp;
1926
+ } else {
1927
+ const lib = await p.text({
1928
+ message: "i18n library:",
1929
+ initialValue: "next-intl"
1930
+ });
1931
+ if (p.isCancel(lib)) cancel2();
1932
+ i18nImport = lib;
1933
+ }
1934
+ const context = await p.text({
1935
+ message: "Project context (optional, for better translations):",
1936
+ placeholder: "e.g. E-commerce platform, SaaS dashboard"
1937
+ });
1938
+ if (p.isCancel(context)) cancel2();
1939
+ const tone = await p.select({
1940
+ message: "Tone:",
1941
+ options: [
1942
+ { value: "neutral", label: "Neutral" },
1943
+ { value: "formal", label: "Formal" },
1944
+ { value: "casual", label: "Casual" }
1945
+ ]
1946
+ });
1947
+ if (p.isCancel(tone)) cancel2();
1948
+ await ensurePackageInstalled(cwd, provider.pkg, "AI provider");
1949
+ if (i18nImport) {
1950
+ await ensurePackageInstalled(cwd, i18nImport, "i18n library");
1951
+ }
1952
+ const configContent = generateConfigFile({
1953
+ providerKey,
1954
+ modelName,
1955
+ sourceLocale,
1956
+ targetLocales,
1957
+ messagesDir,
1958
+ includePatterns,
1959
+ i18nImport,
1960
+ context: context ?? "",
1961
+ tone,
1962
+ mode,
1963
+ componentPath
1964
+ });
1965
+ await writeFile3(configPath, configContent, "utf-8");
1966
+ p.log.success("Created translate-kit.config.ts");
1967
+ if (mode === "inline" && componentPath) {
1968
+ await dropInlineComponents(cwd, componentPath);
1969
+ await setupInlineI18n(
1970
+ cwd,
1971
+ componentPath,
1972
+ sourceLocale,
1973
+ targetLocales,
1974
+ messagesDir
1975
+ );
1976
+ } else if (i18nImport === "next-intl") {
1977
+ await setupNextIntl(cwd, sourceLocale, targetLocales, messagesDir);
1978
+ }
1979
+ const runPipeline = await p.confirm({
1980
+ message: "Run the full pipeline now?"
1981
+ });
1982
+ if (p.isCancel(runPipeline)) cancel2();
1983
+ if (!runPipeline) {
1984
+ p.outro("You're all set! Run translate-kit scan when ready.");
1985
+ return;
1986
+ }
1987
+ let config;
1988
+ try {
1989
+ config = await loadTranslateKitConfig();
1990
+ } catch (err) {
1991
+ const errMsg = err instanceof Error ? err.message : String(err);
1992
+ p.log.error(`Failed to load config: ${errMsg}`);
1993
+ p.outro("Config created but pipeline skipped.");
1994
+ return;
1995
+ }
1996
+ const { model } = config;
1997
+ const scanOptions = {
1998
+ include: includePatterns,
1999
+ exclude: ["**/*.test.*", "**/*.spec.*"],
2000
+ i18nImport
2001
+ };
2002
+ const s1 = p.spinner();
2003
+ 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
+ );
2008
+ s1.stop(
2009
+ `Scanning... ${transformableStrings.length} strings from ${scanResult.fileCount} files`
2010
+ );
2011
+ if (transformableStrings.length === 0) {
2012
+ p.log.warn("No translatable strings found. Check your include patterns.");
2013
+ p.outro("Config created, but no strings to process.");
2014
+ return;
2015
+ }
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
+ const s3 = p.spinner();
2054
+ 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
+ );
2066
+ s3.stop(
2067
+ `Codegen... ${codegenResult.stringsWrapped} strings wrapped in ${codegenResult.filesModified} files`
2068
+ );
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
+ for (const locale of targetLocales) {
2111
+ const st = p.spinner();
2112
+ st.start(`Translating ${locale}...`);
2113
+ const translated = await translateAll({
2114
+ model,
2115
+ entries: sourceFlat,
2116
+ sourceLocale,
2117
+ targetLocale: locale,
2118
+ options: translationOpts
2119
+ });
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
+ st.stop(`Translating ${locale}... done`);
2130
+ }
2131
+ p.outro("You're all set!");
2132
+ }
2133
+ var AI_PROVIDERS, LOCALE_OPTIONS;
2134
+ var init_init = __esm({
2135
+ "src/init.ts"() {
2136
+ "use strict";
2137
+ init_config();
2138
+ init_scanner();
2139
+ init_key_ai();
2140
+ init_codegen();
2141
+ init_translate();
2142
+ init_writer();
2143
+ init_diff();
2144
+ init_flatten();
2145
+ init_t_component();
2146
+ init_parser();
2147
+ AI_PROVIDERS = {
2148
+ openai: {
2149
+ pkg: "@ai-sdk/openai",
2150
+ fn: "openai",
2151
+ defaultModel: "gpt-4o-mini"
2152
+ },
2153
+ anthropic: {
2154
+ pkg: "@ai-sdk/anthropic",
2155
+ fn: "anthropic",
2156
+ defaultModel: "claude-sonnet-4-20250514"
2157
+ },
2158
+ google: {
2159
+ pkg: "@ai-sdk/google",
2160
+ fn: "google",
2161
+ defaultModel: "gemini-2.0-flash"
2162
+ },
2163
+ mistral: {
2164
+ pkg: "@ai-sdk/mistral",
2165
+ fn: "mistral",
2166
+ defaultModel: "mistral-large-latest"
2167
+ },
2168
+ groq: {
2169
+ pkg: "@ai-sdk/groq",
2170
+ fn: "groq",
2171
+ defaultModel: "llama-3.3-70b-versatile"
2172
+ }
2173
+ };
2174
+ LOCALE_OPTIONS = [
2175
+ { value: "es", label: "Spanish (es)" },
2176
+ { value: "fr", label: "French (fr)" },
2177
+ { value: "de", label: "German (de)" },
2178
+ { value: "pt", label: "Portuguese (pt)" },
2179
+ { value: "ja", label: "Japanese (ja)" },
2180
+ { value: "zh", label: "Chinese (zh)" },
2181
+ { value: "ko", label: "Korean (ko)" },
2182
+ { value: "ru", label: "Russian (ru)" },
2183
+ { value: "ar", label: "Arabic (ar)" },
2184
+ { value: "it", label: "Italian (it)" }
2185
+ ];
2186
+ }
2187
+ });
2188
+
2189
+ // src/cli.ts
2190
+ init_config();
2191
+ init_flatten();
2192
+ init_diff();
2193
+ init_translate();
2194
+ init_writer();
2195
+ init_scanner();
2196
+ init_key_ai();
2197
+ init_codegen();
2198
+ init_logger();
2199
+ import "dotenv/config";
2200
+ 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
+ }
2218
+ var translateCommand = defineCommand({
2219
+ meta: {
2220
+ name: "translate",
2221
+ description: "Translate messages to target locales"
2222
+ },
2223
+ args: {
2224
+ "dry-run": {
2225
+ type: "boolean",
2226
+ description: "Show what would be translated without executing",
2227
+ default: false
2228
+ },
2229
+ force: {
2230
+ type: "boolean",
2231
+ description: "Ignore cache, re-translate everything",
2232
+ default: false
2233
+ },
2234
+ locale: {
2235
+ type: "string",
2236
+ description: "Only translate a specific locale"
2237
+ },
2238
+ verbose: {
2239
+ type: "boolean",
2240
+ description: "Verbose output",
2241
+ default: false
2242
+ }
2243
+ },
2244
+ async run({ args }) {
2245
+ const config = await loadTranslateKitConfig();
2246
+ const { sourceLocale, targetLocales, messagesDir, model } = config;
2247
+ const opts = config.translation ?? {};
2248
+ const verbose = args.verbose;
2249
+ const mode = config.mode ?? "keys";
2250
+ const locales = args.locale ? [args.locale] : targetLocales;
2251
+ if (args.locale && !targetLocales.includes(args.locale)) {
2252
+ logWarning(
2253
+ `Locale "${args.locale}" is not in targetLocales [${targetLocales.join(", ")}]`
2254
+ );
2255
+ }
2256
+ let sourceFlat;
2257
+ if (mode === "inline") {
2258
+ const mapData = await loadMapFile(messagesDir);
2259
+ sourceFlat = {};
2260
+ for (const [text2, key] of Object.entries(mapData)) {
2261
+ sourceFlat[key] = text2;
2262
+ }
2263
+ } else {
2264
+ const sourceFile = join4(messagesDir, `${sourceLocale}.json`);
2265
+ const sourceRaw = await loadJsonFile(sourceFile);
2266
+ sourceFlat = flatten(sourceRaw);
2267
+ }
2268
+ if (Object.keys(sourceFlat).length === 0) {
2269
+ logError(
2270
+ mode === "inline" ? `No keys found in .translate-map.json. Run 'translate-kit scan' first.` : `No keys found in ${join4(messagesDir, `${sourceLocale}.json`)}`
2271
+ );
2272
+ process.exit(1);
2273
+ }
2274
+ logStart(sourceLocale, locales);
2275
+ 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
+ }
2321
+ }
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
+ );
2337
+ 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
2344
+ };
2345
+ logLocaleResult(result);
2346
+ results.push(result);
2347
+ }
2348
+ if (!args["dry-run"]) {
2349
+ logSummary(results);
2350
+ }
2351
+ }
2352
+ });
2353
+ var scanCommand = defineCommand({
2354
+ meta: {
2355
+ name: "scan",
2356
+ description: "Scan source code for translatable strings"
2357
+ },
2358
+ args: {
2359
+ "dry-run": {
2360
+ type: "boolean",
2361
+ description: "Show found strings without writing files",
2362
+ default: false
2363
+ }
2364
+ },
2365
+ async run({ args }) {
2366
+ const config = await loadTranslateKitConfig();
2367
+ const mode = config.mode ?? "keys";
2368
+ if (!config.scan) {
2369
+ logError(
2370
+ "No scan configuration found. Add a 'scan' section to your config."
2371
+ );
2372
+ process.exit(1);
2373
+ }
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
+ if (args["dry-run"]) {
2382
+ for (const str of bareStrings) {
2383
+ logInfo(
2384
+ `"${str.text}" (${str.componentName ?? "unknown"}, ${str.file})`
2385
+ );
2386
+ }
2387
+ if (mode === "inline") {
2388
+ logInfo(
2389
+ "\n Inline mode: no source locale JSON will be created. Source text remains in code."
2390
+ );
2391
+ }
2392
+ return;
2393
+ }
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
+ }
2403
+ }
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
+ });
2422
+ await writeMapFile(config.messagesDir, textToKey);
2423
+ logSuccess(
2424
+ `Written .translate-map.json (${Object.keys(textToKey).length} keys)`
2425
+ );
2426
+ if (mode === "inline") {
2427
+ logInfo(
2428
+ "Inline mode: source text stays in code, no source locale JSON created."
2429
+ );
2430
+ } else {
2431
+ const messages = {};
2432
+ for (const [text2, key] of Object.entries(textToKey)) {
2433
+ messages[key] = text2;
2434
+ }
2435
+ const sourceFile = join4(
2436
+ config.messagesDir,
2437
+ `${config.sourceLocale}.json`
2438
+ );
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
+ logSuccess(`Written to ${sourceFile}`);
2444
+ }
2445
+ }
2446
+ });
2447
+ var codegenCommand = defineCommand({
2448
+ meta: {
2449
+ name: "codegen",
2450
+ description: "Replace strings in source code with t() calls"
2451
+ },
2452
+ args: {
2453
+ "dry-run": {
2454
+ type: "boolean",
2455
+ description: "Show what would be changed without modifying files",
2456
+ default: false
2457
+ }
2458
+ },
2459
+ async run({ args }) {
2460
+ const config = await loadTranslateKitConfig();
2461
+ const mode = config.mode ?? "keys";
2462
+ if (!config.scan) {
2463
+ logError(
2464
+ "No scan configuration found. Add a 'scan' section to your config."
2465
+ );
2466
+ process.exit(1);
2467
+ }
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
+ if (args["dry-run"]) {
2474
+ if (mode === "inline") {
2475
+ logInfo(
2476
+ `
2477
+ Would wrap ${Object.keys(textToKey).length} strings with <T> components
2478
+ `
2479
+ );
2480
+ for (const [text2, key] of Object.entries(textToKey)) {
2481
+ logInfo(`"${text2}" \u2192 <T id="${key}">${text2}</T>`);
2482
+ }
2483
+ } else {
2484
+ logInfo(
2485
+ `
2486
+ Would replace ${Object.keys(textToKey).length} strings with t() calls
2487
+ `
2488
+ );
2489
+ for (const [text2, key] of Object.entries(textToKey)) {
2490
+ logInfo(`"${text2}" \u2192 t("${key}")`);
2491
+ }
2492
+ }
2493
+ return;
2494
+ }
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
2502
+ });
2503
+ logSuccess(
2504
+ `Codegen complete: ${result.stringsWrapped} strings wrapped in ${result.filesModified} files (${result.filesProcessed} files processed)`
2505
+ );
2506
+ }
2507
+ });
2508
+ var initCommand = defineCommand({
2509
+ meta: {
2510
+ name: "init",
2511
+ description: "Interactive setup wizard for translate-kit"
2512
+ },
2513
+ async run() {
2514
+ const { runInitWizard: runInitWizard2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2515
+ await runInitWizard2();
2516
+ }
2517
+ });
2518
+ var main = defineCommand({
2519
+ meta: {
2520
+ name: "translate-kit",
2521
+ version: "0.1.0",
2522
+ description: "AI-powered translation SDK for build time"
2523
+ },
2524
+ subCommands: {
2525
+ translate: translateCommand,
2526
+ scan: scanCommand,
2527
+ codegen: codegenCommand,
2528
+ init: initCommand
2529
+ },
2530
+ // Default to translate command
2531
+ async run({ rawArgs }) {
2532
+ if (rawArgs.length === 0 || rawArgs[0]?.startsWith("-")) {
2533
+ await translateCommand.run({
2534
+ args: {
2535
+ _: rawArgs,
2536
+ "dry-run": false,
2537
+ force: false,
2538
+ verbose: false,
2539
+ locale: ""
2540
+ },
2541
+ rawArgs,
2542
+ cmd: translateCommand
2543
+ });
2544
+ }
2545
+ }
2546
+ });
2547
+ runMain(main);
2548
+ //# sourceMappingURL=cli.js.map