translate-kit 0.2.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
@@ -914,7 +914,7 @@ async function generateKeysBatchWithRetry(model, strings, retries) {
914
914
  lastError = error;
915
915
  if (attempt < retries) {
916
916
  const delay = Math.min(Math.pow(2, attempt) * 1e3, 3e4);
917
- await new Promise((resolve) => setTimeout(resolve, delay));
917
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
918
918
  }
919
919
  }
920
920
  }
@@ -1028,6 +1028,18 @@ function hasUseTranslationsImport(ast, importSource) {
1028
1028
  }
1029
1029
  return false;
1030
1030
  }
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
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ return false;
1042
+ }
1031
1043
  function transformConditionalBranch(node, textToKey) {
1032
1044
  if (node.type === "ConditionalExpression") {
1033
1045
  const cons = transformConditionalBranch(
@@ -1131,6 +1143,8 @@ function transform(ast, textToKey, options = {}) {
1131
1143
  return transformInline(ast, textToKey, options);
1132
1144
  }
1133
1145
  const importSource = options.i18nImport ?? "next-intl";
1146
+ const supportsServerSplit = importSource === "next-intl";
1147
+ const isClient = !supportsServerSplit || options.forceClient || detectClientFile(ast);
1134
1148
  let stringsWrapped = 0;
1135
1149
  const componentsNeedingT = /* @__PURE__ */ new Set();
1136
1150
  traverse2(ast, {
@@ -1232,7 +1246,12 @@ function transform(ast, textToKey, options = {}) {
1232
1246
  const key = textToKey[text2];
1233
1247
  const args = [t2.stringLiteral(key)];
1234
1248
  if (templateInfo && templateInfo.placeholders.length > 0) {
1235
- args.push(buildValuesObject(templateInfo.expressions, templateInfo.placeholders));
1249
+ args.push(
1250
+ buildValuesObject(
1251
+ templateInfo.expressions,
1252
+ templateInfo.placeholders
1253
+ )
1254
+ );
1236
1255
  }
1237
1256
  path.node.value = t2.jsxExpressionContainer(
1238
1257
  t2.callExpression(t2.identifier("t"), args)
@@ -1281,7 +1300,12 @@ function transform(ast, textToKey, options = {}) {
1281
1300
  const key = textToKey[text2];
1282
1301
  const args = [t2.stringLiteral(key)];
1283
1302
  if (templateInfo && templateInfo.placeholders.length > 0) {
1284
- args.push(buildValuesObject(templateInfo.expressions, templateInfo.placeholders));
1303
+ args.push(
1304
+ buildValuesObject(
1305
+ templateInfo.expressions,
1306
+ templateInfo.placeholders
1307
+ )
1308
+ );
1285
1309
  }
1286
1310
  path.node.value = t2.callExpression(t2.identifier("t"), args);
1287
1311
  stringsWrapped++;
@@ -1291,43 +1315,86 @@ function transform(ast, textToKey, options = {}) {
1291
1315
  if (stringsWrapped === 0) {
1292
1316
  return { code: generate(ast).code, stringsWrapped: 0, modified: false };
1293
1317
  }
1294
- if (!hasUseTranslationsImport(ast, importSource)) {
1295
- const importDecl = t2.importDeclaration(
1296
- [
1297
- t2.importSpecifier(
1298
- t2.identifier("useTranslations"),
1299
- t2.identifier("useTranslations")
1300
- )
1301
- ],
1302
- t2.stringLiteral(importSource)
1303
- );
1304
- const lastImportIndex = findLastImportIndex(ast);
1305
- if (lastImportIndex >= 0) {
1306
- ast.program.body.splice(lastImportIndex + 1, 0, importDecl);
1307
- } else {
1308
- 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
+ }
1309
1335
  }
1310
- }
1311
- traverse2(ast, {
1312
- FunctionDeclaration(path) {
1313
- const name = path.node.id?.name;
1314
- if (!name || !componentsNeedingT.has(name)) return;
1315
- injectTDeclaration(path);
1316
- },
1317
- VariableDeclarator(path) {
1318
- if (path.node.id.type !== "Identifier") return;
1319
- const name = path.node.id.name;
1320
- if (!componentsNeedingT.has(name)) return;
1321
- const init = path.node.init;
1322
- if (!init) return;
1323
- if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
1324
- if (init.body.type === "BlockStatement") {
1325
- injectTIntoBlock(init.body);
1336
+ traverse2(ast, {
1337
+ FunctionDeclaration(path) {
1338
+ const name = path.node.id?.name;
1339
+ if (!name || !componentsNeedingT.has(name)) return;
1340
+ injectTDeclaration(path);
1341
+ },
1342
+ VariableDeclarator(path) {
1343
+ if (path.node.id.type !== "Identifier") return;
1344
+ const name = path.node.id.name;
1345
+ if (!componentsNeedingT.has(name)) return;
1346
+ const init = path.node.init;
1347
+ if (!init) return;
1348
+ if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
1349
+ if (init.body.type === "BlockStatement") {
1350
+ injectTIntoBlock(init.body);
1351
+ }
1326
1352
  }
1353
+ },
1354
+ noScope: true
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);
1327
1373
  }
1328
- },
1329
- noScope: true
1330
- });
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
+ });
1397
+ }
1331
1398
  const output = generate(ast, { retainLines: false });
1332
1399
  return { code: output.code, stringsWrapped, modified: true };
1333
1400
  }
@@ -1339,7 +1406,7 @@ function injectTDeclaration(path) {
1339
1406
  function injectTIntoBlock(block) {
1340
1407
  for (const stmt of block.body) {
1341
1408
  if (stmt.type === "VariableDeclaration" && stmt.declarations.some(
1342
- (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")
1343
1410
  )) {
1344
1411
  return;
1345
1412
  }
@@ -1352,7 +1419,25 @@ function injectTIntoBlock(block) {
1352
1419
  ]);
1353
1420
  block.body.unshift(tDecl);
1354
1421
  }
1355
- function isClientFile(ast) {
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")
1426
+ )) {
1427
+ return;
1428
+ }
1429
+ }
1430
+ const tDecl = t2.variableDeclaration("const", [
1431
+ t2.variableDeclarator(
1432
+ t2.identifier("t"),
1433
+ t2.awaitExpression(
1434
+ t2.callExpression(t2.identifier("getTranslations"), [])
1435
+ )
1436
+ )
1437
+ ]);
1438
+ block.body.unshift(tDecl);
1439
+ }
1440
+ function detectClientFile(ast) {
1356
1441
  if (ast.program.directives) {
1357
1442
  for (const directive of ast.program.directives) {
1358
1443
  if (directive.value?.value === "use client") {
@@ -1365,7 +1450,18 @@ function isClientFile(ast) {
1365
1450
  return true;
1366
1451
  }
1367
1452
  }
1368
- 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;
1369
1465
  }
1370
1466
  function hasInlineImport(ast, componentPath) {
1371
1467
  let hasT = false;
@@ -1385,13 +1481,46 @@ function hasInlineImport(ast, componentPath) {
1385
1481
  }
1386
1482
  return { hasT, hasHook };
1387
1483
  }
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;
1502
+ }
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;
1514
+ }
1388
1515
  function transformInline(ast, textToKey, options) {
1389
1516
  const componentPath = options.componentPath ?? "@/components/t";
1390
- const isClient = isClientFile(ast);
1517
+ const isClient = options.forceClient || detectClientFile(ast);
1391
1518
  let stringsWrapped = 0;
1392
1519
  const componentsNeedingT = /* @__PURE__ */ new Set();
1393
1520
  let needsTComponent = false;
1394
1521
  let repaired = false;
1522
+ let boundaryRepaired = false;
1523
+ boundaryRepaired = normalizeInlineImports(ast, componentPath, isClient);
1395
1524
  if (!isClient) {
1396
1525
  traverse2(ast, {
1397
1526
  CallExpression(path) {
@@ -1523,7 +1652,10 @@ function transformInline(ast, textToKey, options) {
1523
1652
  ];
1524
1653
  if (templateInfo && templateInfo.placeholders.length > 0) {
1525
1654
  args.push(
1526
- buildValuesObject(templateInfo.expressions, templateInfo.placeholders)
1655
+ buildValuesObject(
1656
+ templateInfo.expressions,
1657
+ templateInfo.placeholders
1658
+ )
1527
1659
  );
1528
1660
  }
1529
1661
  path.node.value = t2.jsxExpressionContainer(
@@ -1577,7 +1709,10 @@ function transformInline(ast, textToKey, options) {
1577
1709
  ];
1578
1710
  if (templateInfo && templateInfo.placeholders.length > 0) {
1579
1711
  args.push(
1580
- buildValuesObject(templateInfo.expressions, templateInfo.placeholders)
1712
+ buildValuesObject(
1713
+ templateInfo.expressions,
1714
+ templateInfo.placeholders
1715
+ )
1581
1716
  );
1582
1717
  }
1583
1718
  path.node.value = t2.callExpression(t2.identifier("t"), args);
@@ -1585,10 +1720,10 @@ function transformInline(ast, textToKey, options) {
1585
1720
  componentsNeedingT.add(compName);
1586
1721
  }
1587
1722
  });
1588
- if (stringsWrapped === 0 && !repaired) {
1723
+ if (stringsWrapped === 0 && !repaired && !boundaryRepaired) {
1589
1724
  return { code: generate(ast).code, stringsWrapped: 0, modified: false };
1590
1725
  }
1591
- if (stringsWrapped === 0 && repaired) {
1726
+ if (stringsWrapped === 0 && (repaired || boundaryRepaired)) {
1592
1727
  const output2 = generate(ast, { retainLines: false });
1593
1728
  return { code: output2.code, stringsWrapped: 0, modified: true };
1594
1729
  }
@@ -1687,9 +1822,109 @@ var init_transform = __esm({
1687
1822
  });
1688
1823
 
1689
1824
  // src/codegen/index.ts
1825
+ import { dirname, extname, join as join2, resolve } from "path";
1690
1826
  import { readFile as readFile3, writeFile } from "fs/promises";
1691
1827
  import { glob as glob2 } from "tinyglobby";
1692
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
+ }
1693
1928
  async function codegen(options, cwd = process.cwd()) {
1694
1929
  const files = await glob2(options.include, {
1695
1930
  ignore: options.exclude ?? [],
@@ -1701,39 +1936,72 @@ async function codegen(options, cwd = process.cwd()) {
1701
1936
  mode: options.mode,
1702
1937
  componentPath: options.componentPath
1703
1938
  };
1704
- const limit = pLimit3(10);
1705
- let completed = 0;
1706
- const fileResults = await Promise.all(
1939
+ const parseLimit = pLimit3(10);
1940
+ const parsedEntries = await Promise.all(
1707
1941
  files.map(
1708
- (filePath) => limit(async () => {
1942
+ (filePath) => parseLimit(async () => {
1709
1943
  const code = await readFile3(filePath, "utf-8");
1710
- let ast;
1711
1944
  try {
1712
- ast = parseFile(code, filePath);
1945
+ const ast = parseFile(code, filePath);
1946
+ return {
1947
+ filePath,
1948
+ code,
1949
+ ast,
1950
+ isClientRoot: detectClientFile(ast)
1951
+ };
1713
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) {
1714
1970
  logWarning(
1715
- `Skipping unparseable file ${filePath}: ${err instanceof Error ? err.message : String(err)}`
1971
+ `Skipping unparseable file ${entry.filePath}: ${entry.parseError ?? "unknown parse error"}`
1716
1972
  );
1717
1973
  completed++;
1718
1974
  options.onProgress?.(completed, files.length);
1719
1975
  return { modified: false, wrapped: 0, skipped: false };
1720
1976
  }
1721
- const result = transform(ast, options.textToKey, transformOpts);
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
+ );
1722
1986
  if (result.modified) {
1723
1987
  try {
1724
- parseFile(result.code, filePath);
1988
+ parseFile(result.code, entry.filePath);
1725
1989
  } catch {
1726
1990
  logWarning(
1727
- `Codegen produced invalid syntax for ${filePath}, file was NOT modified.`
1991
+ `Codegen produced invalid syntax for ${entry.filePath}, file was NOT modified.`
1728
1992
  );
1729
1993
  completed++;
1730
1994
  options.onProgress?.(completed, files.length);
1731
1995
  return { modified: false, wrapped: 0, skipped: true };
1732
1996
  }
1733
- await writeFile(filePath, result.code, "utf-8");
1997
+ await writeFile(entry.filePath, result.code, "utf-8");
1734
1998
  completed++;
1735
1999
  options.onProgress?.(completed, files.length);
1736
- return { modified: true, wrapped: result.stringsWrapped, skipped: false };
2000
+ return {
2001
+ modified: true,
2002
+ wrapped: result.stringsWrapped,
2003
+ skipped: false
2004
+ };
1737
2005
  }
1738
2006
  completed++;
1739
2007
  options.onProgress?.(completed, files.length);
@@ -1758,12 +2026,23 @@ async function codegen(options, cwd = process.cwd()) {
1758
2026
  filesSkipped
1759
2027
  };
1760
2028
  }
2029
+ var SOURCE_EXTENSIONS;
1761
2030
  var init_codegen = __esm({
1762
2031
  "src/codegen/index.ts"() {
1763
2032
  "use strict";
1764
2033
  init_parser();
1765
2034
  init_transform();
1766
2035
  init_logger();
2036
+ SOURCE_EXTENSIONS = [
2037
+ ".ts",
2038
+ ".tsx",
2039
+ ".js",
2040
+ ".jsx",
2041
+ ".mts",
2042
+ ".cts",
2043
+ ".mjs",
2044
+ ".cjs"
2045
+ ];
1767
2046
  }
1768
2047
  });
1769
2048
 
@@ -1907,7 +2186,7 @@ async function translateBatchWithRetry(input, retries) {
1907
2186
  lastError = error;
1908
2187
  if (attempt < retries) {
1909
2188
  const delay = Math.min(Math.pow(2, attempt) * 1e3, 3e4);
1910
- await new Promise((resolve) => setTimeout(resolve, delay));
2189
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
1911
2190
  }
1912
2191
  }
1913
2192
  }
@@ -1974,9 +2253,9 @@ var init_translate = __esm({
1974
2253
 
1975
2254
  // src/writer.ts
1976
2255
  import { writeFile as writeFile2, mkdir } from "fs/promises";
1977
- import { join as join2, dirname } from "path";
2256
+ import { join as join3, dirname as dirname2 } from "path";
1978
2257
  async function writeTranslation(filePath, flatEntries, options) {
1979
- await mkdir(dirname(filePath), { recursive: true });
2258
+ await mkdir(dirname2(filePath), { recursive: true });
1980
2259
  const data = options?.flat ? flatEntries : unflatten(flatEntries);
1981
2260
  const content = JSON.stringify(data, null, 2) + "\n";
1982
2261
  await writeFile2(filePath, content, "utf-8");
@@ -1993,8 +2272,8 @@ async function writeLockFile(messagesDir, sourceFlat, existingLock, translatedKe
1993
2272
  delete lock[key];
1994
2273
  }
1995
2274
  }
1996
- const lockPath = join2(messagesDir, ".translate-lock.json");
1997
- await mkdir(dirname(lockPath), { recursive: true });
2275
+ const lockPath = join3(messagesDir, ".translate-lock.json");
2276
+ await mkdir(dirname2(lockPath), { recursive: true });
1998
2277
  const content = JSON.stringify(lock, null, 2) + "\n";
1999
2278
  await writeFile2(lockPath, content, "utf-8");
2000
2279
  }
@@ -2007,10 +2286,10 @@ var init_writer = __esm({
2007
2286
  });
2008
2287
 
2009
2288
  // src/pipeline.ts
2010
- import { join as join3 } from "path";
2289
+ import { join as join4 } from "path";
2011
2290
  import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
2012
2291
  async function loadMapFile(messagesDir) {
2013
- const mapPath = join3(messagesDir, ".translate-map.json");
2292
+ const mapPath = join4(messagesDir, ".translate-map.json");
2014
2293
  let content;
2015
2294
  try {
2016
2295
  content = await readFile4(mapPath, "utf-8");
@@ -2027,7 +2306,7 @@ async function loadMapFile(messagesDir) {
2027
2306
  }
2028
2307
  }
2029
2308
  async function writeMapFile(messagesDir, map) {
2030
- const mapPath = join3(messagesDir, ".translate-map.json");
2309
+ const mapPath = join4(messagesDir, ".translate-map.json");
2031
2310
  await mkdir2(messagesDir, { recursive: true });
2032
2311
  const content = JSON.stringify(map, null, 2) + "\n";
2033
2312
  await writeFile3(mapPath, content, "utf-8");
@@ -2080,7 +2359,7 @@ async function runScanStep(input) {
2080
2359
  sourceFlat[key] = text2;
2081
2360
  }
2082
2361
  if (mode !== "inline") {
2083
- const sourceFile = join3(
2362
+ const sourceFile = join4(
2084
2363
  config.messagesDir,
2085
2364
  `${config.sourceLocale}.json`
2086
2365
  );
@@ -2134,7 +2413,7 @@ async function runTranslateStep(input) {
2134
2413
  sourceFlat[key] = text2;
2135
2414
  }
2136
2415
  } else {
2137
- const sourceFile = join3(
2416
+ const sourceFile = join4(
2138
2417
  config.messagesDir,
2139
2418
  `${config.sourceLocale}.json`
2140
2419
  );
@@ -2145,7 +2424,7 @@ async function runTranslateStep(input) {
2145
2424
  const localeResults = [];
2146
2425
  for (const locale of locales) {
2147
2426
  const start = Date.now();
2148
- const targetFile = join3(config.messagesDir, `${locale}.json`);
2427
+ const targetFile = join4(config.messagesDir, `${locale}.json`);
2149
2428
  const targetRaw = await loadJsonFile(targetFile);
2150
2429
  const targetFlat = flatten(targetRaw);
2151
2430
  let lockData = await loadLockFile(config.messagesDir);
@@ -2311,8 +2590,9 @@ var init_cli_utils = __esm({
2311
2590
  });
2312
2591
 
2313
2592
  // src/templates/t-component.ts
2314
- function serverTemplate(clientBasename) {
2315
- return `import type { ReactNode } from "react";
2593
+ function serverTemplate(clientBasename, opts) {
2594
+ if (!opts) {
2595
+ return `import type { ReactNode } from "react";
2316
2596
  import { cache } from "react";
2317
2597
  export { I18nProvider } from "./${clientBasename}";
2318
2598
 
@@ -2339,6 +2619,107 @@ export function createT(messages?: Messages) {
2339
2619
  return raw.replace(/\\{(\\w+)\\}/g, (_, k) => String(values[k] ?? \`{\${k}}\`));
2340
2620
  };
2341
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
+ }
2342
2723
  `;
2343
2724
  }
2344
2725
  function generateI18nHelper(opts) {
@@ -2424,17 +2805,17 @@ __export(init_exports, {
2424
2805
  });
2425
2806
  import * as p from "@clack/prompts";
2426
2807
  import { existsSync } from "fs";
2427
- import { basename, join as join4, relative } from "path";
2808
+ import { basename, join as join5, relative } from "path";
2428
2809
  import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
2429
2810
  function detectIncludePatterns(cwd) {
2430
2811
  const patterns = [];
2431
- if (existsSync(join4(cwd, "app")))
2812
+ if (existsSync(join5(cwd, "app")))
2432
2813
  patterns.push("app/**/*.tsx", "app/**/*.jsx");
2433
- if (existsSync(join4(cwd, "src")))
2814
+ if (existsSync(join5(cwd, "src")))
2434
2815
  patterns.push("src/**/*.tsx", "src/**/*.jsx");
2435
- if (existsSync(join4(cwd, "pages")))
2816
+ if (existsSync(join5(cwd, "pages")))
2436
2817
  patterns.push("pages/**/*.tsx", "pages/**/*.jsx");
2437
- if (existsSync(join4(cwd, "src", "app"))) {
2818
+ if (existsSync(join5(cwd, "src", "app"))) {
2438
2819
  return patterns.filter((p2) => !p2.startsWith("app/"));
2439
2820
  }
2440
2821
  return patterns.length > 0 ? patterns : ["**/*.tsx", "**/*.jsx"];
@@ -2447,10 +2828,10 @@ function findPackageInNodeModules(cwd, pkg) {
2447
2828
  let dir = cwd;
2448
2829
  const parts = pkg.split("/");
2449
2830
  while (true) {
2450
- if (existsSync(join4(dir, "node_modules", ...parts, "package.json"))) {
2831
+ if (existsSync(join5(dir, "node_modules", ...parts, "package.json"))) {
2451
2832
  return true;
2452
2833
  }
2453
- const parent = join4(dir, "..");
2834
+ const parent = join5(dir, "..");
2454
2835
  if (parent === dir) break;
2455
2836
  dir = parent;
2456
2837
  }
@@ -2530,22 +2911,22 @@ async function safeWriteModifiedFile(filePath, modified, label) {
2530
2911
  return true;
2531
2912
  }
2532
2913
  function detectSrcDir(cwd) {
2533
- return existsSync(join4(cwd, "src", "app"));
2914
+ return existsSync(join5(cwd, "src", "app"));
2534
2915
  }
2535
2916
  function resolveComponentPath(cwd, componentPath) {
2536
2917
  if (componentPath.startsWith("@/")) {
2537
2918
  const rel = componentPath.slice(2);
2538
- const useSrc = existsSync(join4(cwd, "src"));
2539
- return join4(cwd, useSrc ? "src" : "", rel);
2919
+ const useSrc = existsSync(join5(cwd, "src"));
2920
+ return join5(cwd, useSrc ? "src" : "", rel);
2540
2921
  }
2541
2922
  if (componentPath.startsWith("~/")) {
2542
- return join4(cwd, componentPath.slice(2));
2923
+ return join5(cwd, componentPath.slice(2));
2543
2924
  }
2544
- return join4(cwd, componentPath);
2925
+ return join5(cwd, componentPath);
2545
2926
  }
2546
2927
  function findLayoutFile(base) {
2547
2928
  for (const ext of ["tsx", "jsx", "ts", "js"]) {
2548
- const candidate = join4(base, "app", `layout.${ext}`);
2929
+ const candidate = join5(base, "app", `layout.${ext}`);
2549
2930
  if (existsSync(candidate)) return candidate;
2550
2931
  }
2551
2932
  return void 0;
@@ -2553,7 +2934,7 @@ function findLayoutFile(base) {
2553
2934
  async function createEmptyMessageFiles(msgDir, locales) {
2554
2935
  await mkdir3(msgDir, { recursive: true });
2555
2936
  for (const locale of locales) {
2556
- const msgFile = join4(msgDir, `${locale}.json`);
2937
+ const msgFile = join5(msgDir, `${locale}.json`);
2557
2938
  if (!existsSync(msgFile)) {
2558
2939
  await writeFile4(msgFile, "{}\n", "utf-8");
2559
2940
  }
@@ -2573,14 +2954,14 @@ function ensureAsyncLayout(content) {
2573
2954
  }
2574
2955
  async function setupNextIntl(cwd, sourceLocale, targetLocales, messagesDir) {
2575
2956
  const useSrc = detectSrcDir(cwd);
2576
- const base = useSrc ? join4(cwd, "src") : cwd;
2957
+ const base = useSrc ? join5(cwd, "src") : cwd;
2577
2958
  const allLocales = [sourceLocale, ...targetLocales];
2578
2959
  const filesCreated = [];
2579
- const i18nDir = join4(base, "i18n");
2960
+ const i18nDir = join5(base, "i18n");
2580
2961
  await mkdir3(i18nDir, { recursive: true });
2581
- const requestFile = join4(i18nDir, "request.ts");
2962
+ const requestFile = join5(i18nDir, "request.ts");
2582
2963
  if (!existsSync(requestFile)) {
2583
- const relMessages = relative(i18nDir, join4(cwd, messagesDir));
2964
+ const relMessages = relative(i18nDir, join5(cwd, messagesDir));
2584
2965
  const allLocalesStr = allLocales.map((l) => `"${l}"`).join(", ");
2585
2966
  await writeFile4(
2586
2967
  requestFile,
@@ -2621,7 +3002,7 @@ export default getRequestConfig(async () => {
2621
3002
  );
2622
3003
  filesCreated.push(relative(cwd, requestFile));
2623
3004
  }
2624
- const nextConfigPath = join4(cwd, "next.config.ts");
3005
+ const nextConfigPath = join5(cwd, "next.config.ts");
2625
3006
  if (existsSync(nextConfigPath)) {
2626
3007
  const content = await readFile5(nextConfigPath, "utf-8");
2627
3008
  if (!content.includes("next-intl")) {
@@ -2671,31 +3052,31 @@ export default getRequestConfig(async () => {
2671
3052
  }
2672
3053
  }
2673
3054
  }
2674
- await createEmptyMessageFiles(join4(cwd, messagesDir), allLocales);
3055
+ await createEmptyMessageFiles(join5(cwd, messagesDir), allLocales);
2675
3056
  if (filesCreated.length > 0) {
2676
3057
  p.log.success(`next-intl configured: ${filesCreated.join(", ")}`);
2677
3058
  }
2678
3059
  }
2679
- async function dropInlineComponents(cwd, componentPath) {
3060
+ async function dropInlineComponents(cwd, componentPath, localeOpts) {
2680
3061
  const fsPath = resolveComponentPath(cwd, componentPath);
2681
- const dir = join4(fsPath, "..");
3062
+ const dir = join5(fsPath, "..");
2682
3063
  await mkdir3(dir, { recursive: true });
2683
3064
  const clientFile = `${fsPath}.tsx`;
2684
3065
  const serverFile = `${fsPath}-server.tsx`;
2685
3066
  const clientBasename = basename(fsPath);
2686
3067
  await writeFile4(clientFile, CLIENT_TEMPLATE, "utf-8");
2687
- await writeFile4(serverFile, serverTemplate(clientBasename), "utf-8");
3068
+ await writeFile4(serverFile, serverTemplate(clientBasename, localeOpts), "utf-8");
2688
3069
  const relClient = relative(cwd, clientFile);
2689
3070
  const relServer = relative(cwd, serverFile);
2690
3071
  p.log.success(`Created inline components: ${relClient}, ${relServer}`);
2691
3072
  }
2692
3073
  async function setupInlineI18n(cwd, componentPath, sourceLocale, targetLocales, messagesDir) {
2693
- const useSrc = existsSync(join4(cwd, "src"));
2694
- const base = useSrc ? join4(cwd, "src") : cwd;
3074
+ const useSrc = existsSync(join5(cwd, "src"));
3075
+ const base = useSrc ? join5(cwd, "src") : cwd;
2695
3076
  const filesCreated = [];
2696
- const i18nDir = join4(base, "i18n");
3077
+ const i18nDir = join5(base, "i18n");
2697
3078
  await mkdir3(i18nDir, { recursive: true });
2698
- const helperFile = join4(i18nDir, "index.ts");
3079
+ const helperFile = join5(i18nDir, "index.ts");
2699
3080
  if (!existsSync(helperFile)) {
2700
3081
  const helperContent = generateI18nHelper({
2701
3082
  sourceLocale,
@@ -2736,7 +3117,7 @@ import { getLocale, getMessages } from "@/i18n";
2736
3117
  }
2737
3118
  }
2738
3119
  }
2739
- await createEmptyMessageFiles(join4(cwd, messagesDir), [
3120
+ await createEmptyMessageFiles(join5(cwd, messagesDir), [
2740
3121
  sourceLocale,
2741
3122
  ...targetLocales
2742
3123
  ]);
@@ -2746,7 +3127,7 @@ import { getLocale, getMessages } from "@/i18n";
2746
3127
  }
2747
3128
  async function runInitWizard() {
2748
3129
  const cwd = process.cwd();
2749
- const configPath = join4(cwd, "translate-kit.config.ts");
3130
+ const configPath = join5(cwd, "translate-kit.config.ts");
2750
3131
  p.intro("translate-kit setup");
2751
3132
  if (existsSync(configPath)) {
2752
3133
  const overwrite = await p.confirm({
@@ -2877,7 +3258,11 @@ async function runInitWizard() {
2877
3258
  await writeFile4(configPath, configContent, "utf-8");
2878
3259
  p.log.success("Created translate-kit.config.ts");
2879
3260
  if (mode === "inline" && componentPath) {
2880
- await dropInlineComponents(cwd, componentPath);
3261
+ await dropInlineComponents(cwd, componentPath, {
3262
+ sourceLocale,
3263
+ targetLocales,
3264
+ messagesDir
3265
+ });
2881
3266
  await setupInlineI18n(
2882
3267
  cwd,
2883
3268
  componentPath,
@@ -3023,7 +3408,7 @@ init_usage();
3023
3408
  init_cli_utils();
3024
3409
  import "dotenv/config";
3025
3410
  import { defineCommand, runMain } from "citty";
3026
- import { join as join5 } from "path";
3411
+ import { join as join6 } from "path";
3027
3412
  var translateCommand = defineCommand({
3028
3413
  meta: {
3029
3414
  name: "translate",
@@ -3075,13 +3460,13 @@ var translateCommand = defineCommand({
3075
3460
  sourceFlat2[key] = text2;
3076
3461
  }
3077
3462
  } else {
3078
- const sourceFile = join5(messagesDir, `${sourceLocale}.json`);
3463
+ const sourceFile = join6(messagesDir, `${sourceLocale}.json`);
3079
3464
  const sourceRaw = await loadJsonFile(sourceFile);
3080
3465
  sourceFlat2 = flatten(sourceRaw);
3081
3466
  }
3082
3467
  if (Object.keys(sourceFlat2).length === 0) {
3083
3468
  logError(
3084
- mode === "inline" ? `No keys found in .translate-map.json. Run 'translate-kit scan' first.` : `No keys found in ${join5(messagesDir, `${sourceLocale}.json`)}`
3469
+ mode === "inline" ? `No keys found in .translate-map.json. Run 'translate-kit scan' first.` : `No keys found in ${join6(messagesDir, `${sourceLocale}.json`)}`
3085
3470
  );
3086
3471
  process.exit(1);
3087
3472
  }
@@ -3106,13 +3491,13 @@ var translateCommand = defineCommand({
3106
3491
  sourceFlat[key] = text2;
3107
3492
  }
3108
3493
  } else {
3109
- const sourceFile = join5(messagesDir, `${sourceLocale}.json`);
3494
+ const sourceFile = join6(messagesDir, `${sourceLocale}.json`);
3110
3495
  const sourceRaw = await loadJsonFile(sourceFile);
3111
3496
  sourceFlat = flatten(sourceRaw);
3112
3497
  }
3113
3498
  if (Object.keys(sourceFlat).length === 0) {
3114
3499
  logError(
3115
- mode === "inline" ? `No keys found in .translate-map.json. Run 'translate-kit scan' first.` : `No keys found in ${join5(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`)}`
3116
3501
  );
3117
3502
  process.exit(1);
3118
3503
  }
@@ -3215,7 +3600,7 @@ var scanCommand = defineCommand({
3215
3600
  "Inline mode: source text stays in code, no source locale JSON created."
3216
3601
  );
3217
3602
  } else {
3218
- const sourceFile = join5(
3603
+ const sourceFile = join6(
3219
3604
  config.messagesDir,
3220
3605
  `${config.sourceLocale}.json`
3221
3606
  );