@lang-tag/cli 0.19.0 → 0.20.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.
@@ -214,7 +214,7 @@ function pathBasedConfigGenerator(options = {}) {
214
214
  );
215
215
  path$1 = transformedParts.join(".");
216
216
  }
217
- const newConfig = event.config ? { ...event.config } : {};
217
+ const newConfig = event.getCurrentConfig();
218
218
  if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
219
219
  if (path$1) {
220
220
  newConfig.path = path$1;
@@ -442,13 +442,10 @@ function configKeeper(options = {}) {
442
442
  if (keepMode !== "namespace" && keepMode !== "path" && keepMode !== "both") {
443
443
  return;
444
444
  }
445
- let restoredConfig;
445
+ let restoredConfig = event.getCurrentConfig();
446
446
  if (event.savedConfig === null) {
447
- restoredConfig = { ...event.config };
448
447
  delete restoredConfig.namespace;
449
448
  delete restoredConfig.path;
450
- } else {
451
- restoredConfig = { ...event.savedConfig };
452
449
  }
453
450
  let needsSave = false;
454
451
  const restorePropertyIfNeeded = (propertyKey) => {
@@ -196,7 +196,7 @@ function pathBasedConfigGenerator(options = {}) {
196
196
  );
197
197
  path2 = transformedParts.join(".");
198
198
  }
199
- const newConfig = event.config ? { ...event.config } : {};
199
+ const newConfig = event.getCurrentConfig();
200
200
  if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
201
201
  if (path2) {
202
202
  newConfig.path = path2;
@@ -424,13 +424,10 @@ function configKeeper(options = {}) {
424
424
  if (keepMode !== "namespace" && keepMode !== "path" && keepMode !== "both") {
425
425
  return;
426
426
  }
427
- let restoredConfig;
427
+ let restoredConfig = event.getCurrentConfig();
428
428
  if (event.savedConfig === null) {
429
- restoredConfig = { ...event.config };
430
429
  delete restoredConfig.namespace;
431
430
  delete restoredConfig.path;
432
- } else {
433
- restoredConfig = { ...event.savedConfig };
434
431
  }
435
432
  let needsSave = false;
436
433
  const restorePropertyIfNeeded = (propertyKey) => {
package/index.cjs CHANGED
@@ -8,12 +8,13 @@ const globby = require("globby");
8
8
  const path = require("path");
9
9
  const JSON5 = require("json5");
10
10
  const promises = require("fs/promises");
11
- const path$1 = require("pathe");
12
11
  const url = require("url");
12
+ const path$1 = require("pathe");
13
13
  require("case");
14
14
  const namespaceCollector = require("./chunks/namespace-collector.cjs");
15
15
  const micromatch = require("micromatch");
16
16
  const acorn = require("acorn");
17
+ const tsMorph = require("ts-morph");
17
18
  const mustache = require("mustache");
18
19
  const checkbox = require("@inquirer/checkbox");
19
20
  const confirm = require("@inquirer/confirm");
@@ -564,7 +565,9 @@ async function $LT_CollectCandidateFilesWithTags(props) {
564
565
  langTagConfig: config
565
566
  });
566
567
  }
567
- tags = $LT_FilterEmptyNamespaceTags(tags, logger);
568
+ if (!props.skipEmptyNamespaceCheck) {
569
+ tags = $LT_FilterEmptyNamespaceTags(tags, logger);
570
+ }
568
571
  const relativeFilePath = path.relative(cwd, filePath);
569
572
  candidates.push({ relativeFilePath, tags });
570
573
  }
@@ -905,9 +908,30 @@ async function $LT_WriteToCollections({
905
908
  }
906
909
  await config.collect.collector.postWrite(changedCollections);
907
910
  }
911
+ function deepFreezeObject(obj) {
912
+ const propNames = Object.getOwnPropertyNames(obj);
913
+ for (const name of propNames) {
914
+ const value = obj[name];
915
+ if (value && typeof value === "object") {
916
+ deepFreezeObject(value);
917
+ }
918
+ }
919
+ return Object.freeze(obj);
920
+ }
921
+ function formatFileUrlForDisplay(filePath) {
922
+ return url.pathToFileURL(filePath).href.replace(/\[/g, "%5B").replace(/\]/g, "%5D").replace(/\(/g, "%28").replace(/\)/g, "%29");
923
+ }
924
+ function formatExecutionTime(milliseconds) {
925
+ if (milliseconds >= 1e3) {
926
+ const seconds = milliseconds / 1e3;
927
+ return `${seconds.toFixed(1)}s`;
928
+ }
929
+ return `${Math.round(milliseconds)}ms`;
930
+ }
908
931
  const LANG_TAG_DEFAULT_CONFIG = {
909
932
  tagName: "lang",
910
933
  isLibrary: false,
934
+ enforceLibraryTagPrefix: true,
911
935
  includes: ["src/**/*.{js,ts,jsx,tsx}"],
912
936
  excludes: ["node_modules", "dist", "build"],
913
937
  localesDirectory: "locales",
@@ -968,6 +992,7 @@ This will enable import of language tags from external packages.
968
992
  }
969
993
  },
970
994
  translationArgPosition: 1,
995
+ hideDistDir: "dist",
971
996
  onConfigGeneration: async (event) => {
972
997
  event.logger.info(
973
998
  "Config generation event is not configured. Add onConfigGeneration handler to customize config generation."
@@ -1006,6 +1031,9 @@ async function $LT_ReadConfig(projectPath) {
1006
1031
  if (!config.collect.collector) {
1007
1032
  throw new Error("Collector not found! (config.collect.collector)");
1008
1033
  }
1034
+ if (config.isLibrary && (config.enforceLibraryTagPrefix ?? true) && config.tagName && !config.tagName.startsWith("_")) {
1035
+ config.tagName = `_${config.tagName}`;
1036
+ }
1009
1037
  return config;
1010
1038
  } catch (error) {
1011
1039
  throw error;
@@ -1350,9 +1378,9 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
1350
1378
  console.error("Failed to colorize config:", error);
1351
1379
  }
1352
1380
  }
1353
- const encodedPath = encodeURI(filePath);
1381
+ const fileUrl = formatFileUrlForDisplay(filePath);
1354
1382
  console.log(
1355
- `${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`
1383
+ `${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}${fileUrl}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`
1356
1384
  );
1357
1385
  printLines(colorizedWhole.split("\n"), startLine, errorLines, condense);
1358
1386
  } catch (error) {
@@ -1482,9 +1510,14 @@ async function $LT_GetCommandEssentials() {
1482
1510
  };
1483
1511
  }
1484
1512
  async function $LT_CMD_Collect(options) {
1513
+ const startTime = Date.now();
1485
1514
  const { config, logger } = await $LT_GetCommandEssentials();
1486
1515
  logger.info("Collecting translations from source files...");
1487
- const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
1516
+ const files = await $LT_CollectCandidateFilesWithTags({
1517
+ config,
1518
+ logger,
1519
+ skipEmptyNamespaceCheck: config.isLibrary
1520
+ });
1488
1521
  if (config.debug) {
1489
1522
  for (let file of files) {
1490
1523
  logger.debug("Found {count} translations tags inside: {file}", {
@@ -1493,8 +1526,19 @@ async function $LT_CMD_Collect(options) {
1493
1526
  });
1494
1527
  }
1495
1528
  }
1529
+ const totalTags = files.reduce((sum, file) => sum + file.tags.length, 0);
1496
1530
  if (config.isLibrary) {
1531
+ if (totalTags === 0 && (config.enforceLibraryTagPrefix ?? true) && config.tagName) {
1532
+ const baseTagName = config.tagName.startsWith("_") ? config.tagName.substring(1) : config.tagName;
1533
+ console.log("");
1534
+ logger.warn(
1535
+ '⚠️ No translation tags found in your library code.\n This might be because enforceLibraryTagPrefix is enabled.\n Remember: your tag function must be named {prefixedBaseTagName} (with "_" prefix), not {baseTagName}.\n Example: export function {prefixedBaseTagName}(...) instead of export function {baseTagName}(...)\n The prefix prevents the tag from appearing in TypeScript autocomplete after compilation.',
1536
+ { prefixedBaseTagName: `_${baseTagName}`, baseTagName }
1537
+ );
1538
+ }
1497
1539
  await $LT_WriteAsExportFile({ config, logger, files });
1540
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1541
+ logger.debug("Collection completed ({time})", { time: executionTime });
1498
1542
  return;
1499
1543
  }
1500
1544
  try {
@@ -1503,10 +1547,6 @@ async function $LT_CMD_Collect(options) {
1503
1547
  files,
1504
1548
  config
1505
1549
  });
1506
- const totalTags = files.reduce(
1507
- (sum, file) => sum + file.tags.length,
1508
- 0
1509
- );
1510
1550
  logger.debug("Found {totalTags} translation tags", { totalTags });
1511
1551
  await $LT_WriteToCollections({
1512
1552
  config,
@@ -1514,15 +1554,252 @@ async function $LT_CMD_Collect(options) {
1514
1554
  logger,
1515
1555
  clean: options?.clean
1516
1556
  });
1557
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1558
+ logger.debug("Collection completed ({time})", { time: executionTime });
1517
1559
  } catch (e) {
1518
1560
  const prefix = "LangTagConflictResolution:";
1519
1561
  if (e.message.startsWith(prefix)) {
1520
1562
  logger.error(e.message.substring(prefix.length));
1563
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1564
+ logger.debug("Collection completed ({time})", {
1565
+ time: executionTime
1566
+ });
1521
1567
  return;
1522
1568
  }
1523
1569
  throw e;
1524
1570
  }
1525
1571
  }
1572
+ function $LT_HideExportsInDtsFile(dtsFilePath, variableNames) {
1573
+ const originalContent = fs.readFileSync(dtsFilePath, "utf-8");
1574
+ const project = new tsMorph.Project({
1575
+ skipAddingFilesFromTsConfig: true,
1576
+ skipFileDependencyResolution: true,
1577
+ skipLoadingLibFiles: true
1578
+ });
1579
+ const sourceFile = project.addSourceFileAtPath(dtsFilePath);
1580
+ let hasChanges = false;
1581
+ const exportsToHide = [];
1582
+ const processedStatements = /* @__PURE__ */ new Set();
1583
+ for (const declaration of sourceFile.getVariableDeclarations()) {
1584
+ const name = declaration.getName();
1585
+ if (variableNames.has(name)) {
1586
+ const parent = declaration.getParent();
1587
+ if (parent && parent.getKindName() === "VariableDeclarationList") {
1588
+ const varList = parent;
1589
+ const grandParent = varList.getParent();
1590
+ if (grandParent && grandParent.getKindName() === "VariableStatement") {
1591
+ const varStatement = grandParent;
1592
+ if (varStatement.hasExportKeyword()) {
1593
+ if (!processedStatements.has(varStatement)) {
1594
+ processedStatements.add(varStatement);
1595
+ exportsToHide.push(name);
1596
+ varStatement.toggleModifier("export", false);
1597
+ hasChanges = true;
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+ }
1603
+ }
1604
+ if (!hasChanges) {
1605
+ return {
1606
+ hiddenCount: 0,
1607
+ modifiedContent: originalContent,
1608
+ originalContent
1609
+ };
1610
+ }
1611
+ const modifiedContent = sourceFile.getFullText();
1612
+ return {
1613
+ hiddenCount: exportsToHide.length,
1614
+ modifiedContent,
1615
+ originalContent
1616
+ };
1617
+ }
1618
+ function extractPrefixesFromIncludes(includes) {
1619
+ const prefixes = /* @__PURE__ */ new Set();
1620
+ for (const pattern of includes) {
1621
+ const groupMatch = pattern.match(/^\(([^)]+)\)\/\*\*/);
1622
+ if (groupMatch) {
1623
+ const options = groupMatch[1].split("|").map((s) => s.trim());
1624
+ options.forEach((opt) => prefixes.add(opt));
1625
+ continue;
1626
+ }
1627
+ const simpleMatch = pattern.match(/^([^/]+)\/\*\*/);
1628
+ if (simpleMatch) {
1629
+ prefixes.add(simpleMatch[1]);
1630
+ continue;
1631
+ }
1632
+ const singleMatch = pattern.match(/^([^/]+)\//);
1633
+ if (singleMatch) {
1634
+ prefixes.add(singleMatch[1]);
1635
+ }
1636
+ }
1637
+ return Array.from(prefixes);
1638
+ }
1639
+ function findMatchingDtsFile(sourceFilePath, sourceRelativePath, dtsFileMap, config) {
1640
+ const sourceBaseName = path.basename(
1641
+ sourceFilePath,
1642
+ path.extname(sourceFilePath)
1643
+ );
1644
+ const relativePathKey = sourceRelativePath.replace(
1645
+ /\.(ts|tsx|js|jsx)$/,
1646
+ ""
1647
+ );
1648
+ let dtsFilePath = dtsFileMap.get(relativePathKey);
1649
+ if (dtsFilePath) {
1650
+ return { dtsFilePath, strategy: "relative-path" };
1651
+ }
1652
+ if (config?.includes) {
1653
+ const prefixes = extractPrefixesFromIncludes(config.includes);
1654
+ for (const prefix of prefixes) {
1655
+ const prefixPattern = new RegExp(
1656
+ `^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`
1657
+ );
1658
+ if (prefixPattern.test(sourceRelativePath)) {
1659
+ const strippedPath = sourceRelativePath.replace(
1660
+ prefixPattern,
1661
+ ""
1662
+ );
1663
+ const strippedPathKey = strippedPath.replace(
1664
+ /\.(ts|tsx|js|jsx)$/,
1665
+ ""
1666
+ );
1667
+ dtsFilePath = dtsFileMap.get(strippedPathKey);
1668
+ if (dtsFilePath) {
1669
+ return {
1670
+ dtsFilePath,
1671
+ strategy: "includes-prefix-stripped"
1672
+ };
1673
+ }
1674
+ }
1675
+ }
1676
+ }
1677
+ dtsFilePath = dtsFileMap.get(sourceBaseName);
1678
+ if (dtsFilePath) {
1679
+ return { dtsFilePath, strategy: "base-name" };
1680
+ }
1681
+ return { dtsFilePath: null, strategy: null };
1682
+ }
1683
+ async function $LT_MatchSourceToDtsFiles(files, distPath, cwd, config) {
1684
+ const sourceFileToVariables = /* @__PURE__ */ new Map();
1685
+ for (const file of files) {
1686
+ const sourceFilePath = path.resolve(cwd, file.relativeFilePath);
1687
+ const variableNames = /* @__PURE__ */ new Set();
1688
+ for (const tag of file.tags) {
1689
+ if (tag.variableName) {
1690
+ variableNames.add(tag.variableName);
1691
+ }
1692
+ }
1693
+ if (variableNames.size > 0) {
1694
+ sourceFileToVariables.set(sourceFilePath, variableNames);
1695
+ }
1696
+ }
1697
+ if (sourceFileToVariables.size === 0) {
1698
+ return [];
1699
+ }
1700
+ const dtsFiles = await globby.globby("**/*.d.ts", {
1701
+ cwd: distPath,
1702
+ absolute: true
1703
+ });
1704
+ if (dtsFiles.length === 0) {
1705
+ return [];
1706
+ }
1707
+ const dtsFileMap = /* @__PURE__ */ new Map();
1708
+ for (const dtsFile of dtsFiles) {
1709
+ const relativeDtsPath = path.relative(distPath, dtsFile);
1710
+ const baseName = path.basename(dtsFile, ".d.ts");
1711
+ const relativePathWithoutExt = relativeDtsPath.replace(/\.d\.ts$/, "");
1712
+ dtsFileMap.set(relativePathWithoutExt, dtsFile);
1713
+ dtsFileMap.set(baseName, dtsFile);
1714
+ }
1715
+ const matches = [];
1716
+ const processedDtsFiles = /* @__PURE__ */ new Set();
1717
+ for (const [sourceFilePath, variableNames] of sourceFileToVariables) {
1718
+ const sourceRelativePath = path.relative(cwd, sourceFilePath);
1719
+ const match = findMatchingDtsFile(
1720
+ sourceFilePath,
1721
+ sourceRelativePath,
1722
+ dtsFileMap,
1723
+ config
1724
+ );
1725
+ if (!match.dtsFilePath) {
1726
+ continue;
1727
+ }
1728
+ if (processedDtsFiles.has(match.dtsFilePath)) {
1729
+ continue;
1730
+ }
1731
+ processedDtsFiles.add(match.dtsFilePath);
1732
+ matches.push({
1733
+ sourceFilePath,
1734
+ sourceRelativePath,
1735
+ dtsFilePath: match.dtsFilePath,
1736
+ variableNames
1737
+ });
1738
+ }
1739
+ return matches;
1740
+ }
1741
+ async function $LT_CMD_HideCompiledExports(options) {
1742
+ const { config, logger } = await $LT_GetCommandEssentials();
1743
+ const distDir = options?.distDir || config.hideDistDir || "dist";
1744
+ const distPath = path.resolve(process$1.cwd(), distDir);
1745
+ if (!fs.existsSync(distPath)) {
1746
+ logger.warn("Dist directory does not exist: {distPath}", { distPath });
1747
+ return;
1748
+ }
1749
+ logger.info("Scanning source files for lang-tag variables...");
1750
+ const files = await $LT_CollectCandidateFilesWithTags({
1751
+ config,
1752
+ logger,
1753
+ skipEmptyNamespaceCheck: true
1754
+ });
1755
+ const matches = await $LT_MatchSourceToDtsFiles(
1756
+ files,
1757
+ distPath,
1758
+ process$1.cwd(),
1759
+ config
1760
+ );
1761
+ if (matches.length === 0) {
1762
+ logger.info(
1763
+ "No lang-tag variables found in source files or no matching .d.ts files found."
1764
+ );
1765
+ return;
1766
+ }
1767
+ logger.info("Found {count} .d.ts files to process", {
1768
+ count: matches.length
1769
+ });
1770
+ let hiddenCount = 0;
1771
+ for (const match of matches) {
1772
+ try {
1773
+ const result = $LT_HideExportsInDtsFile(
1774
+ match.dtsFilePath,
1775
+ match.variableNames
1776
+ );
1777
+ if (!result.hiddenCount) {
1778
+ continue;
1779
+ }
1780
+ fs.writeFileSync(match.dtsFilePath, result.modifiedContent, "utf-8");
1781
+ hiddenCount += result.hiddenCount;
1782
+ const hiddenVariables = Array.from(match.variableNames).join(", ");
1783
+ logger.debug(
1784
+ "Hidden exports from {file}: {variables} (from {sourceFile})",
1785
+ {
1786
+ file: path.relative(process$1.cwd(), match.dtsFilePath),
1787
+ variables: hiddenVariables,
1788
+ sourceFile: match.sourceRelativePath
1789
+ }
1790
+ );
1791
+ } catch (error) {
1792
+ logger.warn("Error processing file {file}: {error}", {
1793
+ file: match.sourceRelativePath,
1794
+ error: error.message || String(error)
1795
+ });
1796
+ }
1797
+ }
1798
+ logger.success("Hidden {hiddenCount} exports from {fileCount} files.", {
1799
+ hiddenCount,
1800
+ fileCount: matches.length
1801
+ });
1802
+ }
1526
1803
  async function $LT_CollectExportFiles(logger) {
1527
1804
  const nodeModulesPath = path$1.join(process$1.cwd(), "node_modules");
1528
1805
  if (!fs.existsSync(nodeModulesPath)) {
@@ -1641,11 +1918,11 @@ async function generateImportFiles(config, logger, importManager) {
1641
1918
  const content = renderTemplate$2(templateData);
1642
1919
  await $LT_EnsureDirectoryExists(path.dirname(filePath));
1643
1920
  await promises.writeFile(filePath, content, "utf-8");
1644
- const encodedFilePath = encodeURI(filePath);
1921
+ const fileUrl = formatFileUrlForDisplay(filePath);
1645
1922
  logger.success('Created tag file: "{file}"', {
1646
1923
  file: importedFile.pathRelativeToImportDir
1647
1924
  });
1648
- logger.debug(" └── link: file://{path}", { path: encodedFilePath });
1925
+ logger.debug(" └── link: {url}", { url: fileUrl });
1649
1926
  }
1650
1927
  }
1651
1928
  class ImportManager {
@@ -2144,7 +2421,10 @@ async function detectInitTagOptions(options, config) {
2144
2421
  const isTypeScript = options.typescript !== void 0 ? options.typescript : detectTypeScript(packageJson);
2145
2422
  const isReact = options.react !== void 0 ? options.react : detectReact(packageJson);
2146
2423
  const isLibrary = options.library !== void 0 ? options.library : config.isLibrary;
2147
- const tagName = options.name || config.tagName || "lang";
2424
+ let tagName = options.name || config.tagName || "lang";
2425
+ if (isLibrary && (config.enforceLibraryTagPrefix ?? true) && !tagName.startsWith("_")) {
2426
+ tagName = `_${tagName}`;
2427
+ }
2148
2428
  const fileExtension = isLibrary && isReact ? isTypeScript ? "tsx" : "jsx" : isTypeScript ? "ts" : "js";
2149
2429
  return {
2150
2430
  tagName,
@@ -2245,22 +2525,22 @@ async function $LT_CMD_InitTagFile(options = {}) {
2245
2525
  "2. Create your translation objects and use the tag function"
2246
2526
  );
2247
2527
  logger.info('3. Run "lang-tag collect" to extract translations');
2528
+ if (renderOptions.isLibrary && renderOptions.tagName.startsWith("_") && (config.enforceLibraryTagPrefix ?? true)) {
2529
+ console.log("");
2530
+ logger.info(
2531
+ '📌 Important: Library tag prefix enforcement is enabled\n Your tag uses "_" prefix: {tagName} (instead of {baseTagName})\n This prevents the tag from appearing in TypeScript autocomplete after compilation\n Always use {tagName} (with prefix) in your library code\n This is a best practice for library internals - it keeps your API clean\n The prefix is automatically added by the enforceLibraryTagPrefix option\n To disable this behavior, set enforceLibraryTagPrefix: false in your config',
2532
+ {
2533
+ tagName: renderOptions.tagName,
2534
+ baseTagName: renderOptions.tagName.substring(1)
2535
+ }
2536
+ );
2537
+ }
2248
2538
  } catch (error) {
2249
2539
  logger.error("Failed to write file: {error}", {
2250
2540
  error: error?.message
2251
2541
  });
2252
2542
  }
2253
2543
  }
2254
- function deepFreezeObject(obj) {
2255
- const propNames = Object.getOwnPropertyNames(obj);
2256
- for (const name of propNames) {
2257
- const value = obj[name];
2258
- if (value && typeof value === "object") {
2259
- deepFreezeObject(value);
2260
- }
2261
- }
2262
- return Object.freeze(obj);
2263
- }
2264
2544
  async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
2265
2545
  let libraryImportsDir = config.import.dir;
2266
2546
  if (!libraryImportsDir.endsWith(path.sep)) libraryImportsDir += path.sep;
@@ -2294,13 +2574,23 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
2294
2574
  event.isSaved = true;
2295
2575
  event.savedConfig = updatedConfig;
2296
2576
  logger.debug(
2297
- 'Called save for "{path}" with config "{config}" triggered by: ("{trigger}")',
2577
+ 'Called save for "{path}"{varName} with config "{config}" triggered by: ("{trigger}")',
2298
2578
  {
2299
2579
  path: path$12,
2300
2580
  config: JSON.stringify(updatedConfig),
2301
- trigger: triggerName || "-"
2581
+ trigger: triggerName || "-",
2582
+ varName: tag.variableName ? `(${tag.variableName})` : ""
2302
2583
  }
2303
2584
  );
2585
+ },
2586
+ getCurrentConfig: () => {
2587
+ if (event.savedConfig !== void 0 && event.savedConfig !== null) {
2588
+ return { ...event.savedConfig };
2589
+ }
2590
+ if (event.config) {
2591
+ return { ...event.config };
2592
+ }
2593
+ return {};
2304
2594
  }
2305
2595
  };
2306
2596
  await config.onConfigGeneration(event);
@@ -2315,10 +2605,10 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
2315
2605
  if (replacements.length) {
2316
2606
  const newContent = processor.replaceTags(fileContent, replacements);
2317
2607
  await promises.writeFile(file, newContent, "utf-8");
2318
- const encodedFile = encodeURI(file);
2608
+ const fileUrl = formatFileUrlForDisplay(file);
2319
2609
  logger.info(
2320
- 'Lang tag configurations written for file "{path}" (file://{file}:{line})',
2321
- { path: path$12, file: encodedFile, line: lastUpdatedLine }
2610
+ 'Lang tag configurations written for file "{path}" ({url}:{line})',
2611
+ { path: path$12, url: fileUrl, line: lastUpdatedLine }
2322
2612
  );
2323
2613
  return true;
2324
2614
  }
@@ -2331,6 +2621,7 @@ function isConfigSame(c1, c2) {
2331
2621
  return false;
2332
2622
  }
2333
2623
  async function $LT_CMD_RegenerateTags() {
2624
+ const startTime = Date.now();
2334
2625
  const { config, logger } = await $LT_GetCommandEssentials();
2335
2626
  const files = await globby.globby(config.includes, {
2336
2627
  cwd: process.cwd(),
@@ -2351,11 +2642,13 @@ async function $LT_CMD_RegenerateTags() {
2351
2642
  dirty = true;
2352
2643
  }
2353
2644
  }
2645
+ const executionTime = formatExecutionTime(Date.now() - startTime);
2354
2646
  if (!dirty) {
2355
2647
  logger.info(
2356
2648
  "No changes were made based on the current configuration and files"
2357
2649
  );
2358
2650
  }
2651
+ logger.debug("Regeneration completed ({time})", { time: executionTime });
2359
2652
  }
2360
2653
  function getBasePath(pattern) {
2361
2654
  const globStartIndex = pattern.indexOf("*");
@@ -2464,6 +2757,14 @@ function createCli() {
2464
2757
  ).action(async (options) => {
2465
2758
  await $LT_CMD_InitTagFile(options);
2466
2759
  });
2760
+ commander.program.command("hide-compiled-exports").alias("hce").description(
2761
+ "Hide compiled .d.ts exports of lang-tag variables (remove export modifier, keep type)"
2762
+ ).option(
2763
+ "-d, --dist-dir <dir>",
2764
+ 'Dist directory to process (default: from config or "dist")'
2765
+ ).action(async (options) => {
2766
+ await $LT_CMD_HideCompiledExports({ distDir: options.distDir });
2767
+ });
2467
2768
  return commander.program;
2468
2769
  }
2469
2770
  createCli().parse();
package/index.js CHANGED
@@ -2,18 +2,19 @@
2
2
  import { program } from "commander";
3
3
  import * as process$1 from "node:process";
4
4
  import process__default from "node:process";
5
- import fs, { readFileSync, existsSync, readdirSync, statSync } from "fs";
5
+ import fs, { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from "fs";
6
6
  import { globby } from "globby";
7
7
  import * as path from "path";
8
8
  import path__default, { dirname, resolve, join, sep } from "path";
9
9
  import JSON5 from "json5";
10
10
  import { mkdir, writeFile, readFile } from "fs/promises";
11
- import path$1, { resolve as resolve$1 } from "pathe";
12
11
  import { pathToFileURL, fileURLToPath } from "url";
12
+ import path$1, { resolve as resolve$1 } from "pathe";
13
13
  import "case";
14
14
  import { N as NamespaceCollector } from "./chunks/namespace-collector.js";
15
15
  import micromatch from "micromatch";
16
16
  import * as acorn from "acorn";
17
+ import { Project } from "ts-morph";
17
18
  import mustache from "mustache";
18
19
  import checkbox from "@inquirer/checkbox";
19
20
  import confirm from "@inquirer/confirm";
@@ -544,7 +545,9 @@ async function $LT_CollectCandidateFilesWithTags(props) {
544
545
  langTagConfig: config
545
546
  });
546
547
  }
547
- tags = $LT_FilterEmptyNamespaceTags(tags, logger);
548
+ if (!props.skipEmptyNamespaceCheck) {
549
+ tags = $LT_FilterEmptyNamespaceTags(tags, logger);
550
+ }
548
551
  const relativeFilePath = path__default.relative(cwd, filePath);
549
552
  candidates.push({ relativeFilePath, tags });
550
553
  }
@@ -885,9 +888,30 @@ async function $LT_WriteToCollections({
885
888
  }
886
889
  await config.collect.collector.postWrite(changedCollections);
887
890
  }
891
+ function deepFreezeObject(obj) {
892
+ const propNames = Object.getOwnPropertyNames(obj);
893
+ for (const name of propNames) {
894
+ const value = obj[name];
895
+ if (value && typeof value === "object") {
896
+ deepFreezeObject(value);
897
+ }
898
+ }
899
+ return Object.freeze(obj);
900
+ }
901
+ function formatFileUrlForDisplay(filePath) {
902
+ return pathToFileURL(filePath).href.replace(/\[/g, "%5B").replace(/\]/g, "%5D").replace(/\(/g, "%28").replace(/\)/g, "%29");
903
+ }
904
+ function formatExecutionTime(milliseconds) {
905
+ if (milliseconds >= 1e3) {
906
+ const seconds = milliseconds / 1e3;
907
+ return `${seconds.toFixed(1)}s`;
908
+ }
909
+ return `${Math.round(milliseconds)}ms`;
910
+ }
888
911
  const LANG_TAG_DEFAULT_CONFIG = {
889
912
  tagName: "lang",
890
913
  isLibrary: false,
914
+ enforceLibraryTagPrefix: true,
891
915
  includes: ["src/**/*.{js,ts,jsx,tsx}"],
892
916
  excludes: ["node_modules", "dist", "build"],
893
917
  localesDirectory: "locales",
@@ -948,6 +972,7 @@ This will enable import of language tags from external packages.
948
972
  }
949
973
  },
950
974
  translationArgPosition: 1,
975
+ hideDistDir: "dist",
951
976
  onConfigGeneration: async (event) => {
952
977
  event.logger.info(
953
978
  "Config generation event is not configured. Add onConfigGeneration handler to customize config generation."
@@ -986,6 +1011,9 @@ async function $LT_ReadConfig(projectPath) {
986
1011
  if (!config.collect.collector) {
987
1012
  throw new Error("Collector not found! (config.collect.collector)");
988
1013
  }
1014
+ if (config.isLibrary && (config.enforceLibraryTagPrefix ?? true) && config.tagName && !config.tagName.startsWith("_")) {
1015
+ config.tagName = `_${config.tagName}`;
1016
+ }
989
1017
  return config;
990
1018
  } catch (error) {
991
1019
  throw error;
@@ -1330,9 +1358,9 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
1330
1358
  console.error("Failed to colorize config:", error);
1331
1359
  }
1332
1360
  }
1333
- const encodedPath = encodeURI(filePath);
1361
+ const fileUrl = formatFileUrlForDisplay(filePath);
1334
1362
  console.log(
1335
- `${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`
1363
+ `${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}${fileUrl}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`
1336
1364
  );
1337
1365
  printLines(colorizedWhole.split("\n"), startLine, errorLines, condense);
1338
1366
  } catch (error) {
@@ -1462,9 +1490,14 @@ async function $LT_GetCommandEssentials() {
1462
1490
  };
1463
1491
  }
1464
1492
  async function $LT_CMD_Collect(options) {
1493
+ const startTime = Date.now();
1465
1494
  const { config, logger } = await $LT_GetCommandEssentials();
1466
1495
  logger.info("Collecting translations from source files...");
1467
- const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
1496
+ const files = await $LT_CollectCandidateFilesWithTags({
1497
+ config,
1498
+ logger,
1499
+ skipEmptyNamespaceCheck: config.isLibrary
1500
+ });
1468
1501
  if (config.debug) {
1469
1502
  for (let file of files) {
1470
1503
  logger.debug("Found {count} translations tags inside: {file}", {
@@ -1473,8 +1506,19 @@ async function $LT_CMD_Collect(options) {
1473
1506
  });
1474
1507
  }
1475
1508
  }
1509
+ const totalTags = files.reduce((sum, file) => sum + file.tags.length, 0);
1476
1510
  if (config.isLibrary) {
1511
+ if (totalTags === 0 && (config.enforceLibraryTagPrefix ?? true) && config.tagName) {
1512
+ const baseTagName = config.tagName.startsWith("_") ? config.tagName.substring(1) : config.tagName;
1513
+ console.log("");
1514
+ logger.warn(
1515
+ '⚠️ No translation tags found in your library code.\n This might be because enforceLibraryTagPrefix is enabled.\n Remember: your tag function must be named {prefixedBaseTagName} (with "_" prefix), not {baseTagName}.\n Example: export function {prefixedBaseTagName}(...) instead of export function {baseTagName}(...)\n The prefix prevents the tag from appearing in TypeScript autocomplete after compilation.',
1516
+ { prefixedBaseTagName: `_${baseTagName}`, baseTagName }
1517
+ );
1518
+ }
1477
1519
  await $LT_WriteAsExportFile({ config, logger, files });
1520
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1521
+ logger.debug("Collection completed ({time})", { time: executionTime });
1478
1522
  return;
1479
1523
  }
1480
1524
  try {
@@ -1483,10 +1527,6 @@ async function $LT_CMD_Collect(options) {
1483
1527
  files,
1484
1528
  config
1485
1529
  });
1486
- const totalTags = files.reduce(
1487
- (sum, file) => sum + file.tags.length,
1488
- 0
1489
- );
1490
1530
  logger.debug("Found {totalTags} translation tags", { totalTags });
1491
1531
  await $LT_WriteToCollections({
1492
1532
  config,
@@ -1494,15 +1534,252 @@ async function $LT_CMD_Collect(options) {
1494
1534
  logger,
1495
1535
  clean: options?.clean
1496
1536
  });
1537
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1538
+ logger.debug("Collection completed ({time})", { time: executionTime });
1497
1539
  } catch (e) {
1498
1540
  const prefix = "LangTagConflictResolution:";
1499
1541
  if (e.message.startsWith(prefix)) {
1500
1542
  logger.error(e.message.substring(prefix.length));
1543
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1544
+ logger.debug("Collection completed ({time})", {
1545
+ time: executionTime
1546
+ });
1501
1547
  return;
1502
1548
  }
1503
1549
  throw e;
1504
1550
  }
1505
1551
  }
1552
+ function $LT_HideExportsInDtsFile(dtsFilePath, variableNames) {
1553
+ const originalContent = readFileSync(dtsFilePath, "utf-8");
1554
+ const project = new Project({
1555
+ skipAddingFilesFromTsConfig: true,
1556
+ skipFileDependencyResolution: true,
1557
+ skipLoadingLibFiles: true
1558
+ });
1559
+ const sourceFile = project.addSourceFileAtPath(dtsFilePath);
1560
+ let hasChanges = false;
1561
+ const exportsToHide = [];
1562
+ const processedStatements = /* @__PURE__ */ new Set();
1563
+ for (const declaration of sourceFile.getVariableDeclarations()) {
1564
+ const name = declaration.getName();
1565
+ if (variableNames.has(name)) {
1566
+ const parent = declaration.getParent();
1567
+ if (parent && parent.getKindName() === "VariableDeclarationList") {
1568
+ const varList = parent;
1569
+ const grandParent = varList.getParent();
1570
+ if (grandParent && grandParent.getKindName() === "VariableStatement") {
1571
+ const varStatement = grandParent;
1572
+ if (varStatement.hasExportKeyword()) {
1573
+ if (!processedStatements.has(varStatement)) {
1574
+ processedStatements.add(varStatement);
1575
+ exportsToHide.push(name);
1576
+ varStatement.toggleModifier("export", false);
1577
+ hasChanges = true;
1578
+ }
1579
+ }
1580
+ }
1581
+ }
1582
+ }
1583
+ }
1584
+ if (!hasChanges) {
1585
+ return {
1586
+ hiddenCount: 0,
1587
+ modifiedContent: originalContent,
1588
+ originalContent
1589
+ };
1590
+ }
1591
+ const modifiedContent = sourceFile.getFullText();
1592
+ return {
1593
+ hiddenCount: exportsToHide.length,
1594
+ modifiedContent,
1595
+ originalContent
1596
+ };
1597
+ }
1598
+ function extractPrefixesFromIncludes(includes) {
1599
+ const prefixes = /* @__PURE__ */ new Set();
1600
+ for (const pattern of includes) {
1601
+ const groupMatch = pattern.match(/^\(([^)]+)\)\/\*\*/);
1602
+ if (groupMatch) {
1603
+ const options = groupMatch[1].split("|").map((s) => s.trim());
1604
+ options.forEach((opt) => prefixes.add(opt));
1605
+ continue;
1606
+ }
1607
+ const simpleMatch = pattern.match(/^([^/]+)\/\*\*/);
1608
+ if (simpleMatch) {
1609
+ prefixes.add(simpleMatch[1]);
1610
+ continue;
1611
+ }
1612
+ const singleMatch = pattern.match(/^([^/]+)\//);
1613
+ if (singleMatch) {
1614
+ prefixes.add(singleMatch[1]);
1615
+ }
1616
+ }
1617
+ return Array.from(prefixes);
1618
+ }
1619
+ function findMatchingDtsFile(sourceFilePath, sourceRelativePath, dtsFileMap, config) {
1620
+ const sourceBaseName = path__default.basename(
1621
+ sourceFilePath,
1622
+ path__default.extname(sourceFilePath)
1623
+ );
1624
+ const relativePathKey = sourceRelativePath.replace(
1625
+ /\.(ts|tsx|js|jsx)$/,
1626
+ ""
1627
+ );
1628
+ let dtsFilePath = dtsFileMap.get(relativePathKey);
1629
+ if (dtsFilePath) {
1630
+ return { dtsFilePath, strategy: "relative-path" };
1631
+ }
1632
+ if (config?.includes) {
1633
+ const prefixes = extractPrefixesFromIncludes(config.includes);
1634
+ for (const prefix of prefixes) {
1635
+ const prefixPattern = new RegExp(
1636
+ `^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`
1637
+ );
1638
+ if (prefixPattern.test(sourceRelativePath)) {
1639
+ const strippedPath = sourceRelativePath.replace(
1640
+ prefixPattern,
1641
+ ""
1642
+ );
1643
+ const strippedPathKey = strippedPath.replace(
1644
+ /\.(ts|tsx|js|jsx)$/,
1645
+ ""
1646
+ );
1647
+ dtsFilePath = dtsFileMap.get(strippedPathKey);
1648
+ if (dtsFilePath) {
1649
+ return {
1650
+ dtsFilePath,
1651
+ strategy: "includes-prefix-stripped"
1652
+ };
1653
+ }
1654
+ }
1655
+ }
1656
+ }
1657
+ dtsFilePath = dtsFileMap.get(sourceBaseName);
1658
+ if (dtsFilePath) {
1659
+ return { dtsFilePath, strategy: "base-name" };
1660
+ }
1661
+ return { dtsFilePath: null, strategy: null };
1662
+ }
1663
+ async function $LT_MatchSourceToDtsFiles(files, distPath, cwd, config) {
1664
+ const sourceFileToVariables = /* @__PURE__ */ new Map();
1665
+ for (const file of files) {
1666
+ const sourceFilePath = path__default.resolve(cwd, file.relativeFilePath);
1667
+ const variableNames = /* @__PURE__ */ new Set();
1668
+ for (const tag of file.tags) {
1669
+ if (tag.variableName) {
1670
+ variableNames.add(tag.variableName);
1671
+ }
1672
+ }
1673
+ if (variableNames.size > 0) {
1674
+ sourceFileToVariables.set(sourceFilePath, variableNames);
1675
+ }
1676
+ }
1677
+ if (sourceFileToVariables.size === 0) {
1678
+ return [];
1679
+ }
1680
+ const dtsFiles = await globby("**/*.d.ts", {
1681
+ cwd: distPath,
1682
+ absolute: true
1683
+ });
1684
+ if (dtsFiles.length === 0) {
1685
+ return [];
1686
+ }
1687
+ const dtsFileMap = /* @__PURE__ */ new Map();
1688
+ for (const dtsFile of dtsFiles) {
1689
+ const relativeDtsPath = path__default.relative(distPath, dtsFile);
1690
+ const baseName = path__default.basename(dtsFile, ".d.ts");
1691
+ const relativePathWithoutExt = relativeDtsPath.replace(/\.d\.ts$/, "");
1692
+ dtsFileMap.set(relativePathWithoutExt, dtsFile);
1693
+ dtsFileMap.set(baseName, dtsFile);
1694
+ }
1695
+ const matches = [];
1696
+ const processedDtsFiles = /* @__PURE__ */ new Set();
1697
+ for (const [sourceFilePath, variableNames] of sourceFileToVariables) {
1698
+ const sourceRelativePath = path__default.relative(cwd, sourceFilePath);
1699
+ const match = findMatchingDtsFile(
1700
+ sourceFilePath,
1701
+ sourceRelativePath,
1702
+ dtsFileMap,
1703
+ config
1704
+ );
1705
+ if (!match.dtsFilePath) {
1706
+ continue;
1707
+ }
1708
+ if (processedDtsFiles.has(match.dtsFilePath)) {
1709
+ continue;
1710
+ }
1711
+ processedDtsFiles.add(match.dtsFilePath);
1712
+ matches.push({
1713
+ sourceFilePath,
1714
+ sourceRelativePath,
1715
+ dtsFilePath: match.dtsFilePath,
1716
+ variableNames
1717
+ });
1718
+ }
1719
+ return matches;
1720
+ }
1721
+ async function $LT_CMD_HideCompiledExports(options) {
1722
+ const { config, logger } = await $LT_GetCommandEssentials();
1723
+ const distDir = options?.distDir || config.hideDistDir || "dist";
1724
+ const distPath = path__default.resolve(process__default.cwd(), distDir);
1725
+ if (!existsSync(distPath)) {
1726
+ logger.warn("Dist directory does not exist: {distPath}", { distPath });
1727
+ return;
1728
+ }
1729
+ logger.info("Scanning source files for lang-tag variables...");
1730
+ const files = await $LT_CollectCandidateFilesWithTags({
1731
+ config,
1732
+ logger,
1733
+ skipEmptyNamespaceCheck: true
1734
+ });
1735
+ const matches = await $LT_MatchSourceToDtsFiles(
1736
+ files,
1737
+ distPath,
1738
+ process__default.cwd(),
1739
+ config
1740
+ );
1741
+ if (matches.length === 0) {
1742
+ logger.info(
1743
+ "No lang-tag variables found in source files or no matching .d.ts files found."
1744
+ );
1745
+ return;
1746
+ }
1747
+ logger.info("Found {count} .d.ts files to process", {
1748
+ count: matches.length
1749
+ });
1750
+ let hiddenCount = 0;
1751
+ for (const match of matches) {
1752
+ try {
1753
+ const result = $LT_HideExportsInDtsFile(
1754
+ match.dtsFilePath,
1755
+ match.variableNames
1756
+ );
1757
+ if (!result.hiddenCount) {
1758
+ continue;
1759
+ }
1760
+ writeFileSync(match.dtsFilePath, result.modifiedContent, "utf-8");
1761
+ hiddenCount += result.hiddenCount;
1762
+ const hiddenVariables = Array.from(match.variableNames).join(", ");
1763
+ logger.debug(
1764
+ "Hidden exports from {file}: {variables} (from {sourceFile})",
1765
+ {
1766
+ file: path__default.relative(process__default.cwd(), match.dtsFilePath),
1767
+ variables: hiddenVariables,
1768
+ sourceFile: match.sourceRelativePath
1769
+ }
1770
+ );
1771
+ } catch (error) {
1772
+ logger.warn("Error processing file {file}: {error}", {
1773
+ file: match.sourceRelativePath,
1774
+ error: error.message || String(error)
1775
+ });
1776
+ }
1777
+ }
1778
+ logger.success("Hidden {hiddenCount} exports from {fileCount} files.", {
1779
+ hiddenCount,
1780
+ fileCount: matches.length
1781
+ });
1782
+ }
1506
1783
  async function $LT_CollectExportFiles(logger) {
1507
1784
  const nodeModulesPath = path$1.join(process__default.cwd(), "node_modules");
1508
1785
  if (!fs.existsSync(nodeModulesPath)) {
@@ -1621,11 +1898,11 @@ async function generateImportFiles(config, logger, importManager) {
1621
1898
  const content = renderTemplate$2(templateData);
1622
1899
  await $LT_EnsureDirectoryExists(dirname(filePath));
1623
1900
  await writeFile(filePath, content, "utf-8");
1624
- const encodedFilePath = encodeURI(filePath);
1901
+ const fileUrl = formatFileUrlForDisplay(filePath);
1625
1902
  logger.success('Created tag file: "{file}"', {
1626
1903
  file: importedFile.pathRelativeToImportDir
1627
1904
  });
1628
- logger.debug(" └── link: file://{path}", { path: encodedFilePath });
1905
+ logger.debug(" └── link: {url}", { url: fileUrl });
1629
1906
  }
1630
1907
  }
1631
1908
  class ImportManager {
@@ -2124,7 +2401,10 @@ async function detectInitTagOptions(options, config) {
2124
2401
  const isTypeScript = options.typescript !== void 0 ? options.typescript : detectTypeScript(packageJson);
2125
2402
  const isReact = options.react !== void 0 ? options.react : detectReact(packageJson);
2126
2403
  const isLibrary = options.library !== void 0 ? options.library : config.isLibrary;
2127
- const tagName = options.name || config.tagName || "lang";
2404
+ let tagName = options.name || config.tagName || "lang";
2405
+ if (isLibrary && (config.enforceLibraryTagPrefix ?? true) && !tagName.startsWith("_")) {
2406
+ tagName = `_${tagName}`;
2407
+ }
2128
2408
  const fileExtension = isLibrary && isReact ? isTypeScript ? "tsx" : "jsx" : isTypeScript ? "ts" : "js";
2129
2409
  return {
2130
2410
  tagName,
@@ -2225,22 +2505,22 @@ async function $LT_CMD_InitTagFile(options = {}) {
2225
2505
  "2. Create your translation objects and use the tag function"
2226
2506
  );
2227
2507
  logger.info('3. Run "lang-tag collect" to extract translations');
2508
+ if (renderOptions.isLibrary && renderOptions.tagName.startsWith("_") && (config.enforceLibraryTagPrefix ?? true)) {
2509
+ console.log("");
2510
+ logger.info(
2511
+ '📌 Important: Library tag prefix enforcement is enabled\n Your tag uses "_" prefix: {tagName} (instead of {baseTagName})\n This prevents the tag from appearing in TypeScript autocomplete after compilation\n Always use {tagName} (with prefix) in your library code\n This is a best practice for library internals - it keeps your API clean\n The prefix is automatically added by the enforceLibraryTagPrefix option\n To disable this behavior, set enforceLibraryTagPrefix: false in your config',
2512
+ {
2513
+ tagName: renderOptions.tagName,
2514
+ baseTagName: renderOptions.tagName.substring(1)
2515
+ }
2516
+ );
2517
+ }
2228
2518
  } catch (error) {
2229
2519
  logger.error("Failed to write file: {error}", {
2230
2520
  error: error?.message
2231
2521
  });
2232
2522
  }
2233
2523
  }
2234
- function deepFreezeObject(obj) {
2235
- const propNames = Object.getOwnPropertyNames(obj);
2236
- for (const name of propNames) {
2237
- const value = obj[name];
2238
- if (value && typeof value === "object") {
2239
- deepFreezeObject(value);
2240
- }
2241
- }
2242
- return Object.freeze(obj);
2243
- }
2244
2524
  async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
2245
2525
  let libraryImportsDir = config.import.dir;
2246
2526
  if (!libraryImportsDir.endsWith(sep)) libraryImportsDir += sep;
@@ -2274,13 +2554,23 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
2274
2554
  event.isSaved = true;
2275
2555
  event.savedConfig = updatedConfig;
2276
2556
  logger.debug(
2277
- 'Called save for "{path}" with config "{config}" triggered by: ("{trigger}")',
2557
+ 'Called save for "{path}"{varName} with config "{config}" triggered by: ("{trigger}")',
2278
2558
  {
2279
2559
  path: path2,
2280
2560
  config: JSON.stringify(updatedConfig),
2281
- trigger: triggerName || "-"
2561
+ trigger: triggerName || "-",
2562
+ varName: tag.variableName ? `(${tag.variableName})` : ""
2282
2563
  }
2283
2564
  );
2565
+ },
2566
+ getCurrentConfig: () => {
2567
+ if (event.savedConfig !== void 0 && event.savedConfig !== null) {
2568
+ return { ...event.savedConfig };
2569
+ }
2570
+ if (event.config) {
2571
+ return { ...event.config };
2572
+ }
2573
+ return {};
2284
2574
  }
2285
2575
  };
2286
2576
  await config.onConfigGeneration(event);
@@ -2295,10 +2585,10 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
2295
2585
  if (replacements.length) {
2296
2586
  const newContent = processor.replaceTags(fileContent, replacements);
2297
2587
  await writeFile(file, newContent, "utf-8");
2298
- const encodedFile = encodeURI(file);
2588
+ const fileUrl = formatFileUrlForDisplay(file);
2299
2589
  logger.info(
2300
- 'Lang tag configurations written for file "{path}" (file://{file}:{line})',
2301
- { path: path2, file: encodedFile, line: lastUpdatedLine }
2590
+ 'Lang tag configurations written for file "{path}" ({url}:{line})',
2591
+ { path: path2, url: fileUrl, line: lastUpdatedLine }
2302
2592
  );
2303
2593
  return true;
2304
2594
  }
@@ -2311,6 +2601,7 @@ function isConfigSame(c1, c2) {
2311
2601
  return false;
2312
2602
  }
2313
2603
  async function $LT_CMD_RegenerateTags() {
2604
+ const startTime = Date.now();
2314
2605
  const { config, logger } = await $LT_GetCommandEssentials();
2315
2606
  const files = await globby(config.includes, {
2316
2607
  cwd: process.cwd(),
@@ -2331,11 +2622,13 @@ async function $LT_CMD_RegenerateTags() {
2331
2622
  dirty = true;
2332
2623
  }
2333
2624
  }
2625
+ const executionTime = formatExecutionTime(Date.now() - startTime);
2334
2626
  if (!dirty) {
2335
2627
  logger.info(
2336
2628
  "No changes were made based on the current configuration and files"
2337
2629
  );
2338
2630
  }
2631
+ logger.debug("Regeneration completed ({time})", { time: executionTime });
2339
2632
  }
2340
2633
  function getBasePath(pattern) {
2341
2634
  const globStartIndex = pattern.indexOf("*");
@@ -2444,6 +2737,14 @@ function createCli() {
2444
2737
  ).action(async (options) => {
2445
2738
  await $LT_CMD_InitTagFile(options);
2446
2739
  });
2740
+ program.command("hide-compiled-exports").alias("hce").description(
2741
+ "Hide compiled .d.ts exports of lang-tag variables (remove export modifier, keep type)"
2742
+ ).option(
2743
+ "-d, --dist-dir <dir>",
2744
+ 'Dist directory to process (default: from config or "dist")'
2745
+ ).action(async (options) => {
2746
+ await $LT_CMD_HideCompiledExports({ distDir: options.distDir });
2747
+ });
2447
2748
  return program;
2448
2749
  }
2449
2750
  createCli().parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -45,7 +45,8 @@
45
45
  "json5": "^2.2.3",
46
46
  "micromatch": "^4.0.8",
47
47
  "mustache": "^4.2.0",
48
- "pathe": "^2.0.3"
48
+ "pathe": "^2.0.3",
49
+ "ts-morph": "^24.0.0"
49
50
  },
50
51
  "keywords": [
51
52
  "i18n",
@@ -1,8 +1,8 @@
1
1
  {{#isTypeScript}}
2
2
  import {
3
- CallableTranslations,
4
- LangTagTranslations,
5
- LangTagTranslationsConfig,
3
+ type CallableTranslations,
4
+ type LangTagTranslations,
5
+ type LangTagTranslationsConfig,
6
6
  createCallableTranslations
7
7
  } from 'lang-tag';
8
8
  {{/isTypeScript}}
@@ -10,7 +10,7 @@ import {
10
10
  import { createCallableTranslations } from 'lang-tag';
11
11
  {{/isTypeScript}}
12
12
  {{#isReact}}
13
- import React, { ReactNode, useMemo } from 'react';
13
+ import React, { type ReactNode, useMemo } from 'react';
14
14
  {{/isReact}}
15
15
 
16
16
  {{#isTypeScript}}
@@ -1,9 +1,9 @@
1
1
  {{#isTypeScript}}
2
2
  import {
3
- CallableTranslations,
4
- LangTagTranslations,
5
- LangTagTranslationsConfig,
6
- PartialFlexibleTranslations,
3
+ type CallableTranslations,
4
+ type LangTagTranslations,
5
+ type LangTagTranslationsConfig,
6
+ type PartialFlexibleTranslations,
7
7
  createCallableTranslations,
8
8
  normalizeTranslations,
9
9
  lookupTranslation
@@ -17,7 +17,7 @@ import React, {
17
17
  createContext,
18
18
  useContext,
19
19
  useMemo,
20
- ReactNode
20
+ type ReactNode
21
21
  } from 'react';
22
22
  {{/isReact}}
23
23
 
@@ -30,8 +30,16 @@ export function processPlaceholders(
30
30
  }
31
31
 
32
32
  const key = placeholder.trim();
33
- if (params && key in params) {
34
- parts.push(params[key]);
33
+ const value = params?.[key];
34
+
35
+ if (React.isValidElement(value)) {
36
+ parts.push(value);
37
+ } else if (
38
+ typeof value === 'string' ||
39
+ typeof value === 'number' ||
40
+ typeof value === 'boolean'
41
+ ) {
42
+ parts.push(String(value));
35
43
  } else {
36
44
  parts.push('');
37
45
  }
@@ -49,12 +57,11 @@ export function processPlaceholders(
49
57
  return parts.join('');
50
58
  }
51
59
 
52
- return parts.map((part, index) => {
53
- if (React.isValidElement(part)) {
54
- return React.cloneElement(part, { key: index });
55
- }
56
- return React.createElement(React.Fragment, { key: index }, part);
57
- }){{#isTypeScript}} as unknown as string{{/isTypeScript}};
60
+ return parts.map((part, index) =>
61
+ React.isValidElement(part)
62
+ ? React.cloneElement(part, { key: index })
63
+ : React.createElement(React.Fragment, { key: index }, part)
64
+ ){{#isTypeScript}} as unknown as string{{/isTypeScript}};
58
65
  }
59
66
  {{/isReact}}
60
67
 
package/type.d.ts CHANGED
@@ -40,6 +40,17 @@ export interface LangTagCLIConfig {
40
40
  * @default false
41
41
  */
42
42
  isLibrary: boolean;
43
+ /**
44
+ * When true and isLibrary is true, automatically adds "_" prefix to tagName
45
+ * to prevent the tag from being suggested in TypeScript autocomplete after compilation.
46
+ * This ensures that library tags remain internal and are not exposed in .d.ts files.
47
+ * @default true
48
+ * @example
49
+ * // With enforceLibraryTagPrefix: true and tagName: "lang"
50
+ * // Generated tag function will be: export function _lang(...)
51
+ * // Config tagName also will be automatically set to "_lang"
52
+ */
53
+ enforceLibraryTagPrefix?: boolean;
43
54
  collect?: {
44
55
  /**
45
56
  * Translation collector that defines how translation tags are organized into output files.
@@ -134,6 +145,12 @@ export interface LangTagCLIConfig {
134
145
  * @default 1
135
146
  */
136
147
  translationArgPosition: 1 | 2;
148
+ /**
149
+ * Directory containing compiled TypeScript declaration files (.d.ts) to remove export modifier.
150
+ * Used by the `hide-compiled-exports` command to remove exports of lang-tag variables.
151
+ * @default 'dist'
152
+ */
153
+ hideDistDir?: string;
137
154
  debug?: boolean;
138
155
  }
139
156
  type Validity = 'ok' | 'invalid-param-1' | 'invalid-param-2' | 'translations-not-found';
@@ -234,6 +251,23 @@ export interface LangTagCLIConfigGenerationEvent {
234
251
  * null = means configuration will be removed
235
252
  **/
236
253
  save(config: LangTagTranslationsConfig | null, triggerName?: string): void;
254
+ /**
255
+ * Returns the current configuration object that should be used as a base for modifications.
256
+ * This method provides a reusable way to get the active configuration:
257
+ * - If `save()` was called, returns `savedConfig` (a mutable copy)
258
+ * - Otherwise, returns `config` (a mutable copy)
259
+ * - If neither exists, returns an empty object `{}`
260
+ *
261
+ * The returned object is always a shallow copy, so it can be safely modified.
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * const currentConfig = event.getCurrentConfig();
266
+ * currentConfig.namespace = 'new-namespace';
267
+ * event.save(currentConfig);
268
+ * ```
269
+ */
270
+ getCurrentConfig(): LangTagTranslationsConfig;
237
271
  }
238
272
  export interface LangTagCLICollectConfigFixEvent {
239
273
  config: LangTagTranslationsConfig;